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:
parent
6664b77f36
commit
29e8dc2c49
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
{
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue