197 lines
5.3 KiB
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)
|
|
}
|