Skip to content

Commit

Permalink
Check labels used on annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
prymitive committed Nov 25, 2021
1 parent 4f54807 commit c5c5cfd
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 33 deletions.
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Changelog

## v0.2.0

### Added

- `--offline` flag for `pint lint` command. When passed only checks that don't send
any live queries to Prometheus server will be run.
- `alerts/template` check will now warn if template if referencing a label that is being
stripped by aggregation.
Example:

```
- alert: Foo
expr: count(up) without(instance) == 0
annotations:
summary: 'foo is down on {{ $labels.instance }}'
```

Would generate a warning since `instance` label is being stripped by `without(instance)`.

## v0.1.5

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion cmd/pint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func actionLint(c *cli.Context) (err error) {
return fmt.Errorf("failed to load config file %q: %w", c.Path(configFlag), err)
}

cfg.SetDisabledChecks(c.StringSlice(disabledFlag))
cfg.SetDisabledChecks(c.Bool(offlineFlag), c.StringSlice(disabledFlag))

d := discovery.NewGlobFileFinder()
toScan, err := d.Find(paths...)
Expand Down
7 changes: 7 additions & 0 deletions cmd/pint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
configFlag = "config"
logLevelFlag = "log-level"
disabledFlag = "disabled"
offlineFlag = "offline"
)

var (
Expand Down Expand Up @@ -56,6 +57,12 @@ func newApp() *cli.App {
Value: cli.NewStringSlice(),
Usage: "List of checks to disable (example: promql/cost)",
},
&cli.BoolFlag{
Name: offlineFlag,
Aliases: []string{"o"},
Value: false,
Usage: "Disable all check that send live queries to Prometheus servers",
},
},
},
{
Expand Down
88 changes: 77 additions & 11 deletions internal/checks/alerts_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"time"

"github.com/cloudflare/pint/internal/parser"
"github.com/cloudflare/pint/internal/parser/utils"
"github.com/prometheus/common/model"
"github.com/prometheus/prometheus/pkg/timestamp"
promTemplate "github.com/prometheus/prometheus/template"
Expand Down Expand Up @@ -74,6 +75,8 @@ func (c TemplateCheck) Check(rule parser.Rule) (problems []Problem) {
return nil
}

aggr := utils.HasOuterAggregation(rule.AlertingRule.Expr.Query)

data := promTemplate.AlertTemplateData(map[string]string{}, map[string]string{}, "", 0)

if rule.AlertingRule.Labels != nil {
Expand Down Expand Up @@ -107,6 +110,18 @@ func (c TemplateCheck) Check(rule parser.Rule) (problems []Problem) {
Severity: c.severity,
})
}

if aggr != nil {
for _, msg := range checkMetricLabels(label.Key.Value, label.Value.Value, aggr.Grouping, aggr.Without) {
problems = append(problems, Problem{
Fragment: fmt.Sprintf("%s: %s", label.Key.Value, label.Value.Value),
Lines: label.Lines(),
Reporter: TemplateCheckName,
Text: msg,
Severity: c.severity,
})
}
}
}
}

Expand All @@ -121,6 +136,18 @@ func (c TemplateCheck) Check(rule parser.Rule) (problems []Problem) {
Severity: c.severity,
})
}

if aggr != nil {
for _, msg := range checkMetricLabels(annotation.Key.Value, annotation.Value.Value, aggr.Grouping, aggr.Without) {
problems = append(problems, Problem{
Fragment: fmt.Sprintf("%s: %s", annotation.Key.Value, annotation.Value.Value),
Lines: annotation.Lines(),
Reporter: TemplateCheckName,
Text: msg,
Severity: c.severity,
})
}
}
}
}

Expand Down Expand Up @@ -160,16 +187,16 @@ func checkForValueInLabels(name, text string) (msgs []string) {
}

