promql: Add 'bool' modifier to comparison functions

When doing comparison operations on vectors, filtering
sometimes gets in the way and you have to go to a fair bit of
effort to workaround it in order to always return a result.
The 'bool' modifier instead of filtering returns 0/1 depending
on the result of the compairson.

This is also a prerequisite to removing plain scalar/scalar comparisons,
as it maintains the current behaviour under a new syntax.
This commit is contained in:
Brian Brazil 2015-09-02 14:51:44 +01:00
parent 6664b77f36
commit 29e8dc2c49
8 changed files with 135 additions and 37 deletions

View file

@ -117,6 +117,9 @@ type BinaryExpr struct {
// The matching behavior for the operation if both operands are vectors. // The matching behavior for the operation if both operands are vectors.
// If they are not this field is nil. // If they are not this field is nil.
VectorMatching *VectorMatching VectorMatching *VectorMatching
// If a comparison operator, return 0/1 rather than filtering.
ReturnBool bool
} }
// Call represents a function call. // Call represents a function call.

View file

@ -639,13 +639,13 @@ func (ev *evaluator) eval(expr Expr) model.Value {
case itemLOR: case itemLOR:
return ev.vectorOr(lhs.(vector), rhs.(vector), e.VectorMatching) return ev.vectorOr(lhs.(vector), rhs.(vector), e.VectorMatching)
default: 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: 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: 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: 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. // 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 { if matching.Card == CardManyToMany {
panic("many-to-many only allowed for AND and OR") 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 vl, vr = vr, vl
} }
value, keep := vectorElemBinop(op, vl, vr) value, keep := vectorElemBinop(op, vl, vr)
if !keep { if returnBool {
if keep {
value = 1.0
} else {
value = 0.0
}
} else if !keep {
continue continue
} }
metric := resultMetric(ls.Metric, op, resultLabels...) 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. // 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)) vec := make(vector, 0, len(lhs))
for _, lhsSample := range lhs { for _, lhsSample := range lhs {
@ -932,6 +938,14 @@ func (ev *evaluator) vectorScalarBinop(op itemType, lhs vector, rhs *model.Scala
lv, rv = rv, lv lv, rv = rv, lv
} }
value, keep := vectorElemBinop(op, lv, rv) value, keep := vectorElemBinop(op, lv, rv)
if returnBool {
if keep {
value = 1.0
} else {
value = 0.0
}
keep = true
}
if keep { if keep {
lhsSample.Value = value lhsSample.Value = value
if shouldDropMetricName(op) { if shouldDropMetricName(op) {

View file

@ -151,6 +151,7 @@ const (
itemOn itemOn
itemGroupLeft itemGroupLeft
itemGroupRight itemGroupRight
itemBool
keywordsEnd keywordsEnd
) )
@ -183,6 +184,7 @@ var key = map[string]itemType{
"on": itemOn, "on": itemOn,
"group_left": itemGroupLeft, "group_left": itemGroupLeft,
"group_right": itemGroupRight, "group_right": itemGroupRight,
"bool": itemBool,
} }
// These are the default string representations for common items. It does not // These are the default string representations for common items. It does not

View file

@ -264,6 +264,9 @@ var tests = []struct {
}, { }, {
input: "group_right", input: "group_right",
expected: []item{{itemGroupRight, 0, "group_right"}}, expected: []item{{itemGroupRight, 0, "group_right"}},
}, {
input: "bool",
expected: []item{{itemBool, 0, "bool"}},
}, },
// Test Selector. // Test Selector.
{ {

View file

@ -485,6 +485,19 @@ func (p *parser) expr() Expr {
vecMatching.Card = CardManyToMany 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. // Parse ON clause.
if p.peek().typ == itemOn { if p.peek().typ == itemOn {
p.next() p.next()
@ -523,6 +536,7 @@ func (p *parser) expr() Expr {
LHS: lhs.RHS, LHS: lhs.RHS,
RHS: rhs, RHS: rhs,
VectorMatching: vecMatching, VectorMatching: vecMatching,
ReturnBool: returnBool,
}, },
VectorMatching: lhs.VectorMatching, VectorMatching: lhs.VectorMatching,
} }
@ -532,6 +546,7 @@ func (p *parser) expr() Expr {
LHS: expr, LHS: expr,
RHS: rhs, RHS: rhs,
VectorMatching: vecMatching, VectorMatching: vecMatching,
ReturnBool: returnBool,
} }
} }
} }

View file

@ -71,37 +71,40 @@ var testExpr = []struct {
expected: &NumberLiteral{-493}, expected: &NumberLiteral{-493},
}, { }, {
input: "1 + 1", input: "1 + 1",
expected: &BinaryExpr{itemADD, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemADD, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 - 1", input: "1 - 1",
expected: &BinaryExpr{itemSUB, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemSUB, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 * 1", input: "1 * 1",
expected: &BinaryExpr{itemMUL, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemMUL, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 % 1", input: "1 % 1",
expected: &BinaryExpr{itemMOD, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemMOD, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 / 1", input: "1 / 1",
expected: &BinaryExpr{itemDIV, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemDIV, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 == 1", input: "1 == 1",
expected: &BinaryExpr{itemEQL, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemEQL, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 != 1", input: "1 != 1",
expected: &BinaryExpr{itemNEQ, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemNEQ, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 > 1", input: "1 > 1",
expected: &BinaryExpr{itemGTR, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemGTR, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 >= 1", input: "1 >= 1",
expected: &BinaryExpr{itemGTE, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemGTE, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 < 1", input: "1 < 1",
expected: &BinaryExpr{itemLSS, &NumberLiteral{1}, &NumberLiteral{1}, nil}, expected: &BinaryExpr{itemLSS, &NumberLiteral{1}, &NumberLiteral{1}, nil, false},
}, { }, {
input: "1 <= 1", 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", input: "+1 + -2 * 1",
expected: &BinaryExpr{ expected: &BinaryExpr{
@ -256,6 +259,19 @@ var testExpr = []struct {
}, },
RHS: &NumberLiteral{1}, 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", input: "2.5 / bar",
expected: &BinaryExpr{ expected: &BinaryExpr{
@ -513,6 +529,18 @@ var testExpr = []struct {
input: `http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}`, input: `http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}`,
fail: true, fail: true,
errMsg: "label \"instance\" must not occur in ON and INCLUDE clause at once", 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. // Test vector selector.
{ {

53
promql/testdata/comparison.test vendored Normal file
View file

@ -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

View file

@ -108,26 +108,6 @@ eval instant at 50m SUM(http_requests) BY (job) / 0
{job="app-server"} +Inf {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) eval instant at 50m SUM(http_requests) BY (job) + SUM(http_requests) BY (job)
{job="api-server"} 2000 {job="api-server"} 2000
{job="app-server"} 5200 {job="app-server"} 5200