diff --git a/promql/ast.go b/promql/ast.go index 9606bb2e8..5a7e24caf 100644 --- a/promql/ast.go +++ b/promql/ast.go @@ -231,6 +231,9 @@ type VectorMatching struct { // On contains the labels which define equality of a pair // of elements from the vectors. On model.LabelNames + // Ignoring excludes the given label names from matching, + // rather than only using them. + Ignoring bool // Include contains additional labels that should be included in // the result from the side with the higher cardinality. Include model.LabelNames diff --git a/promql/engine.go b/promql/engine.go index 5005f6a6f..2dd010b88 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -728,7 +728,7 @@ func (ev *evaluator) vectorAnd(lhs, rhs vector, matching *VectorMatching) vector if matching.Card != CardManyToMany { panic("set operations must only use many-to-many matching") } - sigf := signatureFunc(matching.On...) + sigf := signatureFunc(matching.Ignoring, matching.On...) var result vector // The set of signatures for the right-hand side vector. @@ -751,7 +751,7 @@ func (ev *evaluator) vectorOr(lhs, rhs vector, matching *VectorMatching) vector if matching.Card != CardManyToMany { panic("set operations must only use many-to-many matching") } - sigf := signatureFunc(matching.On...) + sigf := signatureFunc(matching.Ignoring, matching.On...) var result vector leftSigs := map[uint64]struct{}{} @@ -773,7 +773,7 @@ func (ev *evaluator) vectorUnless(lhs, rhs vector, matching *VectorMatching) vec if matching.Card != CardManyToMany { panic("set operations must only use many-to-many matching") } - sigf := signatureFunc(matching.On...) + sigf := signatureFunc(matching.Ignoring, matching.On...) rightSigs := map[uint64]struct{}{} for _, rs := range rhs { @@ -796,7 +796,7 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorM } var ( result = vector{} - sigf = signatureFunc(matching.On...) + sigf = signatureFunc(matching.Ignoring, matching.On...) resultLabels = append(matching.On, matching.Include...) ) @@ -851,7 +851,7 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorM } else if !keep { continue } - metric := resultMetric(ls.Metric, op, resultLabels...) + metric := resultMetric(ls.Metric, op, matching.Ignoring, resultLabels...) insertedSigs, exists := matchedSigs[sig] if matching.Card == CardOneToOne { @@ -883,12 +883,16 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorM } // signatureFunc returns a function that calculates the signature for a metric -// based on the provided labels. -func signatureFunc(labels ...model.LabelName) func(m metric.Metric) uint64 { - if len(labels) == 0 { +// based on the provided labels. If ignoring, then the given labels are ignored instead. +func signatureFunc(ignoring bool, labels ...model.LabelName) func(m metric.Metric) uint64 { + if len(labels) == 0 || ignoring { return func(m metric.Metric) uint64 { - m.Del(model.MetricNameLabel) - return uint64(m.Metric.Fingerprint()) + tmp := m.Metric.Clone() + for _, l := range labels { + delete(tmp, l) + } + delete(tmp, model.MetricNameLabel) + return uint64(tmp.Fingerprint()) } } return func(m metric.Metric) uint64 { @@ -898,11 +902,14 @@ func signatureFunc(labels ...model.LabelName) func(m metric.Metric) uint64 { // resultMetric returns the metric for the given sample(s) based on the vector // binary operation and the matching options. -func resultMetric(met metric.Metric, op itemType, labels ...model.LabelName) metric.Metric { - if len(labels) == 0 { +func resultMetric(met metric.Metric, op itemType, ignoring bool, labels ...model.LabelName) metric.Metric { + if len(labels) == 0 || ignoring { if shouldDropMetricName(op) { met.Del(model.MetricNameLabel) } + for _, l := range labels { + met.Del(l) + } return met } // As we definitely write, creating a new metric is the easiest solution. diff --git a/promql/lex.go b/promql/lex.go index fbd999352..cbfa877b2 100644 --- a/promql/lex.go +++ b/promql/lex.go @@ -172,6 +172,7 @@ const ( itemBy itemWithout itemOn + itemIgnoring itemGroupLeft itemGroupRight itemBool @@ -209,6 +210,7 @@ var key = map[string]itemType{ "keeping_extra": itemKeepCommon, "keep_common": itemKeepCommon, "on": itemOn, + "ignoring": itemIgnoring, "group_left": itemGroupLeft, "group_right": itemGroupRight, "bool": itemBool, diff --git a/promql/lex_test.go b/promql/lex_test.go index 549b24f86..49ab711f9 100644 --- a/promql/lex_test.go +++ b/promql/lex_test.go @@ -278,6 +278,9 @@ var tests = []struct { }, { input: "on", expected: []item{{itemOn, 0, "on"}}, + }, { + input: "ignoring", + expected: []item{{itemIgnoring, 0, "ignoring"}}, }, { input: "group_left", expected: []item{{itemGroupLeft, 0, "group_left"}}, diff --git a/promql/parse.go b/promql/parse.go index 125e1060b..ba246d424 100644 --- a/promql/parse.go +++ b/promql/parse.go @@ -462,7 +462,10 @@ func (p *parser) expr() Expr { } // Parse ON clause. - if p.peek().typ == itemOn { + if p.peek().typ == itemOn || p.peek().typ == itemIgnoring { + if p.peek().typ == itemIgnoring { + vecMatching.Ignoring = true + } p.next() vecMatching.On = p.labels() @@ -478,6 +481,10 @@ func (p *parser) expr() Expr { } } + if vecMatching.Ignoring && (vecMatching.Card == CardManyToOne || vecMatching.Card == CardOneToMany) { + p.errorf("IGNORING not permitted with many to one matching") + } + for _, ln := range vecMatching.On { for _, ln2 := range vecMatching.Include { if ln == ln2 { diff --git a/promql/parse_test.go b/promql/parse_test.go index c400ba077..22a07a138 100644 --- a/promql/parse_test.go +++ b/promql/parse_test.go @@ -250,6 +250,14 @@ var testExpr = []struct { input: "1 offset 1d", fail: true, errMsg: "offset modifier must be preceded by an instant or range selector", + }, { + input: "a - on(b) ignoring(c) d", + fail: true, + errMsg: "parse error at char 11: no valid expression found", + }, { + input: "a - ignoring(b) group_left(c) d", + fail: true, + errMsg: "parse error at char 29: IGNORING not permitted with many to one matching", }, // Vector binary operations. { @@ -517,6 +525,28 @@ var testExpr = []struct { On: model.LabelNames{"test", "blub"}, }, }, + }, { + input: "foo and ignoring(test,blub) bar", + expected: &BinaryExpr{ + Op: itemLAND, + LHS: &VectorSelector{ + Name: "foo", + LabelMatchers: metric.LabelMatchers{ + {Type: metric.Equal, Name: model.MetricNameLabel, Value: "foo"}, + }, + }, + RHS: &VectorSelector{ + Name: "bar", + LabelMatchers: metric.LabelMatchers{ + {Type: metric.Equal, Name: model.MetricNameLabel, Value: "bar"}, + }, + }, + VectorMatching: &VectorMatching{ + Card: CardManyToMany, + On: model.LabelNames{"test", "blub"}, + Ignoring: true, + }, + }, }, { input: "foo unless on(bar) baz", expected: &BinaryExpr{ diff --git a/promql/printer.go b/promql/printer.go index 748d77461..06b810b90 100644 --- a/promql/printer.go +++ b/promql/printer.go @@ -160,7 +160,11 @@ func (node *BinaryExpr) String() string { matching := "" vm := node.VectorMatching if vm != nil && len(vm.On) > 0 { - matching = fmt.Sprintf(" ON(%s)", vm.On) + if vm.Ignoring { + matching = fmt.Sprintf(" IGNORING(%s)", vm.On) + } else { + matching = fmt.Sprintf(" ON(%s)", vm.On) + } if vm.Card == CardManyToOne { matching += fmt.Sprintf(" GROUP_LEFT(%s)", vm.Include) } diff --git a/promql/printer_test.go b/promql/printer_test.go index 29b275e0b..e17774ac8 100644 --- a/promql/printer_test.go +++ b/promql/printer_test.go @@ -36,6 +36,12 @@ func TestExprString(t *testing.T) { { in: `sum(task:errors:rate10s{job="s"}) WITHOUT (instance)`, }, + { + in: `a - ON(b) c`, + }, + { + in: `a - IGNORING(b) c`, + }, { in: `up > BOOL 0`, }, diff --git a/promql/testdata/operators.test b/promql/testdata/operators.test index c85984dfb..1c095ebae 100644 --- a/promql/testdata/operators.test +++ b/promql/testdata/operators.test @@ -79,6 +79,14 @@ eval instant at 50m (http_requests{group="canary"} + 1) and on(instance) http_re {group="canary", instance="0", job="api-server"} 301 {group="canary", instance="0", job="app-server"} 701 +eval instant at 50m (http_requests{group="canary"} + 1) and ignoring(group) http_requests{instance="0", group="production"} + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + +eval instant at 50m (http_requests{group="canary"} + 1) and ignoring(group, job) http_requests{instance="0", group="production"} + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + eval instant at 50m http_requests{group="canary"} or http_requests{group="production"} http_requests{group="canary", instance="0", job="api-server"} 300 http_requests{group="canary", instance="0", job="app-server"} 700 @@ -109,6 +117,14 @@ eval instant at 50m (http_requests{group="canary"} + 1) or on(instance) (http_re vector_matching_a{l="x"} 10 vector_matching_a{l="y"} 20 +eval instant at 50m (http_requests{group="canary"} + 1) or ignoring(l, group, job) (http_requests or cpu_count or vector_matching_a) + {group="canary", instance="0", job="api-server"} 301 + {group="canary", instance="0", job="app-server"} 701 + {group="canary", instance="1", job="api-server"} 401 + {group="canary", instance="1", job="app-server"} 801 + vector_matching_a{l="x"} 10 + vector_matching_a{l="y"} 20 + eval instant at 50m http_requests{group="canary"} unless http_requests{instance="0"} http_requests{group="canary", instance="1", job="api-server"} 400 http_requests{group="canary", instance="1", job="app-server"} 800 @@ -125,6 +141,18 @@ eval instant at 50m http_requests{group="canary"} / on(instance,job) http_reques {instance="1", job="api-server"} 2 {instance="1", job="app-server"} 1.3333333333333333 +eval instant at 50m http_requests{group="canary"} unless ignoring(group, instance) http_requests{instance="0"} + +eval instant at 50m http_requests{group="canary"} unless ignoring(group) http_requests{instance="0"} + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="1", job="app-server"} 800 + +eval instant at 50m http_requests{group="canary"} / ignoring(group) http_requests{group="production"} + {instance="0", job="api-server"} 3 + {instance="0", job="app-server"} 1.4 + {instance="1", job="api-server"} 2 + {instance="1", job="app-server"} 1.3333333333333333 + # https://github.com/prometheus/prometheus/issues/1489 eval instant at 50m http_requests AND ON (dummy) vector(1) http_requests{group="canary", instance="0", job="api-server"} 300 @@ -136,6 +164,16 @@ eval instant at 50m http_requests AND ON (dummy) vector(1) http_requests{group="production", instance="1", job="api-server"} 200 http_requests{group="production", instance="1", job="app-server"} 600 +eval instant at 50m http_requests AND IGNORING (group, instance, job) vector(1) + http_requests{group="canary", instance="0", job="api-server"} 300 + http_requests{group="canary", instance="0", job="app-server"} 700 + http_requests{group="canary", instance="1", job="api-server"} 400 + http_requests{group="canary", instance="1", job="app-server"} 800 + http_requests{group="production", instance="0", job="api-server"} 100 + http_requests{group="production", instance="0", job="app-server"} 500 + http_requests{group="production", instance="1", job="api-server"} 200 + http_requests{group="production", instance="1", job="app-server"} 600 + # Comparisons. eval instant at 50m SUM(http_requests) BY (job) > 1000