var aliases = aliasMap{aliases: map[string]map[string]struct{}{}}
var vars []string
var vars = [][]string{}
for _, node := range t.Root.Nodes {
getAliases(node, &aliases)
vars = append(vars, getVariables(node)...)
}
var valAliases = aliases.varAliases(".Value")
for _, v := range vars {
for _, a := range valAliases {
if v == a {
msg := fmt.Sprintf("using %s in labels will generate a new alert on every value change, move it to annotations", v)
if v[0] == a {
msg := fmt.Sprintf("using %s in labels will generate a new alert on every value change, move it to annotations", v[0])
msgs = append(msgs, msg)
}
}
Expand Down Expand Up @@ -200,10 +227,10 @@ func getAliases(node parse.Node, aliases *aliasMap) {
for _, k := range getVariables(arg) {
for _, d := range n.Pipe.Decl {
for _, v := range getVariables(d) {
if _, ok := aliases.aliases[k]; !ok {
aliases.aliases[k] = map[string]struct{}{}
if _, ok := aliases.aliases[k[0]]; !ok {
aliases.aliases[k[0]] = map[string]struct{}{}
}
aliases.aliases[k][v] = struct{}{}
aliases.aliases[k[0]][v[0]] = struct{}{}
}
}
}
Expand All @@ -213,7 +240,7 @@ func getAliases(node parse.Node, aliases *aliasMap) {
}
}

func getVariables(node parse.Node) (vars []string) {
func getVariables(node parse.Node) (vars [][]string) {
switch n := node.(type) {
case *parse.ActionNode:
if len(n.Pipe.Decl) == 0 && len(n.Pipe.Cmds) > 0 {
Expand All @@ -224,12 +251,51 @@ func getVariables(node parse.Node) (vars []string) {
vars = append(vars, getVariables(arg)...)
}
case *parse.FieldNode:
for _, i := range n.Ident {
vars = append(vars, "."+i)
}
n.Ident[0] = "." + n.Ident[0]
vars = append(vars, n.Ident)
case *parse.VariableNode:
vars = append(vars, n.Ident...)
vars = append(vars, n.Ident)
}

return vars
}

func checkMetricLabels(name, text string, metricLabels []string, excludeLabels bool) (msgs []string) {
t, err := textTemplate.
New(name).
Funcs(templateFuncMap).
Option("missingkey=zero").
Parse(strings.Join(append(templateDefs, text), ""))
if err != nil {
// no need to double report errors
return nil
}

var aliases = aliasMap{aliases: map[string]map[string]struct{}{}}
var vars = [][]string{}
for _, node := range t.Root.Nodes {
getAliases(node, &aliases)
vars = append(vars, getVariables(node)...)
}

var labelsAliases = aliases.varAliases(".Labels")
for _, v := range vars {
for _, a := range labelsAliases {
if len(v) > 1 && v[0] == a {
var found bool
for _, l := range metricLabels {
if len(v) > 1 && v[1] == l {
found = true
}
}
if found == excludeLabels {
msg := fmt.Sprintf("template is using %q label but the query doesn't preseve it", v[1])
msgs = append(msgs, msg)

}
}
}
}

return
}
72 changes: 56 additions & 16 deletions internal/checks/alerts_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,22 +226,62 @@ func TestTemplateCheck(t *testing.T) {
},
},
},
/*
{
description: "annotation label missing from metrics",
content: "- alert: Foo Is Down\n expr: up{job=\"foo\"} == 0\n annotations:\n summary: '{{ $labels.job }}'\n",
checker: checks.NewTemplateCheck(checks.Bug),
problems: []checks.Problem{
{
Fragment: `summary: '{{ $labels.job }}`,
Lines: []int{2, 3},
Reporter: "alerts/count",
Text: "query using prom would trigger 1 alert(s) in the last 1d",
Severity: checks.Information,
},
},
},
*/
{
description: "annotation label missing from metrics (by)",
content: "- alert: Foo Is Down\n expr: sum(foo) > 0\n annotations:\n summary: '{{ $labels.job }}'\n",
checker: checks.NewTemplateCheck(checks.Bug),
problems: []checks.Problem{
{
Fragment: `summary: {{ $labels.job }}`,
Lines: []int{4},
Reporter: "alerts/template",
Text: `template is using "job" label but the query doesn't preseve it`,
Severity: checks.Bug,
},
},
},
{
description: "annotation label missing from metrics (by)",
content: "- alert: Foo Is Down\n expr: sum(foo) > 0\n annotations:\n summary: '{{ .Labels.job }}'\n",
checker: checks.NewTemplateCheck(checks.Bug),
problems: []checks.Problem{
{
Fragment: `summary: {{ .Labels.job }}`,
Lines: []int{4},
Reporter: "alerts/template",
Text: `template is using "job" label but the query doesn't preseve it`,
Severity: checks.Bug,
},
},
},
{
description: "annotation label missing from metrics (without)",
content: "- alert: Foo Is Down\n expr: sum(foo) without(job) > 0\n annotations:\n summary: '{{ $labels.job }}'\n",
checker: checks.NewTemplateCheck(checks.Bug),
problems: []checks.Problem{
{
Fragment: `summary: {{ $labels.job }}`,
Lines: []int{4},
Reporter: "alerts/template",
Text: `template is using "job" label but the query doesn't preseve it`,
Severity: checks.Bug,
},
},
},
{
description: "annotation label missing from metrics (without)",
content: "- alert: Foo Is Down\n expr: sum(foo) without(job) > 0\n annotations:\n summary: '{{ .Labels.job }}'\n",
checker: checks.NewTemplateCheck(checks.Bug),
problems: []checks.Problem{
{
Fragment: `summary: {{ .Labels.job }}`,
Lines: []int{4},
Reporter: "alerts/template",
Text: `template is using "job" label but the query doesn't preseve it`,
Severity: checks.Bug,
},
},
},
}
runTests(t, testCases)
}
7 changes: 7 additions & 0 deletions internal/checks/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ var (
TemplateCheckName,
VectorMatchingCheckName,
}
OnlineChecks = []string{
AlertsCheckName,
CostCheckName,
RateCheckName,
SeriesCheckName,
VectorMatchingCheckName,
}
)

