alertmanager/notify/wechat/wechat.go

197 lines
5.3 KiB
Go

// Copyright 2019 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wechat
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"time"
commoncfg "github.com/prometheus/common/config"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/notify"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
)
// Notifier implements a Notifier for wechat notifications.
type Notifier struct {
conf *config.WechatConfig
tmpl *template.Template
logger *slog.Logger
client *http.Client
accessToken string
accessTokenAt time.Time
}
// token is the AccessToken with corpid and corpsecret.
type token struct {
AccessToken string `json:"access_token"`
}
type weChatMessage struct {
Text weChatMessageContent `yaml:"text,omitempty" json:"text,omitempty"`
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
Markdown weChatMessageContent `yaml:"markdown,omitempty" json:"markdown,omitempty"`
}
type weChatMessageContent struct {
Content string `json:"content"`
}
type weChatResponse struct {
Code int `json:"errcode"`
Error string `json:"errmsg"`
}
// New returns a new Wechat notifier.
func New(c *config.WechatConfig, t *template.Template, l *slog.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "wechat", httpOpts...)
if err != nil {
return nil, err
}
return &Notifier{conf: c, tmpl: t, logger: l, client: client}, nil
}
// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return false, err
}
n.logger.Debug("extracted group key", "key", key)
data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
tmpl := notify.TmplText(n.tmpl, data, &err)
if err != nil {
return false, err
}
// Refresh AccessToken over 2 hours
if n.accessToken == "" || time.Since(n.accessTokenAt) > 2*time.Hour {
parameters := url.Values{}
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
if err != nil {
return false, fmt.Errorf("templating error: %w", err)
}
u := n.conf.APIURL.Copy()
u.Path += "gettoken"
u.RawQuery = parameters.Encode()
resp, err := notify.Get(ctx, n.client, u.String())
if err != nil {
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)
var wechatToken token
if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
return false, err
}
if wechatToken.AccessToken == "" {
return false, fmt.Errorf("invalid APISecret for CorpID: %s", n.conf.CorpID)
}
// Cache accessToken
n.accessToken = wechatToken.AccessToken
n.accessTokenAt = time.Now()
}
msg := &weChatMessage{
ToUser: tmpl(n.conf.ToUser),
ToParty: tmpl(n.conf.ToParty),
Totag: tmpl(n.conf.ToTag),
AgentID: tmpl(n.conf.AgentID),
Type: n.conf.MessageType,
Safe: "0",
}
if msg.Type == "markdown" {
msg.Markdown = weChatMessageContent{
Content: tmpl(n.conf.Message),
}
} else {
msg.Text = weChatMessageContent{
Content: tmpl(n.conf.Message),
}
}
if err != nil {
return false, fmt.Errorf("templating error: %w", err)
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return false, err
}
postMessageURL := n.conf.APIURL.Copy()
postMessageURL.Path += "message/send"
q := postMessageURL.Query()
q.Set("access_token", n.accessToken)
postMessageURL.RawQuery = q.Encode()
resp, err := notify.PostJSON(ctx, n.client, postMessageURL.String(), &buf)
if err != nil {
return true, notify.RedactURL(err)
}
defer notify.Drain(resp)
if resp.StatusCode != 200 {
return true, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), fmt.Errorf("unexpected status code %v", resp.StatusCode))
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return true, err
}
n.logger.Debug(string(body), "incident", key)
var weResp weChatResponse
if err := json.Unmarshal(body, &weResp); err != nil {
return true, err
}
// https://work.weixin.qq.com/api/doc#10649
if weResp.Code == 0 {
return false, nil
}
// AccessToken is expired
if weResp.Code == 42001 {
n.accessToken = ""
return true, errors.New(weResp.Error)
}
return false, errors.New(weResp.Error)
}