diff --git a/promql/ast.go b/promql/ast.go index e82386147..b54a67f2d 100644 --- a/promql/ast.go +++ b/promql/ast.go @@ -117,6 +117,9 @@ type BinaryExpr struct { // The matching behavior for the operation if both operands are vectors. // If they are not this field is nil. VectorMatching *VectorMatching + + // If a comparison operator, return 0/1 rather than filtering. + ReturnBool bool } // Call represents a function call. diff --git a/promql/engine.go b/promql/engine.go index 1823776b0..8d657cf28 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -639,13 +639,13 @@ func (ev *evaluator) eval(expr Expr) model.Value { case itemLOR: return ev.vectorOr(lhs.(vector), rhs.(vector), e.VectorMatching) default: - return ev.vectorBinop(e.Op, lhs.(vector), rhs.(vector), e.VectorMatching) + return ev.vectorBinop(e.Op, lhs.(vector), rhs.(vector), e.VectorMatching, e.ReturnBool) } case lt == model.ValVector && rt == model.ValScalar: - return ev.vectorScalarBinop(e.Op, lhs.(vector), rhs.(*model.Scalar), false) + return ev.vectorScalarBinop(e.Op, lhs.(vector), rhs.(*model.Scalar), false, e.ReturnBool) case lt == model.ValScalar && rt == model.ValVector: - return ev.vectorScalarBinop(e.Op, rhs.(vector), lhs.(*model.Scalar), true) + return ev.vectorScalarBinop(e.Op, rhs.(vector), lhs.(*model.Scalar), true, e.ReturnBool) } case *Call: @@ -800,7 +800,7 @@ func (ev *evaluator) vectorOr(lhs, rhs vector, matching *VectorMatching) vector } // vectorBinop evaluates a binary operation between two vector, excluding AND and OR. -func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorMatching) vector { +func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorMatching, returnBool bool) vector { if matching.Card == CardManyToMany { panic("many-to-many only allowed for AND and OR") } @@ -852,7 +852,13 @@ func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorM vl, vr = vr, vl } value, keep := vectorElemBinop(op, vl, vr) - if !keep { + if returnBool { + if keep { + value = 1.0 + } else { + value = 0.0 + } + } else if !keep { continue } metric := resultMetric(ls.Metric, op, resultLabels...) @@ -921,7 +927,7 @@ func resultMetric(met metric.Metric, op itemType, labels ...model.LabelName) met } // vectorScalarBinop evaluates a binary operation between a vector and a scalar. -func (ev *evaluator) vectorScalarBinop(op itemType, lhs vector, rhs *model.Scalar, swap bool) vector { +func (ev *evaluator) vectorScalarBinop(op itemType, lhs vector, rhs *model.Scalar, swap, returnBool bool) vector { vec := make(vector, 0, len(lhs)) for _, lhsSample := range lhs { @@ -932,6 +938,14 @@ func (ev *evaluator) vectorScalarBinop(op itemType, lhs vector, rhs *model.Scala lv, rv = rv, lv } value, keep := vectorElemBinop(op, lv, rv) + if returnBool { + if keep { + value = 1.0 + } else { + value = 0.0 + } + keep = true + } if keep { lhsSample.Value = value if shouldDropMetricName(op) { diff --git a/promql/lex.go b/promql/lex.go index ddd46ef91..9cdccd04b 100644 --- a/promql/lex.go +++ b/promql/lex.go @@ -151,6 +151,7 @@ const ( itemOn itemGroupLeft itemGroupRight + itemBool keywordsEnd ) @@ -183,6 +184,7 @@ var key = map[string]itemType{ "on": itemOn, "group_left": itemGroupLeft, "group_right": itemGroupRight, + "bool": itemBool, } // These are the default string representations for common items. It does not diff --git a/promql/lex_test.go b/promql/lex_test.go index 455b2e5be..40c22de1a 100644 --- a/promql/lex_test.go +++ b/promql/lex_test.go @@ -264,6 +264,9 @@ var tests = []struct { }, { input: "group_right", expected: []item{{itemGroupRight, 0, "group_right"}}, + }, { + input: "bool", + expected: []item{{itemBool, 0, "bool"}}, }, // Test Selector. { diff --git a/promql/parse.go b/promql/parse.go index 354f6f8c4..283ecf723 100644 --- a/promql/parse.go +++ b/promql/parse.go @@ -485,6 +485,19 @@ func (p *parser) expr() Expr { vecMatching.Card = CardManyToMany } + returnBool := false + // Parse bool modifier. + if p.peek().typ == itemBool { + switch op { + case itemEQL, itemNEQ, itemLTE, itemLSS, itemGTE, itemGTR: + break + default: + p.errorf("bool modifier can only be used on comparison operators") + } + p.next() + returnBool = true + } + // Parse ON clause. if p.peek().typ == itemOn { p.next() @@ -523,6 +536,7 @@ func (p *parser) expr() Expr { LHS: lhs.RHS, RHS: rhs, VectorMatching: vecMatching, + ReturnBool: returnBool, }, VectorMatching: lhs.VectorMatching, } @@ -532,6 +546,7 @@ func (p *parser) expr() Expr { LHS: expr, RHS: rhs, VectorMatching: vecMatching, + ReturnBool: returnBool, } } } diff --git a/promql/parse_test.go b/promql/parse_test.go index e8205a83a..d2cfb7150 100644 --- a/promql/parse_test.go +++ b/promql/parse_test.go @@ -71,37 +71,40 @@ var testExpr = []struct { expected: &NumberLiteral{-493}, }, { input: "1 + 1", - expected: &BinaryExpr{itemADD, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemADD, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 - 1", - expected: &BinaryExpr{itemSUB, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemSUB, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 * 1", - expected: &BinaryExpr{itemMUL, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemMUL, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 % 1", - expected: &BinaryExpr{itemMOD, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemMOD, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 / 1", - expected: &BinaryExpr{itemDIV, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemDIV, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 == 1", - expected: &BinaryExpr{itemEQL, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemEQL, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 != 1", - expected: &BinaryExpr{itemNEQ, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemNEQ, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 > 1", - expected: &BinaryExpr{itemGTR, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemGTR, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 >= 1", - expected: &BinaryExpr{itemGTE, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemGTE, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 < 1", - expected: &BinaryExpr{itemLSS, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemLSS, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, }, { input: "1 <= 1", - expected: &BinaryExpr{itemLTE, &NumberLiteral{1}, &NumberLiteral{1}, nil}, + expected: &BinaryExpr{itemLTE, &NumberLiteral{1}, &NumberLiteral{1}, nil, false}, + }, { + input: "1 <= bool 1", + expected: &BinaryExpr{itemLTE, &NumberLiteral{1}, &NumberLiteral{1}, nil, true}, }, { input: "+1 + -2 * 1", expected: &BinaryExpr{ @@ -256,6 +259,19 @@ var testExpr = []struct { }, RHS: &NumberLiteral{1}, }, + }, { + input: "foo == bool 1", + expected: &BinaryExpr{ + Op: itemEQL, + LHS: &VectorSelector{ + Name: "foo", + LabelMatchers: metric.LabelMatchers{ + {Type: metric.Equal, Name: model.MetricNameLabel, Value: "foo"}, + }, + }, + RHS: &NumberLiteral{1}, + ReturnBool: true, + }, }, { input: "2.5 / bar", expected: &BinaryExpr{ @@ -513,6 +529,18 @@ var testExpr = []struct { input: `http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}`, fail: true, errMsg: "label \"instance\" must not occur in ON and INCLUDE clause at once", + }, { + input: "foo + bool bar", + fail: true, + errMsg: "bool modifier can only be used on comparison operators", + }, { + input: "foo + bool 10", + fail: true, + errMsg: "bool modifier can only be used on comparison operators", + }, { + input: "foo and bool 10", + fail: true, + errMsg: "bool modifier can only be used on comparison operators", }, // Test vector selector. { diff --git a/promql/testdata/comparison.test b/promql/testdata/comparison.test new file mode 100644 index 000000000..4ce49daa7 --- /dev/null +++ b/promql/testdata/comparison.test @@ -0,0 +1,53 @@ +load 5m + http_requests{job="api-server", instance="0", group="production"} 0+10x10 + http_requests{job="api-server", instance="1", group="production"} 0+20x10 + http_requests{job="api-server", instance="0", group="canary"} 0+30x10 + http_requests{job="api-server", instance="1", group="canary"} 0+40x10 + http_requests{job="app-server", instance="0", group="production"} 0+50x10 + http_requests{job="app-server", instance="1", group="production"} 0+60x10 + http_requests{job="app-server", instance="0", group="canary"} 0+70x10 + http_requests{job="app-server", instance="1", group="canary"} 0+80x10 + +eval instant at 50m SUM(http_requests) BY (job) > 1000 + {job="app-server"} 2600 + + +eval instant at 50m 1000 < SUM(http_requests) BY (job) + {job="app-server"} 1000 + + +eval instant at 50m SUM(http_requests) BY (job) <= 1000 + {job="api-server"} 1000 + + +eval instant at 50m SUM(http_requests) BY (job) != 1000 + {job="app-server"} 2600 + + +eval instant at 50m SUM(http_requests) BY (job) == 1000 + {job="api-server"} 1000 + +eval instant at 50m SUM(http_requests) BY (job) == bool 1000 + {job="api-server"} 1 + {job="app-server"} 0 + +eval instant at 50m SUM(http_requests) BY (job) == bool SUM(http_requests) BY (job) + {job="api-server"} 1 + {job="app-server"} 1 + +eval instant at 50m SUM(http_requests) BY (job) != bool SUM(http_requests) BY (job) + {job="api-server"} 0 + {job="app-server"} 0 + + +eval instant at 50m 0 == 1 + 0 + +eval instant at 50m 1 == 1 + 1 + +eval instant at 50m 0 == bool 1 + 0 + +eval instant at 50m 1 == bool 1 + 1 diff --git a/promql/testdata/legacy.test b/promql/testdata/legacy.test index 43c0da5fe..c6e3c1d21 100644 --- a/promql/testdata/legacy.test +++ b/promql/testdata/legacy.test @@ -108,26 +108,6 @@ eval instant at 50m SUM(http_requests) BY (job) / 0 {job="app-server"} +Inf -eval instant at 50m SUM(http_requests) BY (job) > 1000 - {job="app-server"} 2600 - - -eval instant at 50m 1000 < SUM(http_requests) BY (job) - {job="app-server"} 1000 - - -eval instant at 50m SUM(http_requests) BY (job) <= 1000 - {job="api-server"} 1000 - - -eval instant at 50m SUM(http_requests) BY (job) != 1000 - {job="app-server"} 2600 - - -eval instant at 50m SUM(http_requests) BY (job) == 1000 - {job="api-server"} 1000 - - eval instant at 50m SUM(http_requests) BY (job) + SUM(http_requests) BY (job) {job="api-server"} 2000 {job="app-server"} 5200