cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.4.4

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/checks/alerts_template.go

360lines · modecode

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