1
2
3
4
5
6
7
8
9
10
11
12
13
14 package template
15
16 import (
17 "bytes"
18 tmplhtml "html/template"
19 "io"
20 "net/url"
21 "path"
22 "path/filepath"
23 "regexp"
24 "sort"
25 "strings"
26 tmpltext "text/template"
27 "time"
28
29 "github.com/prometheus/common/model"
30 "golang.org/x/text/cases"
31 "golang.org/x/text/language"
32
33 "github.com/prometheus/alertmanager/asset"
34 "github.com/prometheus/alertmanager/types"
35 )
36
37
38 type Template struct {
39 text *tmpltext.Template
40 html *tmplhtml.Template
41
42 ExternalURL *url.URL
43 }
44
45
46
47 func FromGlobs(paths ...string) (*Template, error) {
48 t := &Template{
49 text: tmpltext.New("").Option("missingkey=zero"),
50 html: tmplhtml.New("").Option("missingkey=zero"),
51 }
52
53 t.text = t.text.Funcs(tmpltext.FuncMap(DefaultFuncs))
54 t.html = t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs))
55
56 defaultTemplates := []string{"default.tmpl", "email.tmpl"}
57
58 for _, file := range defaultTemplates {
59 f, err := asset.Assets.Open(path.Join("/templates", file))
60 if err != nil {
61 return nil, err
62 }
63 defer f.Close()
64 b, err := io.ReadAll(f)
65 if err != nil {
66 return nil, err
67 }
68 if t.text, err = t.text.Parse(string(b)); err != nil {
69 return nil, err
70 }
71 if t.html, err = t.html.Parse(string(b)); err != nil {
72 return nil, err
73 }
74
75 }
76
77 for _, tp := range paths {
78
79
80 p, err := filepath.Glob(tp)
81 if err != nil {
82 return nil, err
83 }
84 if len(p) > 0 {
85 if t.text, err = t.text.ParseGlob(tp); err != nil {
86 return nil, err
87 }
88 if t.html, err = t.html.ParseGlob(tp); err != nil {
89 return nil, err
90 }
91 }
92 }
93 return t, nil
94 }
95
96
97 func (t *Template) ExecuteTextString(text string, data interface{}) (string, error) {
98 if text == "" {
99 return "", nil
100 }
101 tmpl, err := t.text.Clone()
102 if err != nil {
103 return "", err
104 }
105 tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text)
106 if err != nil {
107 return "", err
108 }
109 var buf bytes.Buffer
110 err = tmpl.Execute(&buf, data)
111 return buf.String(), err
112 }
113
114
115 func (t *Template) ExecuteHTMLString(html string, data interface{}) (string, error) {
116 if html == "" {
117 return "", nil
118 }
119 tmpl, err := t.html.Clone()
120 if err != nil {
121 return "", err
122 }
123 tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(html)
124 if err != nil {
125 return "", err
126 }
127 var buf bytes.Buffer
128 err = tmpl.Execute(&buf, data)
129 return buf.String(), err
130 }
131
132 type FuncMap map[string]interface{}
133
134 var DefaultFuncs = FuncMap{
135 "toUpper": strings.ToUpper,
136 "toLower": strings.ToLower,
137 "title": cases.Title(language.AmericanEnglish).String,
138
139
140 "join": func(sep string, s []string) string {
141 return strings.Join(s, sep)
142 },
143 "match": regexp.MatchString,
144 "safeHtml": func(text string) tmplhtml.HTML {
145 return tmplhtml.HTML(text)
146 },
147 "reReplaceAll": func(pattern, repl, text string) string {
148 re := regexp.MustCompile(pattern)
149 return re.ReplaceAllString(text, repl)
150 },
151 "stringSlice": func(s ...string) []string {
152 return s
153 },
154 }
155
156
157 type Pair struct {
158 Name, Value string
159 }
160
161
162 type Pairs []Pair
163
164
165 func (ps Pairs) Names() []string {
166 ns := make([]string, 0, len(ps))
167 for _, p := range ps {
168 ns = append(ns, p.Name)
169 }
170 return ns
171 }
172
173
174 func (ps Pairs) Values() []string {
175 vs := make([]string, 0, len(ps))
176 for _, p := range ps {
177 vs = append(vs, p.Value)
178 }
179 return vs
180 }
181
182
183 type KV map[string]string
184
185
186 func (kv KV) SortedPairs() Pairs {
187 var (
188 pairs = make([]Pair, 0, len(kv))
189 keys = make([]string, 0, len(kv))
190 sortStart = 0
191 )
192 for k := range kv {
193 if k == string(model.AlertNameLabel) {
194 keys = append([]string{k}, keys...)
195 sortStart = 1
196 } else {
197 keys = append(keys, k)
198 }
199 }
200 sort.Strings(keys[sortStart:])
201
202 for _, k := range keys {
203 pairs = append(pairs, Pair{k, kv[k]})
204 }
205 return pairs
206 }
207
208
209 func (kv KV) Remove(keys []string) KV {
210 keySet := make(map[string]struct{}, len(keys))
211 for _, k := range keys {
212 keySet[k] = struct{}{}
213 }
214
215 res := KV{}
216 for k, v := range kv {
217 if _, ok := keySet[k]; !ok {
218 res[k] = v
219 }
220 }
221 return res
222 }
223
224
225 func (kv KV) Names() []string {
226 return kv.SortedPairs().Names()
227 }
228
229
230 func (kv KV) Values() []string {
231 return kv.SortedPairs().Values()
232 }
233
234
235
236
237
238 type Data struct {
239 Receiver string `json:"receiver"`
240 Status string `json:"status"`
241 Alerts Alerts `json:"alerts"`
242
243 GroupLabels KV `json:"groupLabels"`
244 CommonLabels KV `json:"commonLabels"`
245 CommonAnnotations KV `json:"commonAnnotations"`
246
247 ExternalURL string `json:"externalURL"`
248 }
249
250
251 type Alert struct {
252 Status string `json:"status"`
253 Labels KV `json:"labels"`
254 Annotations KV `json:"annotations"`
255 StartsAt time.Time `json:"startsAt"`
256 EndsAt time.Time `json:"endsAt"`
257 GeneratorURL string `json:"generatorURL"`
258 Fingerprint string `json:"fingerprint"`
259 }
260
261
262 type Alerts []Alert
263
264
265 func (as Alerts) Firing() []Alert {
266 res := []Alert{}
267 for _, a := range as {
268 if a.Status == string(model.AlertFiring) {
269 res = append(res, a)
270 }
271 }
272 return res
273 }
274
275
276 func (as Alerts) Resolved() []Alert {
277 res := []Alert{}
278 for _, a := range as {
279 if a.Status == string(model.AlertResolved) {
280 res = append(res, a)
281 }
282 }
283 return res
284 }
285
286
287 func (t *Template) Data(recv string, groupLabels model.LabelSet, alerts ...*types.Alert) *Data {
288 data := &Data{
289 Receiver: regexp.QuoteMeta(recv),
290 Status: string(types.Alerts(alerts...).Status()),
291 Alerts: make(Alerts, 0, len(alerts)),
292 GroupLabels: KV{},
293 CommonLabels: KV{},
294 CommonAnnotations: KV{},
295 ExternalURL: t.ExternalURL.String(),
296 }
297
298
299
300 for _, a := range types.Alerts(alerts...) {
301 alert := Alert{
302 Status: string(a.Status()),
303 Labels: make(KV, len(a.Labels)),
304 Annotations: make(KV, len(a.Annotations)),
305 StartsAt: a.StartsAt,
306 EndsAt: a.EndsAt,
307 GeneratorURL: a.GeneratorURL,
308 Fingerprint: a.Fingerprint().String(),
309 }
310 for k, v := range a.Labels {
311 alert.Labels[string(k)] = string(v)
312 }
313 for k, v := range a.Annotations {
314 alert.Annotations[string(k)] = string(v)
315 }
316 data.Alerts = append(data.Alerts, alert)
317 }
318
319 for k, v := range groupLabels {
320 data.GroupLabels[string(k)] = string(v)
321 }
322
323 if len(alerts) >= 1 {
324 var (
325 commonLabels = alerts[0].Labels.Clone()
326 commonAnnotations = alerts[0].Annotations.Clone()
327 )
328 for _, a := range alerts[1:] {
329 if len(commonLabels) == 0 && len(commonAnnotations) == 0 {
330 break
331 }
332 for ln, lv := range commonLabels {
333 if a.Labels[ln] != lv {
334 delete(commonLabels, ln)
335 }
336 }
337 for an, av := range commonAnnotations {
338 if a.Annotations[an] != av {
339 delete(commonAnnotations, an)
340 }
341 }
342 }
343 for k, v := range commonLabels {
344 data.CommonLabels[string(k)] = string(v)
345 }
346 for k, v := range commonAnnotations {
347 data.CommonAnnotations[string(k)] = string(v)
348 }
349 }
350
351 return data
352 }
353
View as plain text