diff --git a/internal/checks/alerts_template.go b/internal/checks/alerts_template.go index d83e6e09..2efed682 100644 --- a/internal/checks/alerts_template.go +++ b/internal/checks/alerts_template.go @@ -33,13 +33,9 @@ Since there are no matching time series there are also no labels. If some time s This means that the only labels you can get back from absent call are the ones you pass to it. If you're hoping to get instance specific labels this way and alert when some target is down then that won't work, use the ` + "`up`" + ` metric instead.` TemplateCheckOnDetails = `Using [vector matching](https://prometheus.io/docs/prometheus/latest/querying/operators/#vector-matching) operations will impact which labels are available on the results of your query. -When using ` + "`on()`" + `make sure that all labels you're trying to use in this templare match what the query can return.` +When using ` + "`on()`" + ` make sure that all labels you're trying to use in this templare match what the query can return. +For queries using ` + "ignoring()`" + ` any label included there will be stripped from the results.` TemplateCheckLabelsDetails = `This query doesn't seem to be using any time series and so cannot have any labels.` - - 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 ( @@ -73,10 +69,10 @@ var ( "humanizeDuration": dummyFuncMap, "humanizePercentage": dummyFuncMap, "humanizeTimestamp": dummyFuncMap, + "toTime": dummyFuncMap, "pathPrefix": dummyFuncMap, "externalURL": dummyFuncMap, "parseDuration": dummyFuncMap, - "toTime": dummyFuncMap, } ) @@ -456,11 +452,11 @@ func checkQueryLabels(labelName, labelValue string, src utils.Source) (problems } for _, s := range append(src.Alternatives, src) { if s.FixedLabels && !slices.Contains(s.IncludedLabels, v[1]) { - problems = append(problems, textForProblem(v[1], s, Bug)) + problems = append(problems, textForProblem(v[1], "", s, Bug)) goto NEXT } if slices.Contains(s.ExcludedLabels, v[1]) { - problems = append(problems, textForProblem(v[1], s, Bug)) + problems = append(problems, textForProblem(v[1], v[1], s, Bug)) goto NEXT } } @@ -473,19 +469,23 @@ func checkQueryLabels(labelName, labelValue string, src utils.Source) (problems return problems } -func textForProblem(label string, src utils.Source, severity Severity) exprProblem { - // FIXME add query fragment to the details - +func textForProblem(label, reasonLabel string, src utils.Source, severity Severity) exprProblem { switch { case src.Operation == "absent": return exprProblem{ - text: fmt.Sprintf(msgAbsent, label), + text: fmt.Sprintf("Template is using `%s` label but `%s` is not passing it.", label, src.Call.String()), details: TemplateCheckAbsentDetails, severity: severity, } - case src.Returns == promParser.ValueTypeScalar, src.Returns == promParser.ValueTypeString, src.Operation == "vector": + case src.Operation == "vector": + return exprProblem{ + text: fmt.Sprintf("Template is using `%s` label but `%s` doesn't produce any labels.", label, src.Call.String()), + details: TemplateCheckLabelsDetails, + severity: severity, + } + case src.Returns == promParser.ValueTypeScalar, src.Returns == promParser.ValueTypeString: return exprProblem{ - text: fmt.Sprintf(msgVector, label), + text: fmt.Sprintf("Template is using `%s` label but the query doesn't produce any labels.", label), details: TemplateCheckLabelsDetails, severity: severity, } @@ -496,13 +496,14 @@ func textForProblem(label string, src utils.Source, severity Severity) exprProbl promParser.CardManyToOne.String(), }, src.Operation): return exprProblem{ - text: fmt.Sprintf(msgOn, label), + text: fmt.Sprintf("Template is using `%s` label but the query results won't have this label. %s", + label, src.ExcludeReason[reasonLabel]), details: TemplateCheckOnDetails, severity: severity, } default: return exprProblem{ - text: fmt.Sprintf(msgAggregation, label), + text: fmt.Sprintf("Template is using `%s` label but the query removes it.", label), details: TemplateCheckAggregationDetails, severity: severity, } diff --git a/internal/checks/alerts_template_test.go b/internal/checks/alerts_template_test.go index 2a329f3b..21f09902 100644 --- a/internal/checks/alerts_template_test.go +++ b/internal/checks/alerts_template_test.go @@ -496,7 +496,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `instance` label but `absent()` is not passing it.", + Text: "Template is using `instance` label but `absent(foo{job=\"bar\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -506,7 +506,7 @@ func TestTemplateCheck(t *testing.T) { Last: 7, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `instance` label but `absent()` is not passing it.", + Text: "Template is using `instance` label but `absent(foo{job=\"bar\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -516,7 +516,7 @@ func TestTemplateCheck(t *testing.T) { Last: 7, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `foo` label but `absent()` is not passing it.", + Text: "Template is using `foo` label but `absent(foo{job=\"bar\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -526,7 +526,7 @@ func TestTemplateCheck(t *testing.T) { Last: 8, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `xxx` label but `absent()` is not passing it.", + Text: "Template is using `xxx` label but `absent(foo{job=\"bar\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -551,7 +551,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `instance` label but `absent()` is not passing it.", + Text: "Template is using `instance` label but `absent(sum by (job, instance) (foo))` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -561,7 +561,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `job` label but `absent()` is not passing it.", + Text: "Template is using `job` label but `absent(sum by (job, instance) (foo))` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -586,7 +586,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `instance` label but `absent()` is not passing it.", + Text: "Template is using `instance` label but `absent(sum by (job) (foo))` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -596,7 +596,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `job` label but `absent()` is not passing it.", + Text: "Template is using `job` label but `absent(sum by (job) (foo))` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -621,7 +621,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `job` label but `absent()` is not passing it.", + Text: "Template is using `job` label but `absent({job=~\".+\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -646,7 +646,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `job` label but `absent()` is not passing it.", + Text: "Template is using `job` label but `absent(bar)` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -683,7 +683,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `cluster` label but `absent()` is not passing it.", + Text: "Template is using `cluster` label but `absent(foo{job=\"xxx\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -693,7 +693,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `env` label but `absent()` is not passing it.", + Text: "Template is using `env` label but `absent(foo{job=\"xxx\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -730,7 +730,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `cluster` label but `absent()` is not passing it.", + Text: "Template is using `cluster` label but `absent(foo{job=\"xxx\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -740,7 +740,7 @@ func TestTemplateCheck(t *testing.T) { Last: 5, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `env` label but `absent()` is not passing it.", + Text: "Template is using `env` label but `absent(foo{job=\"xxx\"})` is not passing it.", Details: checks.TemplateCheckAbsentDetails, Severity: checks.Bug, }, @@ -1216,7 +1216,7 @@ func TestTemplateCheck(t *testing.T) { Last: 4, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `instance` label but the query doesn't produce any labels.", + Text: "Template is using `instance` label but `vector(1)` doesn't produce any labels.", Details: checks.TemplateCheckLabelsDetails, Severity: checks.Bug, }, @@ -1236,7 +1236,7 @@ func TestTemplateCheck(t *testing.T) { Last: 4, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `instance` label but the query doesn't produce any labels.", + Text: "Template is using `instance` label but `vector(1)` doesn't produce any labels.", Details: checks.TemplateCheckLabelsDetails, Severity: checks.Bug, }, @@ -1292,7 +1292,7 @@ func TestTemplateCheck(t *testing.T) { Last: 6, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `job_name` label but the query uses `on(...)` without it being set there, this label will be missing from the query result.", + Text: "Template is using `job_name` label but the query results won't have this label. Query is using one-to-one vector matching with `on(instance, app_name)`, only labels included inside `on(...)` will be present on the results.", Details: checks.TemplateCheckOnDetails, Severity: checks.Bug, }, @@ -1302,7 +1302,7 @@ func TestTemplateCheck(t *testing.T) { Last: 4, }, Reporter: checks.TemplateCheckName, - Text: "Template is using `app_type` label but the query uses `on(...)` without it being set there, this label will be missing from the query result.", + Text: "Template is using `app_type` label but the query results won't have this label. Query is using one-to-one vector matching with `on(instance, app_name)`, only labels included inside `on(...)` will be present on the results.", Details: checks.TemplateCheckOnDetails, Severity: checks.Bug, }, @@ -1390,6 +1390,31 @@ func TestTemplateCheck(t *testing.T) { prometheus: noProm, problems: noProblems, }, + { + description: "bar * ignoring(job) foo", + content: ` +- alert: Foo + expr: bar * ignoring(job) foo + annotations: + summary: '{{ .Labels.job }} in cluster {{$labels.cluster}}/{{ $labels.env }} is missing' +`, + checker: newTemplateCheck, + prometheus: noProm, + problems: func(_ string) []checks.Problem { + return []checks.Problem{ + { + Lines: parser.LineRange{ + First: 5, + Last: 5, + }, + Reporter: checks.TemplateCheckName, + Text: "Template is using `job` label but the query results won't have this label. Query is using one-to-one vector matching with `ignoring(job)`, all labels included inside `ignoring(...)` will be removed on the results.", + Details: checks.TemplateCheckOnDetails, + Severity: checks.Bug, + }, + } + }, + }, } runTests(t, testCases) } diff --git a/internal/parser/utils/source.go b/internal/parser/utils/source.go index 7de012f2..c047936e 100644 --- a/internal/parser/utils/source.go +++ b/internal/parser/utils/source.go @@ -1,7 +1,9 @@ package utils import ( + "fmt" "slices" + "strings" "github.com/cloudflare/pint/internal/parser" @@ -23,6 +25,7 @@ const ( type Source struct { Selector *promParser.VectorSelector Call *promParser.Call + ExcludeReason map[string]string // Reason why a label was excluded Operation string Returns promParser.ValueType IncludedLabels []string // Labels that are included by filters, they will be present if exist on source series (by). @@ -71,6 +74,8 @@ func walkNode(node promParser.Node) (s Source) { // Param is the label to store the count value in. s.GuaranteedLabels = appendToSlice(s.GuaranteedLabels, n.Param.(*promParser.StringLiteral).Val) s.IncludedLabels = appendToSlice(s.IncludedLabels, n.Param.(*promParser.StringLiteral).Val) + s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, n.Param.(*promParser.StringLiteral).Val) + delete(s.ExcludeReason, n.Param.(*promParser.StringLiteral).Val) case promParser.QUANTILE: s = parseAggregation(n) s.Operation = "quantile" @@ -96,38 +101,7 @@ func walkNode(node promParser.Node) (s Source) { } case *promParser.BinaryExpr: - switch { - case n.VectorMatching == nil: - s = walkNode(n.LHS) - if s.Returns == promParser.ValueTypeScalar || s.Returns == promParser.ValueTypeString { - s = walkNode(n.RHS) - } - case n.VectorMatching.Card == promParser.CardOneToOne: - s = walkNode(n.LHS) - if n.VectorMatching.On { - s.FixedLabels = true - } - case n.VectorMatching.Card == promParser.CardOneToMany: - s = walkNode(n.RHS) - case n.VectorMatching.Card == promParser.CardManyToMany: - s = walkNode(n.LHS) - if n.Op == promParser.LOR { - s.Alternatives = append(s.Alternatives, walkNode(n.RHS)) - } - case n.VectorMatching.Card == promParser.CardManyToOne: - s = walkNode(n.LHS) - } - if n.VectorMatching != nil { - if s.Operation == "" { - s.Operation = n.VectorMatching.Card.String() - } - s.IncludedLabels = appendToSlice(s.IncludedLabels, n.VectorMatching.Include...) - // on=true when using on(), on=false when using ignore() - if n.VectorMatching.On { - s.IncludedLabels = appendToSlice(s.IncludedLabels, n.VectorMatching.MatchingLabels...) - } - s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, n.VectorMatching.Include...) - } + s = parseBinOps(n) case *promParser.Call: s = parseCall(n) @@ -191,6 +165,14 @@ func appendToSlice(dst []string, values ...string) []string { return dst } +func setInMap(dst map[string]string, key, val string) map[string]string { + if dst == nil { + dst = map[string]string{} + } + dst[key] = val + 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 { @@ -210,6 +192,14 @@ func parseAggregation(n *promParser.AggregateExpr) (s Source) { s.ExcludedLabels = appendToSlice(s.ExcludedLabels, n.Grouping...) s.IncludedLabels = removeFromSlice(s.IncludedLabels, n.Grouping...) s.GuaranteedLabels = removeFromSlice(s.GuaranteedLabels, n.Grouping...) + for _, name := range n.Grouping { + s.ExcludeReason = setInMap( + s.ExcludeReason, + name, + fmt.Sprintf("Query is using aggregation with `without(%s)`, all labels included inside `without(...)` will be removed from the results.", + strings.Join(n.Grouping, ", ")), + ) + } } else { s.FixedLabels = true if len(n.Grouping) == 0 { @@ -220,6 +210,12 @@ func parseAggregation(n *promParser.AggregateExpr) (s Source) { for _, name := range n.Grouping { s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, name) } + s.ExcludeReason = setInMap( + s.ExcludeReason, + "", + fmt.Sprintf("Query is using aggregation with `by(%s)`, only labels included inside `by(...)` will be present on the results.", + strings.Join(n.Grouping, ", ")), + ) } } s.Type = AggregateSource @@ -356,3 +352,66 @@ func parseCall(n *promParser.Call) (s Source) { return s } + +func parseBinOps(n *promParser.BinaryExpr) (s Source) { + switch { + case n.VectorMatching == nil: + s = walkNode(n.LHS) + if s.Returns == promParser.ValueTypeScalar || s.Returns == promParser.ValueTypeString { + s = walkNode(n.RHS) + } + + case n.VectorMatching.Card == promParser.CardOneToOne: + s = walkNode(n.LHS) + if n.VectorMatching.On { + s.FixedLabels = true + s.ExcludeReason = setInMap(s.ExcludeReason, "", fmt.Sprintf( + "Query is using %s vector matching with `on(%s)`, only labels included inside `on(...)` will be present on the results.", + n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "), + )) + } + + case n.VectorMatching.Card == promParser.CardOneToMany: + s = walkNode(n.RHS) + + case n.VectorMatching.Card == promParser.CardManyToMany: + s = walkNode(n.LHS) + if n.Op == promParser.LOR { + s.Alternatives = append(s.Alternatives, walkNode(n.RHS)) + } + + case n.VectorMatching.Card == promParser.CardManyToOne: + s = walkNode(n.LHS) + } + + if n.VectorMatching != nil { + if s.Operation == "" { + s.Operation = n.VectorMatching.Card.String() + } + s.IncludedLabels = appendToSlice(s.IncludedLabels, n.VectorMatching.Include...) + // on=true when using on(), on=false when using ignore() + if n.VectorMatching.On { + s.IncludedLabels = appendToSlice(s.IncludedLabels, n.VectorMatching.MatchingLabels...) + s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, n.VectorMatching.MatchingLabels...) + for _, name := range n.VectorMatching.MatchingLabels { + delete(s.ExcludeReason, name) + } + } else if n.VectorMatching.Card == promParser.CardOneToOne { + s.IncludedLabels = removeFromSlice(s.IncludedLabels, n.VectorMatching.MatchingLabels...) + s.GuaranteedLabels = removeFromSlice(s.GuaranteedLabels, n.VectorMatching.MatchingLabels...) + s.ExcludedLabels = appendToSlice(s.ExcludedLabels, n.VectorMatching.MatchingLabels...) + for _, name := range n.VectorMatching.MatchingLabels { + s.ExcludeReason = setInMap(s.ExcludeReason, name, fmt.Sprintf( + "Query is using %s vector matching with `ignoring(%s)`, all labels included inside `ignoring(...)` will be removed on the results.", + n.VectorMatching.Card, strings.Join(n.VectorMatching.MatchingLabels, ", "), + )) + } + } + s.ExcludedLabels = removeFromSlice(s.ExcludedLabels, n.VectorMatching.Include...) + for _, name := range n.VectorMatching.Include { + delete(s.ExcludeReason, name) + } + } + + return s +} diff --git a/internal/parser/utils/source_test.go b/internal/parser/utils/source_test.go index c107a619..9d78a459 100644 --- a/internal/parser/utils/source_test.go +++ b/internal/parser/utils/source_test.go @@ -47,6 +47,14 @@ func TestLabelsSource(t *testing.T) { FixedLabels: true, }, }, + { + expr: "1 / 5", + output: utils.Source{ + Type: utils.NumberSource, + Returns: promParser.ValueTypeScalar, + FixedLabels: true, + }, + }, { expr: `"test"`, output: utils.Source{ @@ -134,6 +142,14 @@ func TestLabelsSource(t *testing.T) { Selector: mustParseVector("foo", 0), }, }, + { + expr: "foo / 5", + output: utils.Source{ + Type: utils.SelectorSource, + Returns: promParser.ValueTypeVector, + Selector: mustParseVector("foo", 0), + }, + }, { expr: "-foo", output: utils.Source{ @@ -160,6 +176,9 @@ func TestLabelsSource(t *testing.T) { Operation: "sum", Selector: mustParseVector(`foo{job="myjob"}`, 4), ExcludedLabels: []string{"job"}, + ExcludeReason: map[string]string{ + "job": "Query is using aggregation with `without(job)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -171,6 +190,9 @@ func TestLabelsSource(t *testing.T) { Selector: mustParseVector(`foo`, 4), IncludedLabels: []string{"job"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(job)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -183,6 +205,9 @@ func TestLabelsSource(t *testing.T) { IncludedLabels: []string{"job"}, GuaranteedLabels: []string{"job"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(job)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -194,6 +219,9 @@ func TestLabelsSource(t *testing.T) { Selector: mustParseVector(`foo{job="myjob"}`, 4), GuaranteedLabels: []string{"job"}, ExcludedLabels: []string{"instance"}, + ExcludeReason: map[string]string{ + "instance": "Query is using aggregation with `without(instance)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -226,6 +254,9 @@ func TestLabelsSource(t *testing.T) { GuaranteedLabels: []string{"job"}, IncludedLabels: []string{"job"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(job)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -237,6 +268,9 @@ func TestLabelsSource(t *testing.T) { Selector: mustParseVector(`foo`, 6), IncludedLabels: []string{"job"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(job)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -343,6 +377,9 @@ func TestLabelsSource(t *testing.T) { IncludedLabels: []string{"version"}, GuaranteedLabels: []string{"version"}, ExcludedLabels: []string{"job"}, + ExcludeReason: map[string]string{ + "job": "Query is using aggregation with `without(job)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -355,6 +392,9 @@ func TestLabelsSource(t *testing.T) { IncludedLabels: []string{"version"}, GuaranteedLabels: []string{"version"}, ExcludedLabels: []string{"job"}, + ExcludeReason: map[string]string{ + "job": "Query is using aggregation with `without(job)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -367,6 +407,9 @@ func TestLabelsSource(t *testing.T) { GuaranteedLabels: []string{"version"}, IncludedLabels: []string{"job", "version"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(job)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -413,6 +456,9 @@ func TestLabelsSource(t *testing.T) { Operation: "sum", Selector: mustParseVector(`foo`, 9), ExcludedLabels: []string{"instance"}, + ExcludeReason: map[string]string{ + "instance": "Query is using aggregation with `without(instance)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -435,6 +481,9 @@ func TestLabelsSource(t *testing.T) { GuaranteedLabels: []string{"job"}, IncludedLabels: []string{"instance"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using one-to-one vector matching with `on(instance)`, only labels included inside `on(...)` will be present on the results.", + }, }, }, { @@ -488,6 +537,22 @@ func TestLabelsSource(t *testing.T) { Operation: "count", Selector: mustParseVector(`up{job="a"}`, 6), FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using one-to-one vector matching with `on()`, only labels included inside `on(...)` will be present on the results.", + }, + }, + }, + { + expr: `count(up{job="a"} / on (env) up{job="b"})`, + output: utils.Source{ + Type: utils.AggregateSource, + Returns: promParser.ValueTypeVector, + Operation: "count", + Selector: mustParseVector(`up{job="a"}`, 6), + FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using one-to-one vector matching with `on(env)`, only labels included inside `on(...)` will be present on the results.", + }, }, }, { @@ -520,6 +585,24 @@ func TestLabelsSource(t *testing.T) { Selector: mustParseVector(`foo`, 9), }, }, + { + expr: `topk(10, foo) without(cluster)`, + output: utils.Source{ + Type: utils.AggregateSource, + Returns: promParser.ValueTypeVector, + Operation: "topk", + Selector: mustParseVector(`foo`, 9), + }, + }, + { + expr: `topk(10, foo) by(cluster)`, + output: utils.Source{ + Type: utils.AggregateSource, + Returns: promParser.ValueTypeVector, + Operation: "topk", + Selector: mustParseVector(`foo`, 9), + }, + }, { expr: `bottomk(10, sum(rate(foo[5m])) without(job))`, output: utils.Source{ @@ -528,6 +611,9 @@ func TestLabelsSource(t *testing.T) { Operation: "bottomk", Selector: mustParseVector(`foo`, 21), ExcludedLabels: []string{"job"}, + ExcludeReason: map[string]string{ + "job": "Query is using aggregation with `without(job)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -588,6 +674,15 @@ func TestLabelsSource(t *testing.T) { }, }, }, + { + expr: `foo unless bar`, + output: utils.Source{ + Type: utils.SelectorSource, + Returns: promParser.ValueTypeVector, + Operation: promParser.CardManyToMany.String(), + Selector: mustParseVector(`foo`, 0), + }, + }, { expr: `count(sum(up{job="foo", cluster="dev"}) by(job, cluster) == 0) without(job, cluster)`, output: utils.Source{ @@ -595,8 +690,13 @@ func TestLabelsSource(t *testing.T) { Returns: promParser.ValueTypeVector, Operation: "count", Selector: mustParseVector(`up{job="foo", cluster="dev"}`, 10), - ExcludedLabels: []string{"job", "cluster"}, + ExcludedLabels: []string{"job", "cluster"}, // FIXME empty FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(job, cluster)`, only labels included inside `by(...)` will be present on the results.", + "job": "Query is using aggregation with `without(job, cluster)`, all labels included inside `without(...)` will be removed from the results.", + "cluster": "Query is using aggregation with `without(job, cluster)`, all labels included inside `without(...)` will be removed from the results.", + }, }, }, { @@ -714,6 +814,7 @@ sum(foo:count) by(job) > 20`, Operation: "sum", Selector: mustParseVector(`foo:sum`, 8), IncludedLabels: []string{"notify", "job"}, + ExcludeReason: map[string]string{}, }, }, { @@ -725,6 +826,9 @@ sum(foo:count) by(job) > 20`, Selector: mustParseVector(`container_file_descriptors`, 0), IncludedLabels: []string{"instance", "app_name"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using one-to-one vector matching with `on(instance, app_name)`, only labels included inside `on(...)` will be present on the results.", + }, }, }, { @@ -868,6 +972,9 @@ sum(foo:count) by(job) > 20`, Selector: mustParseVector(`foo`, 8), IncludedLabels: []string{"notjob"}, FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(notjob)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -879,6 +986,9 @@ sum(foo:count) by(job) > 20`, Selector: mustParseVector(`node_exporter_build_info`, 6), IncludedLabels: []string{"instance", "version", "foo"}, // FIXME foo shouldn't be there because count() doesn't produce it FixedLabels: true, + ExcludeReason: map[string]string{ + "": "Query is using aggregation with `by(instance, version)`, only labels included inside `by(...)` will be present on the results.", + }, }, }, { @@ -1222,6 +1332,20 @@ sum(foo:count) by(job) > 20`, Selector: mustParseVector("my_metric", 10), }, }, + { + expr: `up{instance="a", job="prometheus"} * ignoring(job) up{instance="a", job="pint"}`, + output: utils.Source{ + Type: utils.SelectorSource, + Returns: promParser.ValueTypeVector, + Operation: promParser.CardOneToOne.String(), + Selector: mustParseVector(`up{instance="a", job="prometheus"}`, 0), + GuaranteedLabels: []string{"instance"}, + ExcludedLabels: []string{"job"}, + ExcludeReason: map[string]string{ + "job": "Query is using one-to-one vector matching with `ignoring(job)`, all labels included inside `ignoring(...)` will be removed on the results.", + }, + }, + }, } for _, tc := range testCases {