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.
// 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.

View File

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

View File

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

View File

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

View File

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

View File

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

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