cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.22.2

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/checks/alerts_template.go

393lines · modecode

1package checks
2
3import (
4 "context"
5 "errors"
6 "fmt"
7 "sort"
8 "strings"
9 textTemplate "text/template"
10 "text/template/parse"
11 "time"
12
13 "github.com/prometheus/common/model"
14 "github.com/prometheus/prometheus/model/labels"
15 "github.com/prometheus/prometheus/model/timestamp"
16 promParser "github.com/prometheus/prometheus/promql/parser"
17 promTemplate "github.com/prometheus/prometheus/template"
18
19 "github.com/cloudflare/pint/internal/discovery"
20 "github.com/cloudflare/pint/internal/parser"
21 "github.com/cloudflare/pint/internal/parser/utils"
22)
23
24const (
25 TemplateCheckName = "alerts/template"
26
27 msgAggregation = "template is using %q label but the query removes it"
28 msgAbsent = "template is using %q label but absent() is not passing it"
29)
30
31var (
32 templateDefs = []string{
33 "{{$labels := .Labels}}",
34 "{{$externalLabels := .ExternalLabels}}",
35 "{{$externalURL := .ExternalURL}}",
36 "{{$value := .Value}}",
37 }
38
39 templateFuncMap = textTemplate.FuncMap{
40 "query": dummyFuncMap,
41 "first": dummyFuncMap,
42 "label": dummyFuncMap,
43 "value": dummyFuncMap,
44 "strvalue": dummyFuncMap,
45 "args": dummyFuncMap,
46 "reReplaceAll": dummyFuncMap,
47 "safeHtml": dummyFuncMap,
48 "match": dummyFuncMap,
49 "title": dummyFuncMap,
50 "toUpper": dummyFuncMap,
51 "toLower": dummyFuncMap,
52 "graphLink": dummyFuncMap,
53 "tableLink": dummyFuncMap,
54 "sortByLabel": dummyFuncMap,
55 "stripPort": dummyFuncMap,
56 "stripDomain": dummyFuncMap,
57 "humanize": dummyFuncMap,
58 "humanize1024": dummyFuncMap,
59 "humanizeDuration": dummyFuncMap,
60 "humanizePercentage": dummyFuncMap,
61 "humanizeTimestamp": dummyFuncMap,
62 "pathPrefix": dummyFuncMap,
63 "externalURL": dummyFuncMap,
64 "parseDuration": dummyFuncMap,
65 }
66)
67
68func dummyFuncMap(q string) string {
69 return q
70}
71
72func NewTemplateCheck() TemplateCheck {
73 return TemplateCheck{}
74}
75
76type TemplateCheck struct{}
77
78func (c TemplateCheck) String() string {
79 return TemplateCheckName
80}
81
82func (c TemplateCheck) Reporter() string {
83 return TemplateCheckName
84}
85
86func (c TemplateCheck) Check(ctx context.Context, rule parser.Rule, entries []discovery.Entry) (problems []Problem) {
87 if rule.AlertingRule == nil {
88 return nil
89 }
90
91 if rule.AlertingRule.Expr.SyntaxError != nil {
92 return nil
93 }
94
95 aggrs := utils.HasOuterAggregation(rule.AlertingRule.Expr.Query)
96 absentCalls := utils.HasOuterAbsent(rule.AlertingRule.Expr.Query)
97
98 data := promTemplate.AlertTemplateData(map[string]string{}, map[string]string{}, "", 0)
99
100 if rule.AlertingRule.Labels != nil {
101 for _, label := range rule.AlertingRule.Labels.Items {
102 if err := checkTemplateSyntax(label.Key.Value, label.Value.Value, data); err != nil {
103 problems = append(problems, Problem{
104 Fragment: fmt.Sprintf("%s: %s", label.Key.Value, label.Value.Value),
105 Lines: label.Lines(),
106 Reporter: c.Reporter(),
107 Text: fmt.Sprintf("template parse error: %s", err),
108 Severity: Fatal,
109 })
110 }
111 // check key
112 for _, msg := range checkForValueInLabels(label.Key.Value, label.Key.Value) {
113 problems = append(problems, Problem{
114 Fragment: fmt.Sprintf("%s: %s", label.Key.Value, label.Value.Value),
115 Lines: label.Lines(),
116 Reporter: c.Reporter(),
117 Text: msg,
118 Severity: Bug,
119 })
120 }
121 // check value
122 for _, msg := range checkForValueInLabels(label.Key.Value, label.Value.Value) {
123 problems = append(problems, Problem{
124 Fragment: fmt.Sprintf("%s: %s", label.Key.Value, label.Value.Value),
125 Lines: label.Lines(),
126 Reporter: c.Reporter(),
127 Text: msg,
128 Severity: Bug,
129 })
130 }
131
132 for _, aggr := range aggrs {
133 for _, msg := range checkMetricLabels(msgAggregation, label.Key.Value, label.Value.Value, aggr.Grouping, aggr.Without) {
134 problems = append(problems, Problem{
135 Fragment: fmt.Sprintf("%s: %s", label.Key.Value, label.Value.Value),
136 Lines: mergeLines(label.Lines(), rule.AlertingRule.Expr.Lines()),
137 Reporter: c.Reporter(),
138 Text: msg,
139 Severity: Bug,
140 })
141 }
142 }
143
144 for _, call := range absentCalls {
145 if len(utils.HasOuterAggregation(call.Fragment)) > 0 {
146 continue
147 }
148 for _, msg := range checkMetricLabels(msgAbsent, label.Key.Value, label.Value.Value, absentLabels(call), false) {
149 problems = append(problems, Problem{
150 Fragment: fmt.Sprintf("%s: %s", label.Key.Value, label.Value.Value),
151 Lines: mergeLines(label.Lines(), rule.AlertingRule.Expr.Lines()),
152 Reporter: c.Reporter(),
153 Text: msg,
154 Severity: Bug,
155 })
156 }
157 }
158 }
159 }
160
161 if rule.AlertingRule.Annotations != nil {
162 for _, annotation := range rule.AlertingRule.Annotations.Items {
163 if err := checkTemplateSyntax(annotation.Key.Value, annotation.Value.Value, data); err != nil {
164 problems = append(problems, Problem{
165 Fragment: fmt.Sprintf("%s: %s", annotation.Key.Value, annotation.Value.Value),
166 Lines: annotation.Lines(),
167 Reporter: c.Reporter(),
168 Text: fmt.Sprintf("template parse error: %s", err),
169 Severity: Fatal,
170 })
171 }
172
173 for _, aggr := range aggrs {
174 for _, msg := range checkMetricLabels(msgAggregation, annotation.Key.Value, annotation.Value.Value, aggr.Grouping, aggr.Without) {
175 problems = append(problems, Problem{
176 Fragment: fmt.Sprintf("%s: %s", annotation.Key.Value, annotation.Value.Value),
177 Lines: mergeLines(annotation.Lines(), rule.AlertingRule.Expr.Lines()),
178 Reporter: c.Reporter(),
179 Text: msg,
180 Severity: Bug,
181 })
182 }
183 }
184
185 for _, call := range absentCalls {
186 if len(utils.HasOuterAggregation(call.Fragment)) > 0 {
187 continue
188 }
189 if call.BinExpr != nil &&
190 call.BinExpr.VectorMatching != nil &&
191 (call.BinExpr.VectorMatching.Card == promParser.CardManyToOne ||
192 call.BinExpr.VectorMatching.Card == promParser.CardOneToMany) &&
193 len(call.BinExpr.VectorMatching.Include) == 0 {
194 continue
195 }
196 for _, msg := range checkMetricLabels(msgAbsent, annotation.Key.Value, annotation.Value.Value, absentLabels(call), false) {
197 problems = append(problems, Problem{
198 Fragment: fmt.Sprintf("%s: %s", annotation.Key.Value, annotation.Value.Value),
199 Lines: mergeLines(annotation.Lines(), rule.AlertingRule.Expr.Lines()),
200 Reporter: c.Reporter(),
201 Text: msg,
202 Severity: Bug,
203 })
204 }
205 }
206
207 }
208 }
209
210 return problems
211}
212
213func checkTemplateSyntax(name, text string, data interface{}) error {
214 tmpl := promTemplate.NewTemplateExpander(
215 context.TODO(),
216 strings.Join(append(templateDefs, text), ""),
217 name,
218 data,
219 model.Time(timestamp.FromTime(time.Now())),
220 nil,
221 nil,
222 nil,
223 )
224 if err := tmpl.ParseTest(); err != nil {
225 e := strings.TrimPrefix(err.Error(), fmt.Sprintf("template: %s:", name))
226 if v := strings.SplitN(e, ":", 2); len(v) > 1 {
227 e = strings.TrimPrefix(v[1], " ")
228 }
229 return errors.New(e)
230 }
231 return nil
232}
233
234func checkForValueInLabels(name, text string) (msgs []string) {
235 t, err := textTemplate.
236 New(name).
237 Funcs(templateFuncMap).
238 Option("missingkey=zero").
239 Parse(strings.Join(append(templateDefs, text), ""))
240 if err != nil {
241 // no need to double report errors
242 return nil
243 }
244
245 aliases := aliasMap{aliases: map[string]map[string]struct{}{}}
246 vars := [][]string{}
247 for _, node := range t.Root.Nodes {
248 getAliases(node, &aliases)
249 vars = append(vars, getVariables(node)...)
250 }
251 valAliases := aliases.varAliases(".Value")
252 for _, v := range vars {
253 for _, a := range valAliases {
254 if v[0] == a {
255 msg := fmt.Sprintf("using %s in labels will generate a new alert on every value change, move it to annotations", v[0])
256 msgs = append(msgs, msg)
257 }
258 }
259 }
260 return msgs
261}
262
263type aliasMap struct {
264 aliases map[string]map[string]struct{}
265}
266
267func (am aliasMap) varAliases(k string) (vals []string) {
268 vals = append(vals, k)
269 if as, ok := am.aliases[k]; ok {
270 for val := range as {
271 vals = append(vals, am.varAliases(val)...)
272 }
273 }
274 return vals
275}
276
277func getAliases(node parse.Node, aliases *aliasMap) {
278 switch n := node.(type) {
279 case *parse.ActionNode:
280 if len(n.Pipe.Decl) == 1 && !n.Pipe.IsAssign && len(n.Pipe.Cmds) == 1 {
281 for _, cmd := range n.Pipe.Cmds {
282 for _, arg := range cmd.Args {
283 for _, k := range getVariables(arg) {
284 for _, d := range n.Pipe.Decl {
285 for _, v := range getVariables(d) {
286 if _, ok := aliases.aliases[k[0]]; !ok {
287 aliases.aliases[k[0]] = map[string]struct{}{}
288 }
289 aliases.aliases[k[0]][v[0]] = struct{}{}
290 }
291 }
292 }
293 }
294 }
295 }
296 }
297}
298
299func getVariables(node parse.Node) (vars [][]string) {
300 switch n := node.(type) {
301 case *parse.ActionNode:
302 if len(n.Pipe.Decl) == 0 && len(n.Pipe.Cmds) > 0 {
303 vars = append(vars, getVariables(n.Pipe.Cmds[0])...)
304 }
305 case *parse.CommandNode:
306 for _, arg := range n.Args {
307 vars = append(vars, getVariables(arg)...)
308 }
309 case *parse.FieldNode:
310 n.Ident[0] = "." + n.Ident[0]
311 vars = append(vars, n.Ident)
312 case *parse.VariableNode:
313 vars = append(vars, n.Ident)
314 }
315
316 return vars
317}
318
319func checkMetricLabels(msg, name, text string, metricLabels []string, excludeLabels bool) (msgs []string) {
320 t, err := textTemplate.
321 New(name).
322 Funcs(templateFuncMap).
323 Option("missingkey=zero").
324 Parse(strings.Join(append(templateDefs, text), ""))
325 if err != nil {
326 // no need to double report errors
327 return nil
328 }
329
330 aliases := aliasMap{aliases: map[string]map[string]struct{}{}}
331 vars := [][]string{}
332 for _, node := range t.Root.Nodes {
333 getAliases(node, &aliases)
334 vars = append(vars, getVariables(node)...)
335 }
336
337 done := map[string]struct{}{}
338 labelsAliases := aliases.varAliases(".Labels")
339 for _, v := range vars {
340 for _, a := range labelsAliases {
341 if len(v) > 1 && v[0] == a {
342 var found bool
343 for _, l := range metricLabels {
344 if len(v) > 1 && v[1] == l {
345 found = true
346 }
347 }
348 if found == excludeLabels {
349 if _, ok := done[v[1]]; !ok {
350 msg := fmt.Sprintf(msg, v[1])
351 msgs = append(msgs, msg)
352 done[v[1]] = struct{}{}
353 }
354 }
355 }
356 }
357 }
358
359 return
360}
361
362func absentLabels(f utils.PromQLFragment) []string {
363 labelMap := map[string]struct{}{}
364
365 for _, child := range f.Fragment.Children {
366 for _, v := range utils.HasVectorSelector(child) {
367 for _, lm := range v.LabelMatchers {
368 if lm.Type == labels.MatchEqual {
369 labelMap[lm.Name] = struct{}{}
370 }
371 }
372 }
373 }
374
375 if f.BinExpr != nil && f.BinExpr.VectorMatching != nil {
376 for _, name := range f.BinExpr.VectorMatching.Include {
377 labelMap[name] = struct{}{}
378 }
379 }
380
381 names := make([]string, 0, len(labelMap))
382 for name := range labelMap {
383 names = append(names, name)
384 }
385
386 return names
387}
388
389func mergeLines(a, b []int) (l []int) {
390 l = append(a, b...)
391 sort.Ints(l)
392 return
393}
394