// Severity of the problem reported
Expand Down
2 changes: 1 addition & 1 deletion internal/checks/promql_comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func (c ComparisonCheck) Check(rule parser.Rule) (problems []Problem) {
problems = append(problems, Problem{
Fragment: rule.AlertingRule.Expr.Value.Value,
Lines: rule.AlertingRule.Expr.Lines(),
Reporter: AlertsCheckName,
Reporter: ComparisonCheckName,
Text: "alert query doesn't have any condition, it will always fire if the metric exists",
Severity: c.severity,
})
Expand Down
7 changes: 6 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ type Config struct {
Rules []Rule `hcl:"rule,block"`
}

func (cfg *Config) SetDisabledChecks(l []string) {
func (cfg *Config) SetDisabledChecks(offline bool, l []string) {
disabled := map[string]struct{}{}
if offline {
for _, name := range checks.OnlineChecks {
disabled[name] = struct{}{}
}
}
for _, s := range l {
re := strictRegex(s)
for _, name := range checks.CheckNames {
Expand Down
4 changes: 2 additions & 2 deletions internal/parser/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
promparser "github.com/prometheus/prometheus/promql/parser"
)

func decodeExpr(expr string) (*PromQLNode, error) {
func DecodeExpr(expr string) (*PromQLNode, error) {
node, err := promparser.ParseExpr(expr)
if err != nil {
pqe := PromQLError{Err: err}
Expand All @@ -30,7 +30,7 @@ func decodeExpr(expr string) (*PromQLNode, error) {
}

for _, child := range promparser.Children(node) {
c, err := decodeExpr(child.String())
c, err := DecodeExpr(child.String())
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/parser/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ func newPromQLExpr(key, val *yaml.Node) *PromQLExpr {
Value: newYamlNodeWithParent(key, val),
}

qlNode, err := decodeExpr(val.Value)
qlNode, err := DecodeExpr(val.Value)
if err != nil {
expr.SyntaxError = err
return &expr
Expand Down
45 changes: 45 additions & 0 deletions internal/parser/utils/aggregation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package utils

import (
"github.com/cloudflare/pint/internal/parser"

promParser "github.com/prometheus/prometheus/promql/parser"
"github.com/rs/zerolog/log"
)

func HasOuterAggregation(node *parser.PromQLNode) *promParser.AggregateExpr {
if n, ok := node.Node.(*promParser.AggregateExpr); ok {
switch n.Op {
case promParser.SUM:
case promParser.MIN:
case promParser.MAX:
case promParser.AVG:
case promParser.GROUP:
case promParser.STDDEV:
case promParser.STDVAR:
case promParser.COUNT:
case promParser.COUNT_VALUES:
case promParser.BOTTOMK:
goto NEXT
case promParser.TOPK:
goto NEXT
case promParser.QUANTILE:
default:
log.Warn().Str("op", n.Op.String()).Msg("Unsupported aggregation operation")
}
return n
}

NEXT:
if n, ok := node.Node.(*promParser.BinaryExpr); ok && n.VectorMatching != nil {
return HasOuterAggregation(node.Children[0])
}

for _, child := range node.Children {
if a := HasOuterAggregation(child); a != nil {
return a
}
}

return nil
}
Loading

0 comments on commit c5c5cfd

Please sign in to comment.