1
2
3
4
5
6
7
8
9
10
11
12
13
14 package slack
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
30 "github.com/prometheus/alertmanager/config"
31 "github.com/prometheus/alertmanager/notify"
32 "github.com/prometheus/alertmanager/template"
33 "github.com/prometheus/alertmanager/types"
34 )
35
36
37 const maxTitleLenRunes = 1024
38
39
40 type Notifier struct {
41 conf *config.SlackConfig
42 tmpl *template.Template
43 logger log.Logger
44 client *http.Client
45 retrier *notify.Retrier
46 }
47
48
49 func New(c *config.SlackConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
50 client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "slack", httpOpts...)
51 if err != nil {
52 return nil, err
53 }
54
55 return &Notifier{
56 conf: c,
57 tmpl: t,
58 logger: l,
59 client: client,
60 retrier: ¬ify.Retrier{},
61 }, nil
62 }
63
64
65 type request struct {
66 Channel string `json:"channel,omitempty"`
67 Username string `json:"username,omitempty"`
68 IconEmoji string `json:"icon_emoji,omitempty"`
69 IconURL string `json:"icon_url,omitempty"`
70 LinkNames bool `json:"link_names,omitempty"`
71 Attachments []attachment `json:"attachments"`
72 }
73
74
75 type attachment struct {
76 Title string `json:"title,omitempty"`
77 TitleLink string `json:"title_link,omitempty"`
78 Pretext string `json:"pretext,omitempty"`
79 Text string `json:"text"`
80 Fallback string `json:"fallback"`
81 CallbackID string `json:"callback_id"`
82 Fields []config.SlackField `json:"fields,omitempty"`
83 Actions []config.SlackAction `json:"actions,omitempty"`
84 ImageURL string `json:"image_url,omitempty"`
85 ThumbURL string `json:"thumb_url,omitempty"`
86 Footer string `json:"footer"`
87 Color string `json:"color,omitempty"`
88 MrkdwnIn []string `json:"mrkdwn_in,omitempty"`
89 }
90
91
92 func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
93 var err error
94 var (
95 data = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
96 tmplText = notify.TmplText(n.tmpl, data, &err)
97 )
98 var markdownIn []string
99
100 if len(n.conf.MrkdwnIn) == 0 {
101 markdownIn = []string{"fallback", "pretext", "text"}
102 } else {
103 markdownIn = n.conf.MrkdwnIn
104 }
105
106 title, truncated := notify.TruncateInRunes(tmplText(n.conf.Title), maxTitleLenRunes)
107 if truncated {
108 key, err := notify.ExtractGroupKey(ctx)
109 if err != nil {
110 return false, err
111 }
112 level.Warn(n.logger).Log("msg", "Truncated title", "key", key, "max_runes", maxTitleLenRunes)
113 }
114 att := &attachment{
115 Title: title,
116 TitleLink: tmplText(n.conf.TitleLink),
117 Pretext: tmplText(n.conf.Pretext),
118 Text: tmplText(n.conf.Text),
119 Fallback: tmplText(n.conf.Fallback),
120 CallbackID: tmplText(n.conf.CallbackID),
121 ImageURL: tmplText(n.conf.ImageURL),
122 ThumbURL: tmplText(n.conf.ThumbURL),
123 Footer: tmplText(n.conf.Footer),
124 Color: tmplText(n.conf.Color),
125 MrkdwnIn: markdownIn,
126 }
127
128 numFields := len(n.conf.Fields)
129 if numFields > 0 {
130 fields := make([]config.SlackField, numFields)
131 for index, field := range n.conf.Fields {
132
133 var short bool
134 if field.Short != nil {
135 short = *field.Short
136 } else {
137 short = n.conf.ShortFields
138 }
139
140
141 fields[index] = config.SlackField{
142 Title: tmplText(field.Title),
143 Value: tmplText(field.Value),
144 Short: &short,
145 }
146 }
147 att.Fields = fields
148 }
149
150 numActions := len(n.conf.Actions)
151 if numActions > 0 {
152 actions := make([]config.SlackAction, numActions)
153 for index, action := range n.conf.Actions {
154 slackAction := config.SlackAction{
155 Type: tmplText(action.Type),
156 Text: tmplText(action.Text),
157 URL: tmplText(action.URL),
158 Style: tmplText(action.Style),
159 Name: tmplText(action.Name),
160 Value: tmplText(action.Value),
161 }
162
163 if action.ConfirmField != nil {
164 slackAction.ConfirmField = &config.SlackConfirmationField{
165 Title: tmplText(action.ConfirmField.Title),
166 Text: tmplText(action.ConfirmField.Text),
167 OkText: tmplText(action.ConfirmField.OkText),
168 DismissText: tmplText(action.ConfirmField.DismissText),
169 }
170 }
171
172 actions[index] = slackAction
173 }
174 att.Actions = actions
175 }
176
177 req := &request{
178 Channel: tmplText(n.conf.Channel),
179 Username: tmplText(n.conf.Username),
180 IconEmoji: tmplText(n.conf.IconEmoji),
181 IconURL: tmplText(n.conf.IconURL),
182 LinkNames: n.conf.LinkNames,
183 Attachments: []attachment{*att},
184 }
185 if err != nil {
186 return false, err
187 }
188
189 var buf bytes.Buffer
190 if err := json.NewEncoder(&buf).Encode(req); err != nil {
191 return false, err
192 }
193
194 var u string
195 if n.conf.APIURL != nil {
196 u = n.conf.APIURL.String()
197 } else {
198 content, err := os.ReadFile(n.conf.APIURLFile)
199 if err != nil {
200 return false, err
201 }
202 u = strings.TrimSpace(string(content))
203 }
204
205 resp, err := notify.PostJSON(ctx, n.client, u, &buf)
206 if err != nil {
207 return true, notify.RedactURL(err)
208 }
209 defer notify.Drain(resp)
210
211
212
213
214 retry, err := n.retrier.Check(resp.StatusCode, resp.Body)
215 err = errors.Wrap(err, fmt.Sprintf("channel %q", req.Channel))
216 return retry, err
217 }
218
View as plain text