cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.7.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/checks/alerts_template.go

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