1
2
3
4
5
6
7
8
9
10
11
12
13
14 package wechat
15
16 import (
17 "bytes"
18 "context"
19 "encoding/json"
20 "fmt"
21 "io"
22 "net/http"
23 "net/url"
24 "time"
25
26 "github.com/go-kit/log"
27 "github.com/go-kit/log/level"
28 "github.com/pkg/errors"
29 commoncfg "github.com/prometheus/common/config"
30
31 "github.com/prometheus/alertmanager/config"
32 "github.com/prometheus/alertmanager/notify"
33 "github.com/prometheus/alertmanager/template"
34 "github.com/prometheus/alertmanager/types"
35 )
36
37
38 type Notifier struct {
39 conf *config.WechatConfig
40 tmpl *template.Template
41 logger log.Logger
42 client *http.Client
43
44 accessToken string
45 accessTokenAt time.Time
46 }
47
48
49 type token struct {
50 AccessToken string `json:"access_token"`
51 }
52
53 type weChatMessage struct {
54 Text weChatMessageContent `yaml:"text,omitempty" json:"text,omitempty"`
55 ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
56 ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
57 Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
58 AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
59 Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
60 Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
61 Markdown weChatMessageContent `yaml:"markdown,omitempty" json:"markdown,omitempty"`
62 }
63
64 type weChatMessageContent struct {
65 Content string `json:"content"`
66 }
67
68 type weChatResponse struct {
69 Code int `json:"code"`
70 Error string `json:"error"`
71 }
72
73
74 func New(c *config.WechatConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
75 client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "wechat", httpOpts...)
76 if err != nil {
77 return nil, err
78 }
79
80 return &Notifier{conf: c, tmpl: t, logger: l, client: client}, nil
81 }
82
83
84 func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
85 key, err := notify.ExtractGroupKey(ctx)
86 if err != nil {
87 return false, err
88 }
89
90 level.Debug(n.logger).Log("incident", key)
91 data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
92
93 tmpl := notify.TmplText(n.tmpl, data, &err)
94 if err != nil {
95 return false, err
96 }
97
98
99 if n.accessToken == "" || time.Since(n.accessTokenAt) > 2*time.Hour {
100 parameters := url.Values{}
101 parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
102 parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
103 if err != nil {
104 return false, fmt.Errorf("templating error: %s", err)
105 }
106
107 u := n.conf.APIURL.Copy()
108 u.Path += "gettoken"
109 u.RawQuery = parameters.Encode()
110
111 resp, err := notify.Get(ctx, n.client, u.String())
112 if err != nil {
113 return true, notify.RedactURL(err)
114 }
115 defer notify.Drain(resp)
116
117 var wechatToken token
118 if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
119 return false, err
120 }
121
122 if wechatToken.AccessToken == "" {
123 return false, fmt.Errorf("invalid APISecret for CorpID: %s", n.conf.CorpID)
124 }
125
126
127 n.accessToken = wechatToken.AccessToken
128 n.accessTokenAt = time.Now()
129 }
130
131 msg := &weChatMessage{
132 ToUser: tmpl(n.conf.ToUser),
133 ToParty: tmpl(n.conf.ToParty),
134 Totag: tmpl(n.conf.ToTag),
135 AgentID: tmpl(n.conf.AgentID),
136 Type: n.conf.MessageType,
137 Safe: "0",
138 }
139
140 if msg.Type == "markdown" {
141 msg.Markdown = weChatMessageContent{
142 Content: tmpl(n.conf.Message),
143 }
144 } else {
145 msg.Text = weChatMessageContent{
146 Content: tmpl(n.conf.Message),
147 }
148 }
149 if err != nil {
150 return false, fmt.Errorf("templating error: %s", err)
151 }
152
153 var buf bytes.Buffer
154 if err := json.NewEncoder(&buf).Encode(msg); err != nil {
155 return false, err
156 }
157
158 postMessageURL := n.conf.APIURL.Copy()
159 postMessageURL.Path += "message/send"
160 q := postMessageURL.Query()
161 q.Set("access_token", n.accessToken)
162 postMessageURL.RawQuery = q.Encode()
163
164 resp, err := notify.PostJSON(ctx, n.client, postMessageURL.String(), &buf)
165 if err != nil {
166 return true, notify.RedactURL(err)
167 }
168 defer notify.Drain(resp)
169
170 if resp.StatusCode != 200 {
171 return true, fmt.Errorf("unexpected status code %v", resp.StatusCode)
172 }
173
174 body, err := io.ReadAll(resp.Body)
175 if err != nil {
176 return true, err
177 }
178 level.Debug(n.logger).Log("response", string(body), "incident", key)
179
180 var weResp weChatResponse
181 if err := json.Unmarshal(body, &weResp); err != nil {
182 return true, err
183 }
184
185
186 if weResp.Code == 0 {
187 return false, nil
188 }
189
190
191 if weResp.Code == 42001 {
192 n.accessToken = ""
193 return true, errors.New(weResp.Error)
194 }
195
196 return false, errors.New(weResp.Error)
197 }
198
View as plain text