Skip to content

Commit

Permalink
More complete label source coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
prymitive committed Oct 28, 2024
1 parent 4f27a0c commit 0fff8c1
Show file tree
Hide file tree
Showing 4 changed files with 495 additions and 70 deletions.
59 changes: 7 additions & 52 deletions internal/checks/alerts_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ When using ` + "`on()`" + `make sure that all labels you're trying to use in thi
msgAggregation = "Template is using `%s` label but the query removes it."
msgAbsent = "Template is using `%s` label but `absent()` is not passing it."
msgOn = "Template is using `%s` label but the query uses `on(...)` without it being set there, this label will be missing from the query result."
msgVector = "Template is using `%s` label but the query doesn't produce any labels."
)

var (
Expand Down Expand Up @@ -119,8 +120,6 @@ func (c TemplateCheck) Check(ctx context.Context, _ discovery.Path, rule parser.
}

src := utils.LabelsSource(rule.AlertingRule.Expr.Query)
vectors := utils.HasVectorSelector(rule.AlertingRule.Expr.Query)

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

if rule.AlertingRule.Labels != nil {
Expand Down Expand Up @@ -161,22 +160,6 @@ func (c TemplateCheck) Check(ctx context.Context, _ discovery.Path, rule parser.
Severity: problem.severity,
})
}

labelNames := getTemplateLabels(label.Key.Value, label.Value.Value)
if len(labelNames) > 0 && len(vectors) == 0 {
for _, name := range labelNames {
problems = append(problems, Problem{
Lines: parser.LineRange{
First: label.Key.Lines.First,
Last: label.Value.Lines.Last,
},
Reporter: c.Reporter(),
Text: fmt.Sprintf("Template is using `%s` label but the query doesn't produce any labels.", name),
Details: TemplateCheckLabelsDetails,
Severity: Bug,
})
}
}
}
}

Expand Down Expand Up @@ -208,22 +191,6 @@ func (c TemplateCheck) Check(ctx context.Context, _ discovery.Path, rule parser.
})
}

labelNames := getTemplateLabels(annotation.Key.Value, annotation.Value.Value)
if len(labelNames) > 0 && len(vectors) == 0 {
for _, name := range labelNames {
problems = append(problems, Problem{
Lines: parser.LineRange{
First: annotation.Key.Lines.First,
Last: annotation.Value.Lines.Last,
},
Reporter: c.Reporter(),
Text: fmt.Sprintf("Template is using `%s` label but the query doesn't produce any labels.", name),
Details: TemplateCheckLabelsDetails,
Severity: Bug,
})
}
}

