Implement relative complement set operator "unless"

The `unless` set operator can be used to return all vector elements from
the LHS which do not match the elements on the RHS. A use case is to
return all metrics for nodes which do not have a specific role:

    node_load1 unless on(instance) chef_role{role="app"}
This commit is contained in:
Tobias Schmidt 2016-04-02 18:52:18 -04:00
parent 4c3dc25e35
commit 8cc86f25c0
6 changed files with 169 additions and 26 deletions

View File

@ -629,6 +629,8 @@ func (ev *evaluator) eval(expr Expr) model.Value {
return ev.vectorAnd(lhs.(vector), rhs.(vector), e.VectorMatching) return ev.vectorAnd(lhs.(vector), rhs.(vector), e.VectorMatching)
case itemLOR: case itemLOR:
return ev.vectorOr(lhs.(vector), rhs.(vector), e.VectorMatching) return ev.vectorOr(lhs.(vector), rhs.(vector), e.VectorMatching)
case itemLUnless:
return ev.vectorUnless(lhs.(vector), rhs.(vector), e.VectorMatching)
default: default:
return ev.vectorBinop(e.Op, lhs.(vector), rhs.(vector), e.VectorMatching, e.ReturnBool) return ev.vectorBinop(e.Op, lhs.(vector), rhs.(vector), e.VectorMatching, e.ReturnBool)
} }
@ -724,9 +726,8 @@ func (ev *evaluator) matrixSelector(node *MatrixSelector) matrix {
func (ev *evaluator) vectorAnd(lhs, rhs vector, matching *VectorMatching) vector { func (ev *evaluator) vectorAnd(lhs, rhs vector, matching *VectorMatching) vector {
if matching.Card != CardManyToMany { if matching.Card != CardManyToMany {
panic("logical operations must always be many-to-many matching") panic("set operations must only use many-to-many matching")
} }
// If no matching labels are specified, match by all labels.
sigf := signatureFunc(matching.On...) sigf := signatureFunc(matching.On...)
var result vector var result vector
@ -748,7 +749,7 @@ func (ev *evaluator) vectorAnd(lhs, rhs vector, matching *VectorMatching) vector
func (ev *evaluator) vectorOr(lhs, rhs vector, matching *VectorMatching) vector { func (ev *evaluator) vectorOr(lhs, rhs vector, matching *VectorMatching) vector {
if matching.Card != CardManyToMany { if matching.Card != CardManyToMany {
panic("logical operations must always be many-to-many matching") panic("set operations must only use many-to-many matching")
} }
sigf := signatureFunc(matching.On...) sigf := signatureFunc(matching.On...)
@ -768,10 +769,30 @@ func (ev *evaluator) vectorOr(lhs, rhs vector, matching *VectorMatching) vector
return result return result
} }
// vectorBinop evaluates a binary operation between two vector, excluding AND and OR. func (ev *evaluator) vectorUnless(lhs, rhs vector, matching *VectorMatching) vector {
if matching.Card != CardManyToMany {
panic("set operations must only use many-to-many matching")
}
sigf := signatureFunc(matching.On...)
rightSigs := map[uint64]struct{}{}
for _, rs := range rhs {
rightSigs[sigf(rs.Metric)] = struct{}{}
}
var result vector
for _, ls := range lhs {
if _, ok := rightSigs[sigf(ls.Metric)]; !ok {
result = append(result, ls)
}
}
return result
}
// vectorBinop evaluates a binary operation between two vectors, excluding set operators.
func (ev *evaluator) vectorBinop(op itemType, lhs, rhs vector, matching *VectorMatching, returnBool bool) 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 set operators")
} }
var ( var (
result = vector{} result = vector{}

View File

@ -48,7 +48,7 @@ func (i item) String() string {
return fmt.Sprintf("%q", i.val) return fmt.Sprintf("%q", i.val)
} }
// isOperator returns true if the item corresponds to a logical or arithmetic operator. // isOperator returns true if the item corresponds to a arithmetic or set operator.
// Returns false otherwise. // Returns false otherwise.
func (i itemType) isOperator() bool { return i > operatorsStart && i < operatorsEnd } func (i itemType) isOperator() bool { return i > operatorsStart && i < operatorsEnd }
@ -71,6 +71,15 @@ func (i itemType) isComparisonOperator() bool {
} }
} }
// isSetOperator returns whether the item corresponds to a set operator.
func (i itemType) isSetOperator() bool {
switch i {
case itemLAND, itemLOR, itemLUnless:
return true
}
return false
}
// Constants for operator precedence in expressions. // Constants for operator precedence in expressions.
// //
const LowestPrec = 0 // Non-operators. const LowestPrec = 0 // Non-operators.
@ -82,7 +91,7 @@ func (i itemType) precedence() int {
switch i { switch i {
case itemLOR: case itemLOR:
return 1 return 1
case itemLAND: case itemLAND, itemLUnless:
return 2 return 2
case itemEQL, itemNEQ, itemLTE, itemLSS, itemGTE, itemGTR: case itemEQL, itemNEQ, itemLTE, itemLSS, itemGTE, itemGTR:
return 3 return 3
@ -127,6 +136,7 @@ const (
itemDIV itemDIV
itemLAND itemLAND
itemLOR itemLOR
itemLUnless
itemEQL itemEQL
itemNEQ itemNEQ
itemLTE itemLTE
@ -168,8 +178,9 @@ const (
var key = map[string]itemType{ var key = map[string]itemType{
// Operators. // Operators.
"and": itemLAND, "and": itemLAND,
"or": itemLOR, "or": itemLOR,
"unless": itemLUnless,
// Aggregators. // Aggregators.
"sum": itemSum, "sum": itemSum,

View File

@ -217,6 +217,9 @@ var tests = []struct {
}, { }, {
input: `or`, input: `or`,
expected: []item{{itemLOR, 0, `or`}}, expected: []item{{itemLOR, 0, `or`}},
}, {
input: `unless`,
expected: []item{{itemLUnless, 0, `unless`}},
}, },
// Test aggregators. // Test aggregators.
{ {

View File

@ -447,7 +447,7 @@ func (p *parser) expr() Expr {
vecMatching := &VectorMatching{ vecMatching := &VectorMatching{
Card: CardOneToOne, Card: CardOneToOne,
} }
if op == itemLAND || op == itemLOR { if op.isSetOperator() {
vecMatching.Card = CardManyToMany vecMatching.Card = CardManyToMany
} }
@ -1042,7 +1042,7 @@ func (p *parser) checkType(node Node) (typ model.ValueType) {
rt := p.checkType(n.RHS) rt := p.checkType(n.RHS)
if !n.Op.isOperator() { if !n.Op.isOperator() {
p.errorf("only logical and arithmetic operators allowed in binary expression, got %q", n.Op) p.errorf("binary expression does not support operator %q", n.Op)
} }
if (lt != model.ValScalar && lt != model.ValVector) || (rt != model.ValScalar && rt != model.ValVector) { if (lt != model.ValScalar && lt != model.ValVector) || (rt != model.ValScalar && rt != model.ValVector) {
p.errorf("binary expression must contain only scalar and vector types") p.errorf("binary expression must contain only scalar and vector types")
@ -1055,18 +1055,18 @@ func (p *parser) checkType(node Node) (typ model.ValueType) {
n.VectorMatching = nil n.VectorMatching = nil
} else { } else {
// Both operands are vectors. // Both operands are vectors.
if n.Op == itemLAND || n.Op == itemLOR { if n.Op.isSetOperator() {
if n.VectorMatching.Card == CardOneToMany || n.VectorMatching.Card == CardManyToOne { if n.VectorMatching.Card == CardOneToMany || n.VectorMatching.Card == CardManyToOne {
p.errorf("no grouping allowed for AND and OR operations") p.errorf("no grouping allowed for %q operation", n.Op)
} }
if n.VectorMatching.Card != CardManyToMany { if n.VectorMatching.Card != CardManyToMany {
p.errorf("AND and OR operations must always be many-to-many") p.errorf("set operations must always be many-to-many")
} }
} }
} }
if (lt == model.ValScalar || rt == model.ValScalar) && (n.Op == itemLAND || n.Op == itemLOR) { if (lt == model.ValScalar || rt == model.ValScalar) && n.Op.isSetOperator() {
p.errorf("AND and OR not allowed in binary scalar expression") p.errorf("set operator %q not allowed in binary scalar expression", n.Op)
} }
case *Call: case *Call:

View File

@ -213,7 +213,7 @@ var testExpr = []struct {
}, { }, {
input: "1 and 1", input: "1 and 1",
fail: true, fail: true,
errMsg: "AND and OR not allowed in binary scalar expression", errMsg: "set operator \"and\" not allowed in binary scalar expression",
}, { }, {
input: "1 == 1", input: "1 == 1",
fail: true, fail: true,
@ -221,7 +221,11 @@ var testExpr = []struct {
}, { }, {
input: "1 or 1", input: "1 or 1",
fail: true, fail: true,
errMsg: "AND and OR not allowed in binary scalar expression", errMsg: "set operator \"or\" not allowed in binary scalar expression",
}, {
input: "1 unless 1",
fail: true,
errMsg: "set operator \"unless\" not allowed in binary scalar expression",
}, { }, {
input: "1 !~ 1", input: "1 !~ 1",
fail: true, fail: true,
@ -339,6 +343,24 @@ var testExpr = []struct {
}, },
VectorMatching: &VectorMatching{Card: CardManyToMany}, VectorMatching: &VectorMatching{Card: CardManyToMany},
}, },
}, {
input: "foo unless bar",
expected: &BinaryExpr{
Op: itemLUnless,
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},
},
}, { }, {
// Test and/or precedence and reassigning of operands. // Test and/or precedence and reassigning of operands.
input: "foo + bar or bla and blub", input: "foo + bar or bla and blub",
@ -378,6 +400,45 @@ var testExpr = []struct {
}, },
VectorMatching: &VectorMatching{Card: CardManyToMany}, VectorMatching: &VectorMatching{Card: CardManyToMany},
}, },
}, {
// Test and/or/unless precedence.
input: "foo and bar unless baz or qux",
expected: &BinaryExpr{
Op: itemLOR,
LHS: &BinaryExpr{
Op: itemLUnless,
LHS: &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},
},
RHS: &VectorSelector{
Name: "baz",
LabelMatchers: metric.LabelMatchers{
{Type: metric.Equal, Name: model.MetricNameLabel, Value: "baz"},
},
},
VectorMatching: &VectorMatching{Card: CardManyToMany},
},
RHS: &VectorSelector{
Name: "qux",
LabelMatchers: metric.LabelMatchers{
{Type: metric.Equal, Name: model.MetricNameLabel, Value: "qux"},
},
},
VectorMatching: &VectorMatching{Card: CardManyToMany},
},
}, { }, {
// Test precedence and reassigning of operands. // Test precedence and reassigning of operands.
input: "bar + on(foo) bla / on(baz, buz) group_right(test) blub", input: "bar + on(foo) bla / on(baz, buz) group_right(test) blub",
@ -456,6 +517,27 @@ var testExpr = []struct {
On: model.LabelNames{"test", "blub"}, On: model.LabelNames{"test", "blub"},
}, },
}, },
}, {
input: "foo unless on(bar) baz",
expected: &BinaryExpr{
Op: itemLUnless,
LHS: &VectorSelector{
Name: "foo",
LabelMatchers: metric.LabelMatchers{
{Type: metric.Equal, Name: model.MetricNameLabel, Value: "foo"},
},
},
RHS: &VectorSelector{
Name: "baz",
LabelMatchers: metric.LabelMatchers{
{Type: metric.Equal, Name: model.MetricNameLabel, Value: "baz"},
},
},
VectorMatching: &VectorMatching{
Card: CardManyToMany,
On: model.LabelNames{"bar"},
},
},
}, { }, {
input: "foo / on(test,blub) group_left(bar) bar", input: "foo / on(test,blub) group_left(bar) bar",
expected: &BinaryExpr{ expected: &BinaryExpr{
@ -503,19 +585,27 @@ var testExpr = []struct {
}, { }, {
input: "foo and 1", input: "foo and 1",
fail: true, fail: true,
errMsg: "AND and OR not allowed in binary scalar expression", errMsg: "set operator \"and\" not allowed in binary scalar expression",
}, { }, {
input: "1 and foo", input: "1 and foo",
fail: true, fail: true,
errMsg: "AND and OR not allowed in binary scalar expression", errMsg: "set operator \"and\" not allowed in binary scalar expression",
}, { }, {
input: "foo or 1", input: "foo or 1",
fail: true, fail: true,
errMsg: "AND and OR not allowed in binary scalar expression", errMsg: "set operator \"or\" not allowed in binary scalar expression",
}, { }, {
input: "1 or foo", input: "1 or foo",
fail: true, fail: true,
errMsg: "AND and OR not allowed in binary scalar expression", errMsg: "set operator \"or\" not allowed in binary scalar expression",
}, {
input: "foo unless 1",
fail: true,
errMsg: "set operator \"unless\" not allowed in binary scalar expression",
}, {
input: "1 unless foo",
fail: true,
errMsg: "set operator \"unless\" not allowed in binary scalar expression",
}, { }, {
input: "1 or on(bar) foo", input: "1 or on(bar) foo",
fail: true, fail: true,
@ -527,19 +617,27 @@ var testExpr = []struct {
}, { }, {
input: "foo and on(bar) group_left(baz) bar", input: "foo and on(bar) group_left(baz) bar",
fail: true, fail: true,
errMsg: "no grouping allowed for AND and OR operations", errMsg: "no grouping allowed for \"and\" operation",
}, { }, {
input: "foo and on(bar) group_right(baz) bar", input: "foo and on(bar) group_right(baz) bar",
fail: true, fail: true,
errMsg: "no grouping allowed for AND and OR operations", errMsg: "no grouping allowed for \"and\" operation",
}, { }, {
input: "foo or on(bar) group_left(baz) bar", input: "foo or on(bar) group_left(baz) bar",
fail: true, fail: true,
errMsg: "no grouping allowed for AND and OR operations", errMsg: "no grouping allowed for \"or\" operation",
}, { }, {
input: "foo or on(bar) group_right(baz) bar", input: "foo or on(bar) group_right(baz) bar",
fail: true, fail: true,
errMsg: "no grouping allowed for AND and OR operations", errMsg: "no grouping allowed for \"or\" operation",
}, {
input: "foo unless on(bar) group_left(baz) bar",
fail: true,
errMsg: "no grouping allowed for \"unless\" operation",
}, {
input: "foo unless on(bar) group_right(baz) bar",
fail: true,
errMsg: "no grouping allowed for \"unless\" operation",
}, { }, {
input: `http_requests{group="production"} / on(instance) group_left cpu_count{type="smp"}`, input: `http_requests{group="production"} / on(instance) group_left cpu_count{type="smp"}`,
fail: true, fail: true,

View File

@ -109,6 +109,16 @@ eval instant at 50m (http_requests{group="canary"} + 1) or on(instance) (http_re
vector_matching_a{l="x"} 10 vector_matching_a{l="x"} 10
vector_matching_a{l="y"} 20 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
eval instant at 50m http_requests{group="canary"} unless on(job) http_requests{instance="0"}
eval instant at 50m http_requests{group="canary"} unless on(job, instance) 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"} / on(instance,job) http_requests{group="production"} eval instant at 50m http_requests{group="canary"} / on(instance,job) http_requests{group="production"}
{instance="0", job="api-server"} 3 {instance="0", job="api-server"} 3
{instance="0", job="app-server"} 1.4 {instance="0", job="app-server"} 1.4