1
2
3
4
5
6
7
8
9
10
11
12
13
14 package opsgenie
15
16 import (
17 "bytes"
18 "context"
19 "encoding/json"
20 "fmt"
21 "net/http"
22 "os"
23 "strings"
24
25 "github.com/go-kit/log"
26 "github.com/go-kit/log/level"
27 "github.com/pkg/errors"
28 commoncfg "github.com/prometheus/common/config"
29 "github.com/prometheus/common/model"
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 const maxMessageLenRunes = 130
39
40
41 type Notifier struct {
42 conf *config.OpsGenieConfig
43 tmpl *template.Template
44 logger log.Logger
45 client *http.Client
46 retrier *notify.Retrier
47 }
48
49
50 func New(c *config.OpsGenieConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
51 client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "opsgenie", httpOpts...)
52 if err != nil {
53 return nil, err
54 }
55 return &Notifier{
56 conf: c,
57 tmpl: t,
58 logger: l,
59 client: client,
60 retrier: ¬ify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
61 }, nil
62 }
63
64 type opsGenieCreateMessage struct {
65 Alias string `json:"alias"`
66 Message string `json:"message"`
67 Description string `json:"description,omitempty"`
68 Details map[string]string `json:"details"`
69 Source string `json:"source"`
70 Responders []opsGenieCreateMessageResponder `json:"responders,omitempty"`
71 Tags []string `json:"tags,omitempty"`
72 Note string `json:"note,omitempty"`
73 Priority string `json:"priority,omitempty"`
74 Entity string `json:"entity,omitempty"`
75 Actions []string `json:"actions,omitempty"`
76 }
77
78 type opsGenieCreateMessageResponder struct {
79 ID string `json:"id,omitempty"`
80 Name string `json:"name,omitempty"`
81 Username string `json:"username,omitempty"`
82 Type string `json:"type"`
83 }
84
85 type opsGenieCloseMessage struct {
86 Source string `json:"source"`
87 }
88
89 type opsGenieUpdateMessageMessage struct {
90 Message string `json:"message,omitempty"`
91 }
92
93 type opsGenieUpdateDescriptionMessage struct {
94 Description string `json:"description,omitempty"`
95 }
96
97
98 func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
99 requests, retry, err := n.createRequests(ctx, as...)
100 if err != nil {
101 return retry, err
102 }
103
104 for _, req := range requests {
105 req.Header.Set("User-Agent", notify.UserAgentHeader)
106 resp, err := n.client.Do(req)
107 if err != nil {
108 return true, err
109 }
110 shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
111 notify.Drain(resp)
112 if err != nil {
113 return shouldRetry, err
114 }
115 }
116 return true, nil
117 }
118
119
120 func safeSplit(s, sep string) []string {
121 a := strings.Split(strings.TrimSpace(s), sep)
122 b := a[:0]
123 for _, x := range a {
124 if x != "" {
125 b = append(b, x)
126 }
127 }
128 return b
129 }
130
131
132 func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*http.Request, bool, error) {
133 key, err := notify.ExtractGroupKey(ctx)
134 if err != nil {
135 return nil, false, err
136 }
137 data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
138
139 level.Debug(n.logger).Log("alert", key)
140
141 tmpl := notify.TmplText(n.tmpl, data, &err)
142
143 details := make(map[string]string)
144
145 for k, v := range data.CommonLabels {
146 details[k] = v
147 }
148
149 for k, v := range n.conf.Details {
150 details[k] = tmpl(v)
151 }
152
153 requests := []*http.Request{}
154
155 var (
156 alias = key.Hash()
157 alerts = types.Alerts(as...)
158 )
159 switch alerts.Status() {
160 case model.AlertResolved:
161 resolvedEndpointURL := n.conf.APIURL.Copy()
162 resolvedEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/close", alias)
163 q := resolvedEndpointURL.Query()
164 q.Set("identifierType", "alias")
165 resolvedEndpointURL.RawQuery = q.Encode()
166 msg := &opsGenieCloseMessage{Source: tmpl(n.conf.Source)}
167 var buf bytes.Buffer
168 if err := json.NewEncoder(&buf).Encode(msg); err != nil {
169 return nil, false, err
170 }
171 req, err := http.NewRequest("POST", resolvedEndpointURL.String(), &buf)
172 if err != nil {
173 return nil, true, err
174 }
175 requests = append(requests, req.WithContext(ctx))
176 default:
177 message, truncated := notify.TruncateInRunes(tmpl(n.conf.Message), maxMessageLenRunes)
178 if truncated {
179 level.Warn(n.logger).Log("msg", "Truncated message", "alert", key, "max_runes", maxMessageLenRunes)
180 }
181
182 createEndpointURL := n.conf.APIURL.Copy()
183 createEndpointURL.Path += "v2/alerts"
184
185 var responders []opsGenieCreateMessageResponder
186 for _, r := range n.conf.Responders {
187 responder := opsGenieCreateMessageResponder{
188 ID: tmpl(r.ID),
189 Name: tmpl(r.Name),
190 Username: tmpl(r.Username),
191 Type: tmpl(r.Type),
192 }
193
194 if responder == (opsGenieCreateMessageResponder{}) {
195
196
197 continue
198 }
199
200 if responder.Type == "teams" {
201 teams := safeSplit(responder.Name, ",")
202 for _, team := range teams {
203 newResponder := opsGenieCreateMessageResponder{
204 Name: tmpl(team),
205 Type: tmpl("team"),
206 }
207 responders = append(responders, newResponder)
208 }
209 continue
210 }
211
212 responders = append(responders, responder)
213 }
214
215 msg := &opsGenieCreateMessage{
216 Alias: alias,
217 Message: message,
218 Description: tmpl(n.conf.Description),
219 Details: details,
220 Source: tmpl(n.conf.Source),
221 Responders: responders,
222 Tags: safeSplit(tmpl(n.conf.Tags), ","),
223 Note: tmpl(n.conf.Note),
224 Priority: tmpl(n.conf.Priority),
225 Entity: tmpl(n.conf.Entity),
226 Actions: safeSplit(tmpl(n.conf.Actions), ","),
227 }
228 var buf bytes.Buffer
229 if err := json.NewEncoder(&buf).Encode(msg); err != nil {
230 return nil, false, err
231 }
232 req, err := http.NewRequest("POST", createEndpointURL.String(), &buf)
233 if err != nil {
234 return nil, true, err
235 }
236 requests = append(requests, req.WithContext(ctx))
237
238 if n.conf.UpdateAlerts {
239 updateMessageEndpointURL := n.conf.APIURL.Copy()
240 updateMessageEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/message", alias)
241 q := updateMessageEndpointURL.Query()
242 q.Set("identifierType", "alias")
243 updateMessageEndpointURL.RawQuery = q.Encode()
244 updateMsgMsg := &opsGenieUpdateMessageMessage{
245 Message: msg.Message,
246 }
247 var updateMessageBuf bytes.Buffer
248 if err := json.NewEncoder(&updateMessageBuf).Encode(updateMsgMsg); err != nil {
249 return nil, false, err
250 }
251 req, err := http.NewRequest("PUT", updateMessageEndpointURL.String(), &updateMessageBuf)
252 if err != nil {
253 return nil, true, err
254 }
255 requests = append(requests, req)
256
257 updateDescriptionEndpointURL := n.conf.APIURL.Copy()
258 updateDescriptionEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/description", alias)
259 q = updateDescriptionEndpointURL.Query()
260 q.Set("identifierType", "alias")
261 updateDescriptionEndpointURL.RawQuery = q.Encode()
262 updateDescMsg := &opsGenieUpdateDescriptionMessage{
263 Description: msg.Description,
264 }
265
266 var updateDescriptionBuf bytes.Buffer
267 if err := json.NewEncoder(&updateDescriptionBuf).Encode(updateDescMsg); err != nil {
268 return nil, false, err
269 }
270 req, err = http.NewRequest("PUT", updateDescriptionEndpointURL.String(), &updateDescriptionBuf)
271 if err != nil {
272 return nil, true, err
273 }
274 requests = append(requests, req.WithContext(ctx))
275 }
276 }
277
278 var apiKey string
279 if n.conf.APIKey != "" {
280 apiKey = tmpl(string(n.conf.APIKey))
281 } else {
282 content, err := os.ReadFile(n.conf.APIKeyFile)
283 if err != nil {
284 return nil, false, errors.Wrap(err, "read key_file error")
285 }
286 apiKey = tmpl(string(content))
287 }
288
289 if err != nil {
290 return nil, false, errors.Wrap(err, "templating error")
291 }
292
293 for _, req := range requests {
294 req.Header.Set("Content-Type", "application/json")
295 req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", apiKey))
296 }
297
298 return requests, true, nil
299 }
300
View as plain text