1
2
3
4
5
6
7
8
9
10
11
12
13
14 package victorops
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 = 20480
39
40
41 type Notifier struct {
42 conf *config.VictorOpsConfig
43 tmpl *template.Template
44 logger log.Logger
45 client *http.Client
46 retrier *notify.Retrier
47 }
48
49
50 func New(c *config.VictorOpsConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
51 client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "victorops", 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
61
62 retrier: ¬ify.Retrier{},
63 }, nil
64 }
65
66 const (
67 victorOpsEventTrigger = "CRITICAL"
68 victorOpsEventResolve = "RECOVERY"
69 )
70
71
72 func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
73 var err error
74 var (
75 data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
76 tmpl = notify.TmplText(n.tmpl, data, &err)
77 apiURL = n.conf.APIURL.Copy()
78 )
79
80 var apiKey string
81 if n.conf.APIKey != "" {
82 apiKey = string(n.conf.APIKey)
83 } else {
84 content, fileErr := os.ReadFile(n.conf.APIKeyFile)
85 if fileErr != nil {
86 return false, errors.Wrap(fileErr, "failed to read API key from file")
87 }
88 apiKey = strings.TrimSpace(string(content))
89 }
90
91 apiURL.Path += fmt.Sprintf("%s/%s", apiKey, tmpl(n.conf.RoutingKey))
92 if err != nil {
93 return false, fmt.Errorf("templating error: %s", err)
94 }
95
96 buf, err := n.createVictorOpsPayload(ctx, as...)
97 if err != nil {
98 return true, err
99 }
100
101 resp, err := notify.PostJSON(ctx, n.client, apiURL.String(), buf)
102 if err != nil {
103 return true, notify.RedactURL(err)
104 }
105 defer notify.Drain(resp)
106
107 return n.retrier.Check(resp.StatusCode, resp.Body)
108 }
109
110
111 func (n *Notifier) createVictorOpsPayload(ctx context.Context, as ...*types.Alert) (*bytes.Buffer, error) {
112 victorOpsAllowedEvents := map[string]bool{
113 "INFO": true,
114 "WARNING": true,
115 "CRITICAL": true,
116 }
117
118 key, err := notify.ExtractGroupKey(ctx)
119 if err != nil {
120 return nil, err
121 }
122
123 var (
124 alerts = types.Alerts(as...)
125 data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
126 tmpl = notify.TmplText(n.tmpl, data, &err)
127
128 messageType = tmpl(n.conf.MessageType)
129 stateMessage = tmpl(n.conf.StateMessage)
130 )
131
132 if alerts.Status() == model.AlertFiring && !victorOpsAllowedEvents[messageType] {
133 messageType = victorOpsEventTrigger
134 }
135
136 if alerts.Status() == model.AlertResolved {
137 messageType = victorOpsEventResolve
138 }
139
140 stateMessage, truncated := notify.TruncateInRunes(stateMessage, maxMessageLenRunes)
141 if truncated {
142 level.Warn(n.logger).Log("msg", "Truncated state_message", "incident", key, "max_runes", maxMessageLenRunes)
143 }
144
145 msg := map[string]string{
146 "message_type": messageType,
147 "entity_id": key.Hash(),
148 "entity_display_name": tmpl(n.conf.EntityDisplayName),
149 "state_message": stateMessage,
150 "monitoring_tool": tmpl(n.conf.MonitoringTool),
151 }
152
153 if err != nil {
154 return nil, fmt.Errorf("templating error: %s", err)
155 }
156
157
158 for k, v := range n.conf.CustomFields {
159 msg[k] = tmpl(v)
160 if err != nil {
161 return nil, fmt.Errorf("templating error: %s", err)
162 }
163 }
164
165 var buf bytes.Buffer
166 if err := json.NewEncoder(&buf).Encode(msg); err != nil {
167 return nil, err
168 }
169 return &buf, nil
170 }
171
View as plain text