cloudflare/pint

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.4.2

Branches

Tags

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

Clone

HTTPS

Download ZIP

internal/checks/alerts_template.go

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