Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More complete label source coverage #1172

Merged
merged 1 commit into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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