if hasValue(annotation.Key.Value, annotation.Value.Value) && !hasHumanize(annotation.Key.Value, annotation.Value.Value) {
for _, problem := range c.checkHumanizeIsNeeded(rule.AlertingRule.Expr.Query) {
problems = append(problems, Problem{
Expand Down Expand Up @@ -473,24 +440,6 @@ func findTemplateVariables(name, text string) (vars [][]string, aliases aliasMap
return vars, aliases, true
}

func getTemplateLabels(name, text string) (names []string) {
vars, aliases, ok := findTemplateVariables(name, text)
if !ok {
return nil
}

labelsAliases := aliases.varAliases(".Labels")
for _, v := range vars {
for _, a := range labelsAliases {
if len(v) > 1 && v[0] == a {
names = append(names, v[1])
}
}
}

return names
}

func checkQueryLabels(labelName, labelValue string, src utils.Source) (problems []exprProblem) {
vars, aliases, ok := findTemplateVariables(labelName, labelValue)
if !ok {
Expand Down Expand Up @@ -534,6 +483,12 @@ func textForProblem(label string, src utils.Source, severity Severity) exprProbl
details: TemplateCheckAbsentDetails,
severity: severity,
}
case src.Returns == promParser.ValueTypeScalar, src.Returns == promParser.ValueTypeString, src.Operation == "vector":
return exprProblem{
text: fmt.Sprintf(msgVector, label),
details: TemplateCheckLabelsDetails,
severity: severity,
}
case slices.Contains([]string{
promParser.CardOneToOne.String(),
promParser.CardOneToMany.String(),
Expand Down
12 changes: 12 additions & 0 deletions internal/checks/alerts_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1378,6 +1378,18 @@ func TestTemplateCheck(t *testing.T) {
}
},
},
{
description: "time - metric",
content: `
- alert: Foo
expr: (time() - foo_timestamp_unix) > 5*3600
labels:
notify: "{{ $labels.notify }}"
`,
checker: newTemplateCheck,
prometheus: noProm,
problems: noProblems,
},
}
runTests(t, testCases)
}
127 changes: 116 additions & 11 deletions internal/parser/utils/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type Source struct {
Selector *promParser.VectorSelector
Call *promParser.Call
Operation string
Returns promParser.ValueType
IncludedLabels []string // Labels that are included by filters, they will be present if exist on source series (by).
ExcludedLabels []string // Labels guaranteed to be excluded from the results (without).
GuaranteedLabels []string // Labels guaranteed to be present on the results (matchers).
Expand Down Expand Up @@ -98,7 +99,7 @@ func walkNode(node promParser.Node) (s Source) {
switch {
case n.VectorMatching == nil:
s = walkNode(n.LHS)
if s.Type == NumberSource {
if s.Returns == promParser.ValueTypeScalar || s.Returns == promParser.ValueTypeString {
s = walkNode(n.RHS)
}
case n.VectorMatching.Card == promParser.CardOneToOne:
Expand Down Expand Up @@ -139,12 +140,16 @@ func walkNode(node promParser.Node) (s Source) {

case *promParser.NumberLiteral:
s.Type = NumberSource
s.Returns = promParser.ValueTypeScalar
s.FixedLabels = true

case *promParser.ParenExpr:
s = walkNode(n.Expr)

case *promParser.StringLiteral:
s.Type = StringSource
s.Returns = promParser.ValueTypeString
s.FixedLabels = true

case *promParser.UnaryExpr:
s = walkNode(n.Expr)
Expand All @@ -154,16 +159,9 @@ func walkNode(node promParser.Node) (s Source) {

case *promParser.VectorSelector:
s.Type = SelectorSource
s.Returns = promParser.ValueTypeVector
s.Selector = n
// Any label used in positive filters is gurnateed to be present.
for _, lm := range s.Selector.LabelMatchers {
if lm.Name == labels.MetricName {
continue
}
if lm.Type == labels.MatchEqual || lm.Type == labels.MatchRegexp {
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, lm.Name)
}
}
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

default:
// unhandled type
Expand Down Expand Up @@ -193,6 +191,19 @@ func appendToSlice(dst []string, values ...string) []string {
return dst
}

func guaranteedLabelsFromSelector(selector *promParser.VectorSelector) (names []string) {
// Any label used in positive filters is gurnateed to be present.
for _, lm := range selector.LabelMatchers {
if lm.Name == labels.MetricName {
continue
}
if lm.Type == labels.MatchEqual || lm.Type == labels.MatchRegexp {
names = appendToSlice(names, lm.Name)
}
}
return names
}

func parseAggregation(n *promParser.AggregateExpr) (s Source) {
s = walkNode(n.Expr)
if n.Without {
Expand All @@ -212,6 +223,7 @@ func parseAggregation(n *promParser.AggregateExpr) (s Source) {
}
}
s.Type = AggregateSource
s.Returns = promParser.ValueTypeVector
s.Call = nil
return s
}
Expand All @@ -236,7 +248,29 @@ func parseCall(n *promParser.Call) (s Source) {
}
}

if n.Func.Name == "absent" {
switch n.Func.Name {
case "abs", "sgn", "acos", "acosh", "asin", "asinh", "atan", "atanh", "cos", "cosh", "sin", "sinh", "tan", "tanh":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "ceil", "floor", "round":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "changes", "resets":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "clamp", "clamp_max", "clamp_min":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "absent", "absent_over_time":
s.Returns = promParser.ValueTypeVector
s.FixedLabels = true
for _, lm := range s.Selector.LabelMatchers {
if lm.Name == labels.MetricName {
Expand All @@ -247,6 +281,77 @@ func parseCall(n *promParser.Call) (s Source) {
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, lm.Name)
}
}

case "avg_over_time", "count_over_time", "last_over_time", "max_over_time", "min_over_time", "present_over_time", "quantile_over_time", "stddev_over_time", "stdvar_over_time", "sum_over_time":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "days_in_month", "day_of_month", "day_of_week", "day_of_year", "hour", "minute", "month", "year":
s.Returns = promParser.ValueTypeVector
// No labels if we don't pass any arguments.
// Otherwise no change to labels.
if len(s.Call.Args) == 0 {
s.FixedLabels = true
} else {
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)
}

case "deg", "rad", "ln", "log10", "log2", "sqrt", "exp":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "delta", "idelta", "increase", "deriv", "irate", "rate":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "histogram_avg", "histogram_count", "histogram_sum", "histogram_stddev", "histogram_stdvar", "histogram_fraction", "histogram_quantile":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "holt_winters", "predict_linear":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "label_replace", "label_join":
// One label added to the results.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, s.Call.Args[1].(*promParser.StringLiteral).Val)

case "pi":
s.Returns = promParser.ValueTypeScalar
s.FixedLabels = true

case "scalar":
s.Returns = promParser.ValueTypeScalar
s.FixedLabels = true

case "sort", "sort_desc":
// No change to labels.
s.Returns = promParser.ValueTypeVector

case "time":
s.Returns = promParser.ValueTypeScalar
s.FixedLabels = true

case "timestamp":
// No change to labels.
s.Returns = promParser.ValueTypeVector
s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, guaranteedLabelsFromSelector(s.Selector)...)

case "vector":
s.Returns = promParser.ValueTypeVector
s.FixedLabels = true

default:
// Unsupported function
s.Returns = promParser.ValueTypeNone
s.Call = nil
}

return s
Expand Down
Loading

0 comments on commit 0fff8c1

Please sign in to comment.