Add OR operation and vector matching options.

This commits implements the OR operation between two vectors.
Vector matching using the ON clause is added to limit the set of
labels that define a match between two elements. Group modifiers
(GROUP_LEFT/GROUP_RIGHT) to request many-to-one matching are added.
This commit is contained in:
Fabian Reinartz 2015-03-03 10:58:28 +01:00
parent cce2f30a8b
commit 6f754073d5
8 changed files with 2070 additions and 1164 deletions

View file

@ -241,12 +241,24 @@ type (
// least one of the two operand Nodes must be a VectorNode. The other may be
// a VectorNode or ScalarNode. Both criteria are checked at runtime.
VectorArithExpr struct {
opType BinOpType
lhs Node
rhs Node
opType BinOpType
lhs Node
rhs Node
matchCardinality VectorMatchCardinality
matchOn clientmodel.LabelNames
includeLabels clientmodel.LabelNames
}
)
type VectorMatchCardinality int
const (
MatchOneToOne VectorMatchCardinality = iota
MatchManyToOne
MatchOneToMany
MatchManyToMany
)
// ----------------------------------------------------------------------------
// MatrixNode types.
@ -371,14 +383,21 @@ func (node *ScalarFunctionCall) Eval(timestamp clientmodel.Timestamp) clientmode
return node.function.callFn(timestamp, node.args).(clientmodel.SampleValue)
}
func (node *VectorAggregation) labelsToGroupingKey(labels clientmodel.Metric) uint64 {
summer := fnv.New64a()
for _, label := range node.groupBy {
summer.Write([]byte(labels[label]))
summer.Write([]byte{clientmodel.SeparatorByte})
// hashForLabels returns a hash value taken from the label/value pairs of the
// specified labels in the metric.
func hashForLabels(metric clientmodel.Metric, labels clientmodel.LabelNames) uint64 {
var result uint64
s := fnv.New64a()
for _, label := range labels {
s.Write([]byte(label))
s.Write([]byte{clientmodel.SeparatorByte})
s.Write([]byte(metric[label]))
result ^= s.Sum64()
s.Reset()
}
return summer.Sum64()
return result
}
// EvalVectorInstant evaluates a VectorNode with an instant query.
@ -484,7 +503,7 @@ func (node *VectorAggregation) Eval(timestamp clientmodel.Timestamp) Vector {
vector := node.vector.Eval(timestamp)
result := map[uint64]*groupedAggregation{}
for _, sample := range vector {
groupingKey := node.labelsToGroupingKey(sample.Metric.Metric)
groupingKey := hashForLabels(sample.Metric.Metric, node.groupBy)
if groupedResult, ok := result[groupingKey]; ok {
if node.keepExtraLabels {
groupedResult.labels = labelIntersection(groupedResult.labels, sample.Metric)
@ -729,10 +748,6 @@ func evalVectorBinop(opType BinOpType,
return lhs, true
}
return 0, false
case And:
return lhs, true
case Or:
return lhs, true // TODO: implement OR
}
panic("Not all enum values enumerated in switch")
}
@ -749,55 +764,214 @@ func labelsEqual(labels1, labels2 clientmodel.Metric) bool {
// Eval implements the VectorNode interface and returns the result of
// the expression.
func (node *VectorArithExpr) Eval(timestamp clientmodel.Timestamp) Vector {
result := Vector{}
if node.lhs.Type() == ScalarType && node.rhs.Type() == VectorType {
lhs := node.lhs.(ScalarNode).Eval(timestamp)
rhs := node.rhs.(VectorNode).Eval(timestamp)
for _, rhsSample := range rhs {
value, keep := evalVectorBinop(node.opType, lhs, rhsSample.Value)
if keep {
rhsSample.Value = value
if node.opType.shouldDropMetric() {
rhsSample.Metric.Delete(clientmodel.MetricNameLabel)
}
result = append(result, rhsSample)
}
}
return result
} else if node.lhs.Type() == VectorType && node.rhs.Type() == ScalarType {
lhs := node.lhs.(VectorNode).Eval(timestamp)
rhs := node.rhs.(ScalarNode).Eval(timestamp)
for _, lhsSample := range lhs {
value, keep := evalVectorBinop(node.opType, lhsSample.Value, rhs)
if keep {
lhsSample.Value = value
if node.opType.shouldDropMetric() {
lhsSample.Metric.Delete(clientmodel.MetricNameLabel)
}
result = append(result, lhsSample)
}
}
return result
} else if node.lhs.Type() == VectorType && node.rhs.Type() == VectorType {
// Calculate vector-to-vector operation.
if node.lhs.Type() == VectorType && node.rhs.Type() == VectorType {
lhs := node.lhs.(VectorNode).Eval(timestamp)
rhs := node.rhs.(VectorNode).Eval(timestamp)
for _, lhsSample := range lhs {
for _, rhsSample := range rhs {
if labelsEqual(lhsSample.Metric.Metric, rhsSample.Metric.Metric) {
value, keep := evalVectorBinop(node.opType, lhsSample.Value, rhsSample.Value)
if keep {
lhsSample.Value = value
if node.opType.shouldDropMetric() {
lhsSample.Metric.Delete(clientmodel.MetricNameLabel)
}
result = append(result, lhsSample)
}
}
}
}
return result
return node.evalVectors(timestamp, lhs, rhs)
}
panic("Invalid vector arithmetic expression operands")
// Calculate vector-to-scalar operation.
var lhs Vector
var rhs clientmodel.SampleValue
swap := false
if node.lhs.Type() == ScalarType && node.rhs.Type() == VectorType {
lhs = node.rhs.(VectorNode).Eval(timestamp)
rhs = node.lhs.(ScalarNode).Eval(timestamp)
swap = true
} else {
lhs = node.lhs.(VectorNode).Eval(timestamp)
rhs = node.rhs.(ScalarNode).Eval(timestamp)
}
result := make(Vector, 0, len(lhs))
for _, lhsSample := range lhs {
lv, rv := lhsSample.Value, rhs
// lhs always contains the vector. If the original position was different
// swap for calculating the value.
if swap {
lv, rv = rv, lv
}
value, keep := evalVectorBinop(node.opType, lv, rv)
if keep {
lhsSample.Value = value
if node.opType.shouldDropMetric() {
lhsSample.Metric.Delete(clientmodel.MetricNameLabel)
}
result = append(result, lhsSample)
}
}
return result
}
// evalVectors evaluates the binary operation for the given vectors.
func (node *VectorArithExpr) evalVectors(timestamp clientmodel.Timestamp, lhs, rhs Vector) Vector {
result := make(Vector, 0, len(rhs))
// The control flow below handles one-to-one or many-to-one matching.
// For one-to-many, swap sidedness and account for the swap when calculating
// values.
if node.matchCardinality == MatchOneToMany {
lhs, rhs = rhs, lhs
}
// All samples from the rhs hashed by the matching label/values.
rm := make(map[uint64]*Sample)
// Maps the hash of the label values used for matching to the hashes of the label
// values of the include labels (if any). It is used to keep track of already
// inserted samples.
added := make(map[uint64][]uint64)
// Add all rhs samples to a map so we can easily find matches later.
for _, rs := range rhs {
hash := node.hashForMetric(rs.Metric.Metric)
// The rhs is guaranteed to be the 'one' side. Having multiple samples
// with the same hash means that the matching is many-to-many,
// which is not supported.
if _, found := rm[hash]; node.matchCardinality != MatchManyToMany && found {
// Many-to-many matching not allowed.
// TODO(fabxc): Return a query error here once AST nodes support that.
return Vector{}
}
// In many-to-many matching the entry is simply overwritten. It can thus only
// be used to check whether any matching rhs entry exists but not retrieve them all.
rm[hash] = rs
}
// For all lhs samples find a respective rhs sample and perform
// the binary operation.
for _, ls := range lhs {
hash := node.hashForMetric(ls.Metric.Metric)
// Any lhs sample we encounter in an OR operation belongs to the result.
if node.opType == Or {
ls.Metric = node.resultMetric(ls, nil)
result = append(result, ls)
added[hash] = nil // Ensure matching rhs sample is not added later.
continue
}
rs, found := rm[hash] // Look for a match in the rhs vector.
if !found {
continue
}
var value clientmodel.SampleValue
var keep bool
if node.opType == And {
value = ls.Value
keep = true
} else {
if _, exists := added[hash]; node.matchCardinality == MatchOneToOne && exists {
// Many-to-one matching must be explicit.
// TODO(fabxc): Return a query error here once AST nodes support that.
return Vector{}
}
// Account for potentially swapped sidedness.
vl, vr := ls.Value, rs.Value
if node.matchCardinality == MatchOneToMany {
vl, vr = vr, vl
}
value, keep = evalVectorBinop(node.opType, vl, vr)
}
if keep {
metric := node.resultMetric(ls, rs)
// Check if the same label set has been added for a many-to-one matching before.
if node.matchCardinality == MatchManyToOne || node.matchCardinality == MatchOneToMany {
insHash := hashForLabels(metric.Metric, node.includeLabels)
if ihs, exists := added[hash]; exists {
for _, ih := range ihs {
if ih == insHash {
// TODO(fabxc): Return a query error here once AST nodes support that.
return Vector{}
}
}
added[hash] = append(ihs, insHash)
} else {
added[hash] = []uint64{insHash}
}
}
ns := &Sample{
Metric: metric,
Value: value,
Timestamp: timestamp,
}
result = append(result, ns)
added[hash] = added[hash] // Set existance to true.
}
}
// Add all remaining samples in the rhs in an OR operation if they
// have not been matched up with a lhs sample.
if node.opType == Or {
for hash, rs := range rm {
if _, exists := added[hash]; !exists {
rs.Metric = node.resultMetric(rs, nil)
result = append(result, rs)
}
}
}
return result
}
// resultMetric returns the metric for the given sample(s) based on the vector
// binary operation and the matching options. If a label that has to be included is set on
// both sides an error is returned.
func (node *VectorArithExpr) resultMetric(ls, rs *Sample) clientmodel.COWMetric {
if len(node.matchOn) == 0 || node.opType == Or || node.opType == And {
if node.opType.shouldDropMetric() {
ls.Metric.Delete(clientmodel.MetricNameLabel)
}
return ls.Metric
}
m := clientmodel.Metric{}
for _, ln := range node.matchOn {
m[ln] = ls.Metric.Metric[ln]
}
for _, ln := range node.includeLabels {
// Included labels from the `group_x` modifier are taken from the "many"-side.
v, ok := ls.Metric.Metric[ln]
if ok {
m[ln] = v
}
}
return clientmodel.COWMetric{false, m}
}
// hashForMetric calculates a hash value for the given metric based on the matching
// options for the binary operation.
func (node *VectorArithExpr) hashForMetric(metric clientmodel.Metric) uint64 {
var labels clientmodel.LabelNames
if len(node.matchOn) > 0 {
var match bool
for _, ln := range node.matchOn {
if _, match = metric[ln]; !match {
break
}
}
// If the metric does not contain the labels to match on, build the hash
// over the whole metric to give it a unique hash.
if !match {
labels = make(clientmodel.LabelNames, 0, len(metric))
for ln := range metric {
labels = append(labels, ln)
}
} else {
labels = node.matchOn
}
} else {
labels = make(clientmodel.LabelNames, 0, len(metric))
for ln := range metric {
if ln != clientmodel.MetricNameLabel {
labels = append(labels, ln)
}
}
}
return hashForLabels(metric, labels)
}
// Eval implements the MatrixNode interface and returns the value of
@ -962,7 +1136,7 @@ func nodesHaveTypes(nodes Nodes, exprTypes []ExprType) bool {
// NewArithExpr returns a (not yet evaluated) expression node (of type
// VectorArithExpr or ScalarArithExpr).
func NewArithExpr(opType BinOpType, lhs Node, rhs Node) (Node, error) {
func NewArithExpr(opType BinOpType, lhs Node, rhs Node, matchCard VectorMatchCardinality, matchOn, include clientmodel.LabelNames) (Node, error) {
if !nodesHaveTypes(Nodes{lhs, rhs}, []ExprType{ScalarType, VectorType}) {
return nil, errors.New("binary operands must be of vector or scalar type")
}
@ -971,13 +1145,25 @@ func NewArithExpr(opType BinOpType, lhs Node, rhs Node) (Node, error) {
if lhs.Type() == ScalarType || rhs.Type() == ScalarType {
return nil, errors.New("AND and OR operators may only be used between vectors")
}
// Logical operations must never be used with group modifiers.
if len(include) > 0 {
return nil, errors.New("AND and OR operators must not have a group modifier")
}
}
if lhs.Type() != VectorType || rhs.Type() != VectorType {
if matchCard != MatchOneToOne || matchOn != nil || include != nil {
return nil, errors.New("binary scalar expressions cannot have vector matching options")
}
}
if lhs.Type() == VectorType || rhs.Type() == VectorType {
return &VectorArithExpr{
opType: opType,
lhs: lhs,
rhs: rhs,
opType: opType,
lhs: lhs,
rhs: rhs,
matchCardinality: matchCard,
matchOn: matchOn,
includeLabels: include,
}, nil
}

View file

@ -82,8 +82,40 @@ func NewVectorAggregation(aggrTypeStr string, vector ast.Node, groupBy clientmod
return ast.NewVectorAggregation(aggrType, vector.(ast.VectorNode), groupBy, keepExtraLabels), nil
}
// vectorMatching combines data used to match samples between vectors.
type vectorMatching struct {
matchCardinality ast.VectorMatchCardinality
matchOn clientmodel.LabelNames
includeLabels clientmodel.LabelNames
}
// newVectorMatching is a convenience function to create a new vectorMatching.
func newVectorMatching(card string, matchOn, include clientmodel.LabelNames) (*vectorMatching, error) {
var matchCardinalities = map[string]ast.VectorMatchCardinality{
"": ast.MatchOneToOne,
"GROUP_LEFT": ast.MatchManyToOne,
"GROUP_RIGHT": ast.MatchOneToMany,
}
matchCard, ok := matchCardinalities[card]
if !ok {
return nil, fmt.Errorf("invalid vector match cardinality %q", card)
}
if matchCard != ast.MatchOneToOne && len(include) == 0 {
return nil, fmt.Errorf("grouped vector matching must provide labels")
}
// There must be no overlap between both labelname lists.
for _, matchLabel := range matchOn {
for _, incLabel := range include {
if matchLabel == incLabel {
return nil, fmt.Errorf("use of label %s in ON and %s clauses not allowed", incLabel, card)
}
}
}
return &vectorMatching{matchCard, matchOn, include}, nil
}
// NewArithExpr is a convenience function to create a new AST arithmetic expression.
func NewArithExpr(opTypeStr string, lhs ast.Node, rhs ast.Node) (ast.Node, error) {
func NewArithExpr(opTypeStr string, lhs ast.Node, rhs ast.Node, vecMatching *vectorMatching) (ast.Node, error) {
var opTypes = map[string]ast.BinOpType{
"+": ast.Add,
"-": ast.Sub,
@ -103,7 +135,15 @@ func NewArithExpr(opTypeStr string, lhs ast.Node, rhs ast.Node) (ast.Node, error
if !ok {
return nil, fmt.Errorf("invalid binary operator %q", opTypeStr)
}
expr, err := ast.NewArithExpr(opType, lhs, rhs)
var vm vectorMatching
if vecMatching != nil {
vm = *vecMatching
// And/or always do many-to-many matching.
if opType == ast.And || opType == ast.Or {
vm.matchCardinality = ast.MatchManyToMany
}
}
expr, err := ast.NewArithExpr(opType, lhs, rhs, vm.matchCardinality, vm.matchOn, vm.includeLabels)
if err != nil {
return nil, fmt.Errorf(err.Error())
}

View file

@ -423,6 +423,63 @@ var testMatrix = ast.Matrix{
},
Values: getTestValueStream(0, 90, 9, testStartTime),
},
{
Metric: clientmodel.COWMetric{
Metric: clientmodel.Metric{
clientmodel.MetricNameLabel: "vector_matching_a",
"l": "x",
},
},
Values: getTestValueStream(0, 100, 1, testStartTime),
},
{
Metric: clientmodel.COWMetric{
Metric: clientmodel.Metric{
clientmodel.MetricNameLabel: "vector_matching_a",
"l": "y",
},
},
Values: getTestValueStream(0, 100, 2, testStartTime),
},
{
Metric: clientmodel.COWMetric{
Metric: clientmodel.Metric{
clientmodel.MetricNameLabel: "vector_matching_b",
"l": "x",
},
},
Values: getTestValueStream(0, 100, 4, testStartTime),
},
{
Metric: clientmodel.COWMetric{
Metric: clientmodel.Metric{
clientmodel.MetricNameLabel: "cpu_count",
"instance": "0",
"type": "numa",
},
},
Values: getTestValueStream(0, 500, 30, testStartTime),
},
{
Metric: clientmodel.COWMetric{
Metric: clientmodel.Metric{
clientmodel.MetricNameLabel: "cpu_count",
"instance": "0",
"type": "smp",
},
},
Values: getTestValueStream(0, 200, 10, testStartTime),
},
{
Metric: clientmodel.COWMetric{
Metric: clientmodel.Metric{
clientmodel.MetricNameLabel: "cpu_count",
"instance": "1",
"type": "smp",
},
},
Values: getTestValueStream(0, 200, 20, testStartTime),
},
}
var testVector = getTestVectorFromTestMatrix(testMatrix)

View file

@ -84,6 +84,9 @@ DESCRIPTION|description return DESCRIPTION
PERMANENT|permanent return PERMANENT
BY|by return GROUP_OP
ON|on return MATCH_OP
GROUP_LEFT|GROUP_RIGHT lval.str = lexer.token(); return MATCH_MOD
group_left|group_right lval.str = strings.ToUpper(lexer.token()); return MATCH_MOD
KEEPING_EXTRA|keeping_extra return KEEPING_EXTRA
OFFSET|offset return OFFSET
AVG|SUM|MAX|MIN|COUNT lval.str = lexer.token(); return AGGR_OP

File diff suppressed because it is too large Load diff

View file

@ -32,6 +32,7 @@
labelSet clientmodel.LabelSet
labelMatcher *metric.LabelMatcher
labelMatchers metric.LabelMatchers
vectorMatching *vectorMatching
}
/* We simulate multiple start symbols for closely-related grammars via dummy tokens. See
@ -42,8 +43,8 @@
%token <str> IDENTIFIER STRING DURATION METRICNAME
%token <num> NUMBER
%token PERMANENT GROUP_OP KEEPING_EXTRA OFFSET
%token <str> AGGR_OP CMP_OP ADDITIVE_OP MULT_OP
%token PERMANENT GROUP_OP KEEPING_EXTRA OFFSET MATCH_OP
%token <str> AGGR_OP CMP_OP ADDITIVE_OP MULT_OP MATCH_MOD
%token ALERT IF FOR WITH SUMMARY DESCRIPTION
%type <ruleNodeSlice> func_arg_list
@ -51,6 +52,7 @@
%type <labelSet> label_assign label_assign_list rule_labels
%type <labelMatcher> label_match
%type <labelMatchers> label_match_list label_matches
%type <vectorMatching> vector_matching
%type <ruleNode> rule_expr func_arg
%type <boolean> qualifier extra_labels_opts
%type <str> for_duration metric_name label_match_type offset_opts
@ -207,34 +209,50 @@ rule_expr : '(' rule_expr ')'
}
/* Yacc can only attach associativity to terminals, so we
* have to list all operators here. */
| rule_expr ADDITIVE_OP rule_expr
| rule_expr ADDITIVE_OP vector_matching rule_expr
{
var err error
$$, err = NewArithExpr($2, $1, $3)
$$, err = NewArithExpr($2, $1, $4, $3)
if err != nil { yylex.Error(err.Error()); return 1 }
}
| rule_expr MULT_OP rule_expr
| rule_expr MULT_OP vector_matching rule_expr
{
var err error
$$, err = NewArithExpr($2, $1, $3)
$$, err = NewArithExpr($2, $1, $4, $3)
if err != nil { yylex.Error(err.Error()); return 1 }
}
| rule_expr CMP_OP rule_expr
| rule_expr CMP_OP vector_matching rule_expr
{
var err error
$$, err = NewArithExpr($2, $1, $3)
$$, err = NewArithExpr($2, $1, $4, $3)
if err != nil { yylex.Error(err.Error()); return 1 }
}
| NUMBER
{ $$ = ast.NewScalarLiteral($1)}
;
extra_labels_opts :
extra_labels_opts : /* empty */
{ $$ = false }
| KEEPING_EXTRA
{ $$ = true }
;
vector_matching : /* empty */
{ $$ = nil }
| MATCH_OP '(' label_list ')'
{
var err error
$$, err = newVectorMatching("", $3, nil)
if err != nil { yylex.Error(err.Error()); return 1 }
}
| MATCH_OP '(' label_list ')' MATCH_MOD '(' label_list ')'
{
var err error
$$, err = newVectorMatching($5, $3, $7)
if err != nil { yylex.Error(err.Error()); return 1 }
}
;
grouping_opts :
{ $$ = clientmodel.LabelNames{} }
| GROUP_OP '(' label_list ')'

View file

@ -23,6 +23,7 @@ type yySymType struct {
labelSet clientmodel.LabelSet
labelMatcher *metric.LabelMatcher
labelMatchers metric.LabelMatchers
vectorMatching *vectorMatching
}
const START_RULES = 57346
@ -36,16 +37,18 @@ const PERMANENT = 57353
const GROUP_OP = 57354
const KEEPING_EXTRA = 57355
const OFFSET = 57356
const AGGR_OP = 57357
const CMP_OP = 57358
const ADDITIVE_OP = 57359
const MULT_OP = 57360
const ALERT = 57361
const IF = 57362
const FOR = 57363
const WITH = 57364
const SUMMARY = 57365
const DESCRIPTION = 57366
const MATCH_OP = 57357
const AGGR_OP = 57358
const CMP_OP = 57359
const ADDITIVE_OP = 57360
const MULT_OP = 57361
const MATCH_MOD = 57362
const ALERT = 57363
const IF = 57364
const FOR = 57365
const WITH = 57366
const SUMMARY = 57367
const DESCRIPTION = 57368
var yyToknames = []string{
"START_RULES",
@ -59,10 +62,12 @@ var yyToknames = []string{
"GROUP_OP",
"KEEPING_EXTRA",
"OFFSET",
"MATCH_OP",
"AGGR_OP",
"CMP_OP",
"ADDITIVE_OP",
"MULT_OP",
"MATCH_MOD",
"ALERT",
"IF",
"FOR",
@ -77,7 +82,7 @@ const yyEofCode = 1
const yyErrCode = 2
const yyMaxDepth = 200
//line parser.y:261
//line parser.y:279
//line yacctab:1
var yyExca = []int{
@ -89,97 +94,101 @@ var yyExca = []int{
-2, 10,
}
const yyNprod = 52
const yyNprod = 55
const yyPrivate = 57344
var yyTokenNames []string
var yyStates []string
const yyLast = 142
const yyLast = 155
var yyAct = []int{
58, 76, 55, 52, 51, 30, 45, 6, 24, 20,
61, 22, 10, 53, 18, 13, 12, 84, 68, 83,
67, 11, 18, 36, 37, 38, 21, 19, 20, 21,
19, 20, 8, 54, 90, 7, 50, 21, 19, 20,
92, 18, 10, 53, 18, 13, 12, 70, 63, 62,
88, 11, 18, 57, 31, 21, 19, 20, 86, 87,
66, 40, 8, 10, 78, 7, 13, 12, 79, 69,
18, 29, 11, 80, 82, 81, 28, 85, 21, 19,
20, 19, 20, 8, 91, 77, 7, 41, 40, 94,
25, 23, 39, 18, 59, 18, 44, 98, 27, 73,
101, 99, 60, 96, 17, 43, 75, 9, 46, 56,
31, 47, 16, 33, 97, 102, 13, 65, 35, 48,
100, 95, 64, 32, 77, 93, 72, 25, 34, 2,
3, 14, 5, 4, 1, 42, 89, 15, 26, 74,
71, 49,
76, 59, 81, 56, 53, 52, 30, 46, 6, 24,
10, 54, 22, 13, 12, 21, 19, 20, 19, 20,
11, 62, 21, 19, 20, 20, 18, 90, 96, 111,
99, 18, 8, 18, 55, 7, 51, 60, 18, 18,
90, 63, 97, 65, 66, 21, 19, 20, 107, 75,
68, 67, 10, 54, 64, 13, 12, 21, 19, 20,
74, 18, 11, 31, 58, 85, 83, 90, 94, 89,
84, 27, 40, 18, 8, 28, 23, 7, 112, 86,
88, 87, 29, 91, 21, 19, 20, 82, 73, 10,
72, 98, 13, 12, 92, 93, 101, 71, 41, 11,
18, 42, 41, 25, 49, 106, 45, 78, 109, 108,
80, 8, 103, 36, 7, 61, 44, 17, 105, 37,
9, 47, 57, 31, 104, 33, 48, 16, 13, 70,
35, 113, 110, 102, 38, 39, 32, 69, 77, 82,
100, 25, 34, 2, 3, 14, 5, 4, 1, 43,
95, 15, 26, 79, 50,
}
var yyPact = []int{
125, -1000, -1000, 57, 93, -1000, 21, 57, 121, 72,
47, 42, -1000, -1000, -1000, 107, 122, -1000, 110, 57,
57, 57, 62, 60, -1000, 80, 94, 84, 6, 57,
96, 24, 68, -1000, 82, -22, -9, -17, 64, -1000,
121, 94, 115, -1000, -1000, -1000, 109, -1000, 33, -10,
-1000, -1000, 21, -1000, 39, 18, -1000, 120, 74, 79,
57, 94, -1000, -1000, -1000, -1000, -1000, -1000, 36, 98,
57, -11, -1000, 57, 31, -1000, -1000, 25, 13, -1000,
-1000, 96, 10, -1000, 119, 21, -1000, 118, 114, 81,
106, -1000, -1000, -1000, -1000, -1000, 68, -1000, 78, 113,
76, 108, -1000,
139, -1000, -1000, 83, 106, -1000, 67, 83, 135, 43,
44, 51, -1000, -1000, -1000, 119, 136, -1000, 122, 104,
104, 104, 40, 72, -1000, 89, 107, 97, 4, 83,
109, 33, 9, -1000, 93, -13, 83, 23, 83, 83,
-1000, 135, 107, 130, -1000, -1000, -1000, 121, -1000, 68,
58, -1000, -1000, 67, -1000, 28, 18, -1000, 132, 80,
81, 83, 107, 6, 132, -7, 0, -1000, -1000, -1000,
-1000, -1000, -1000, 46, 111, 83, 37, -1000, 83, 65,
-1000, -1000, 41, 5, -1000, 10, -1000, 109, -2, -1000,
134, 67, -1000, 133, 126, 88, 116, 98, -1000, -1000,
-1000, -1000, -1000, 9, -1000, 17, 84, 132, 125, -3,
52, -1000, 124, -1000,
}
var yyPgo = []int{
0, 141, 140, 5, 1, 139, 0, 8, 91, 138,
3, 4, 137, 2, 136, 107, 135, 6, 134, 133,
132, 131,
0, 154, 0, 6, 2, 153, 1, 9, 76, 152,
113, 4, 5, 151, 3, 150, 120, 149, 7, 148,
147, 146, 145,
}
var yyR1 = []int{
0, 18, 18, 19, 19, 20, 21, 21, 14, 14,
12, 12, 15, 15, 6, 6, 6, 5, 5, 4,
9, 9, 9, 8, 8, 7, 16, 16, 17, 17,
10, 10, 10, 10, 10, 10, 10, 10, 10, 10,
10, 10, 13, 13, 3, 3, 2, 2, 1, 1,
11, 11,
0, 19, 19, 20, 20, 21, 22, 22, 15, 15,
13, 13, 16, 16, 6, 6, 6, 5, 5, 4,
9, 9, 9, 8, 8, 7, 17, 17, 18, 18,
11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
11, 11, 14, 14, 10, 10, 10, 3, 3, 2,
2, 1, 1, 12, 12,
}
var yyR2 = []int{
0, 2, 2, 0, 2, 1, 5, 11, 0, 2,
0, 1, 1, 1, 0, 3, 2, 1, 3, 3,
0, 2, 3, 1, 3, 3, 1, 1, 0, 2,
3, 4, 3, 4, 3, 5, 6, 6, 3, 3,
3, 1, 0, 1, 0, 4, 1, 3, 1, 3,
1, 1,
3, 4, 3, 4, 3, 5, 6, 6, 4, 4,
4, 1, 0, 1, 0, 4, 8, 0, 4, 1,
3, 1, 3, 1, 1,
}
var yyChk = []int{
-1000, -18, 4, 5, -19, -20, -10, 29, 26, -15,
6, 15, 10, 9, -21, -12, 19, 11, 31, 17,
18, 16, -10, -8, -7, 6, -9, 26, 29, 29,
-3, 12, -15, 6, 6, 8, -10, -10, -10, 30,
28, 27, -16, 25, 16, -17, 14, 27, -8, -1,
30, -11, -10, 7, -10, -13, 13, 29, -6, 26,
20, 32, -7, -17, 7, 8, 27, 30, 28, 30,
29, -2, 6, 25, -5, 27, -4, 6, -10, -17,
-11, -3, -10, 30, 28, -10, 27, 28, 25, -14,
21, -13, 30, 6, -4, 7, 22, 8, -6, 23,
7, 24, 7,
-1000, -19, 4, 5, -20, -21, -11, 31, 28, -16,
6, 16, 10, 9, -22, -13, 21, 11, 33, 18,
19, 17, -11, -8, -7, 6, -9, 28, 31, 31,
-3, 12, -16, 6, 6, 8, -10, 15, -10, -10,
32, 30, 29, -17, 27, 17, -18, 14, 29, -8,
-1, 32, -12, -11, 7, -11, -14, 13, 31, -6,
28, 22, 34, -11, 31, -11, -11, -7, -18, 7,
8, 29, 32, 30, 32, 31, -2, 6, 27, -5,
29, -4, 6, -11, -18, -2, -12, -3, -11, 32,
30, -11, 29, 30, 27, -15, 23, 32, -14, 32,
6, -4, 7, 24, 8, 20, -6, 31, 25, -2,
7, 32, 26, 7,
}
var yyDef = []int{
0, -2, 3, 0, -2, 2, 5, 0, 0, 20,
13, 44, 41, 12, 4, 0, 0, 11, 0, 0,
0, 0, 0, 0, 23, 0, 28, 0, 0, 0,
42, 0, 14, 13, 0, 0, 38, 39, 40, 30,
0, 28, 0, 26, 27, 32, 0, 21, 0, 0,
34, 48, 50, 51, 0, 0, 43, 0, 0, 0,
0, 28, 24, 31, 25, 29, 22, 33, 0, 44,
0, 0, 46, 0, 0, 16, 17, 0, 8, 35,
49, 42, 0, 45, 0, 6, 15, 0, 0, 0,
0, 36, 37, 47, 18, 19, 14, 9, 0, 0,
0, 0, 7,
13, 47, 41, 12, 4, 0, 0, 11, 0, 44,
44, 44, 0, 0, 23, 0, 28, 0, 0, 0,
42, 0, 14, 13, 0, 0, 0, 0, 0, 0,
30, 0, 28, 0, 26, 27, 32, 0, 21, 0,
0, 34, 51, 53, 54, 0, 0, 43, 0, 0,
0, 0, 28, 38, 0, 39, 40, 24, 31, 25,
29, 22, 33, 0, 47, 0, 0, 49, 0, 0,
16, 17, 0, 8, 35, 0, 52, 42, 0, 48,
0, 6, 15, 0, 0, 0, 0, 45, 36, 37,
50, 18, 19, 14, 9, 0, 0, 0, 0, 0,
0, 46, 0, 7,
}
var yyTok1 = []int{
@ -187,21 +196,21 @@ var yyTok1 = []int{
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
29, 30, 3, 3, 28, 3, 3, 3, 3, 3,
31, 32, 3, 3, 30, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 25, 3, 3, 3, 3, 3, 3, 3, 3,
3, 27, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 31, 3, 32, 3, 3, 3, 3, 3, 3,
3, 33, 3, 34, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 26, 3, 27,
3, 3, 3, 28, 3, 29,
}
var yyTok2 = []int{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24,
22, 23, 24, 25, 26,
}
var yyTok3 = []int{
0,
@ -433,12 +442,12 @@ yydefault:
switch yynt {
case 5:
//line parser.y:74
//line parser.y:76
{
yylex.(*RulesLexer).parsedExpr = yyS[yypt-0].ruleNode
}
case 6:
//line parser.y:79
//line parser.y:81
{
rule, err := CreateRecordingRule(yyS[yypt-3].str, yyS[yypt-2].labelSet, yyS[yypt-0].ruleNode, yyS[yypt-4].boolean)
if err != nil {
@ -448,7 +457,7 @@ yydefault:
yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule)
}
case 7:
//line parser.y:85
//line parser.y:87
{
rule, err := CreateAlertingRule(yyS[yypt-9].str, yyS[yypt-7].ruleNode, yyS[yypt-6].str, yyS[yypt-4].labelSet, yyS[yypt-2].str, yyS[yypt-0].str)
if err != nil {
@ -458,94 +467,94 @@ yydefault:
yylex.(*RulesLexer).parsedRules = append(yylex.(*RulesLexer).parsedRules, rule)
}
case 8:
//line parser.y:93
//line parser.y:95
{
yyVAL.str = "0s"
}
case 9:
//line parser.y:95
//line parser.y:97
{
yyVAL.str = yyS[yypt-0].str
}
case 10:
//line parser.y:99
//line parser.y:101
{
yyVAL.boolean = false
}
case 11:
//line parser.y:101
//line parser.y:103
{
yyVAL.boolean = true
}
case 12:
//line parser.y:105
{
yyVAL.str = yyS[yypt-0].str
}
case 13:
//line parser.y:107
{
yyVAL.str = yyS[yypt-0].str
}
case 13:
//line parser.y:109
{
yyVAL.str = yyS[yypt-0].str
}
case 14:
//line parser.y:111
//line parser.y:113
{
yyVAL.labelSet = clientmodel.LabelSet{}
}
case 15:
//line parser.y:113
//line parser.y:115
{
yyVAL.labelSet = yyS[yypt-1].labelSet
}
case 16:
//line parser.y:115
//line parser.y:117
{
yyVAL.labelSet = clientmodel.LabelSet{}
}
case 17:
//line parser.y:118
//line parser.y:120
{
yyVAL.labelSet = yyS[yypt-0].labelSet
}
case 18:
//line parser.y:120
//line parser.y:122
{
for k, v := range yyS[yypt-0].labelSet {
yyVAL.labelSet[k] = v
}
}
case 19:
//line parser.y:124
//line parser.y:126
{
yyVAL.labelSet = clientmodel.LabelSet{clientmodel.LabelName(yyS[yypt-2].str): clientmodel.LabelValue(yyS[yypt-0].str)}
}
case 20:
//line parser.y:128
{
yyVAL.labelMatchers = metric.LabelMatchers{}
}
case 21:
//line parser.y:130
{
yyVAL.labelMatchers = metric.LabelMatchers{}
}
case 22:
case 21:
//line parser.y:132
{
yyVAL.labelMatchers = metric.LabelMatchers{}
}
case 22:
//line parser.y:134
{
yyVAL.labelMatchers = yyS[yypt-1].labelMatchers
}
case 23:
//line parser.y:136
//line parser.y:138
{
yyVAL.labelMatchers = metric.LabelMatchers{yyS[yypt-0].labelMatcher}
}
case 24:
//line parser.y:138
//line parser.y:140
{
yyVAL.labelMatchers = append(yyVAL.labelMatchers, yyS[yypt-0].labelMatcher)
}
case 25:
//line parser.y:142
//line parser.y:144
{
var err error
yyVAL.labelMatcher, err = newLabelMatcher(yyS[yypt-1].str, clientmodel.LabelName(yyS[yypt-2].str), clientmodel.LabelValue(yyS[yypt-0].str))
@ -555,32 +564,32 @@ yydefault:
}
}
case 26:
//line parser.y:150
//line parser.y:152
{
yyVAL.str = "="
}
case 27:
//line parser.y:152
//line parser.y:154
{
yyVAL.str = yyS[yypt-0].str
}
case 28:
//line parser.y:156
//line parser.y:158
{
yyVAL.str = "0s"
}
case 29:
//line parser.y:158
//line parser.y:160
{
yyVAL.str = yyS[yypt-0].str
}
case 30:
//line parser.y:162
//line parser.y:164
{
yyVAL.ruleNode = yyS[yypt-1].ruleNode
}
case 31:
//line parser.y:164
//line parser.y:166
{
var err error
yyVAL.ruleNode, err = NewVectorSelector(yyS[yypt-2].labelMatchers, yyS[yypt-0].str)
@ -590,7 +599,7 @@ yydefault:
}
}
case 32:
//line parser.y:170
//line parser.y:172
{
var err error
m, err := metric.NewLabelMatcher(metric.Equal, clientmodel.MetricNameLabel, clientmodel.LabelValue(yyS[yypt-2].str))
@ -606,7 +615,7 @@ yydefault:
}
}
case 33:
//line parser.y:179
//line parser.y:181
{
var err error
yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-3].str, yyS[yypt-1].ruleNodeSlice)
@ -616,7 +625,7 @@ yydefault:
}
}
case 34:
//line parser.y:185
//line parser.y:187
{
var err error
yyVAL.ruleNode, err = NewFunctionCall(yyS[yypt-2].str, []ast.Node{})
@ -626,7 +635,7 @@ yydefault:
}
}
case 35:
//line parser.y:191
//line parser.y:193
{
var err error
yyVAL.ruleNode, err = NewMatrixSelector(yyS[yypt-4].ruleNode, yyS[yypt-2].str, yyS[yypt-0].str)
@ -636,7 +645,7 @@ yydefault:
}
}
case 36:
//line parser.y:197
//line parser.y:199
{
var err error
yyVAL.ruleNode, err = NewVectorAggregation(yyS[yypt-5].str, yyS[yypt-3].ruleNode, yyS[yypt-1].labelNameSlice, yyS[yypt-0].boolean)
@ -646,7 +655,7 @@ yydefault:
}
}
case 37:
//line parser.y:203
//line parser.y:205
{
var err error
yyVAL.ruleNode, err = NewVectorAggregation(yyS[yypt-5].str, yyS[yypt-1].ruleNode, yyS[yypt-4].labelNameSlice, yyS[yypt-3].boolean)
@ -656,87 +665,112 @@ yydefault:
}
}
case 38:
//line parser.y:211
//line parser.y:213
{
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-2].str, yyS[yypt-3].ruleNode, yyS[yypt-0].ruleNode, yyS[yypt-1].vectorMatching)
if err != nil {
yylex.Error(err.Error())
return 1
}
}
case 39:
//line parser.y:217
//line parser.y:219
{
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-2].str, yyS[yypt-3].ruleNode, yyS[yypt-0].ruleNode, yyS[yypt-1].vectorMatching)
if err != nil {
yylex.Error(err.Error())
return 1
}
}
case 40:
//line parser.y:223
//line parser.y:225
{
var err error
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-1].str, yyS[yypt-2].ruleNode, yyS[yypt-0].ruleNode)
yyVAL.ruleNode, err = NewArithExpr(yyS[yypt-2].str, yyS[yypt-3].ruleNode, yyS[yypt-0].ruleNode, yyS[yypt-1].vectorMatching)
if err != nil {
yylex.Error(err.Error())
return 1
}
}
case 41:
//line parser.y:229
//line parser.y:231
{
yyVAL.ruleNode = ast.NewScalarLiteral(yyS[yypt-0].num)
}
case 42:
//line parser.y:233
//line parser.y:235
{
yyVAL.boolean = false
}
case 43:
//line parser.y:235
//line parser.y:237
{
yyVAL.boolean = true
}
case 44:
//line parser.y:239
//line parser.y:241
{
yyVAL.vectorMatching = nil
}
case 45:
//line parser.y:243
{
var err error
yyVAL.vectorMatching, err = newVectorMatching("", yyS[yypt-1].labelNameSlice, nil)
if err != nil {
yylex.Error(err.Error())
return 1
}
}
case 46:
//line parser.y:249
{
var err error
yyVAL.vectorMatching, err = newVectorMatching(yyS[yypt-3].str, yyS[yypt-5].labelNameSlice, yyS[yypt-1].labelNameSlice)
if err != nil {
yylex.Error(err.Error())
return 1
}
}
case 47:
//line parser.y:257
{
yyVAL.labelNameSlice = clientmodel.LabelNames{}
}
case 45:
//line parser.y:241
case 48:
//line parser.y:259
{
yyVAL.labelNameSlice = yyS[yypt-1].labelNameSlice
}
case 46:
//line parser.y:245
case 49:
//line parser.y:263
{
yyVAL.labelNameSlice = clientmodel.LabelNames{clientmodel.LabelName(yyS[yypt-0].str)}
}
case 47:
//line parser.y:247
case 50:
//line parser.y:265
{
yyVAL.labelNameSlice = append(yyVAL.labelNameSlice, clientmodel.LabelName(yyS[yypt-0].str))
}
case 48:
//line parser.y:251
case 51:
//line parser.y:269
{
yyVAL.ruleNodeSlice = []ast.Node{yyS[yypt-0].ruleNode}
}
case 49:
//line parser.y:253
case 52:
//line parser.y:271
{
yyVAL.ruleNodeSlice = append(yyVAL.ruleNodeSlice, yyS[yypt-0].ruleNode)
}
case 50:
//line parser.y:257
case 53:
//line parser.y:275
{
yyVAL.ruleNode = yyS[yypt-0].ruleNode
}
case 51:
//line parser.y:259
case 54:
//line parser.y:277
{
yyVAL.ruleNode = ast.NewStringLiteral(yyS[yypt-0].str)
}

View file

@ -630,6 +630,12 @@ func TestExpressions(t *testing.T) {
`request_duration_seconds_bucket{instance="ins2", job="job2", le="0.1"} => 40 @[%v]`,
`request_duration_seconds_bucket{instance="ins2", job="job2", le="0.2"} => 70 @[%v]`,
`request_duration_seconds_bucket{instance="ins2", job="job2", le="+Inf"} => 90 @[%v]`,
`vector_matching_a{l="x"} => 10 @[%v]`,
`vector_matching_a{l="y"} => 20 @[%v]`,
`vector_matching_b{l="x"} => 40 @[%v]`,
`cpu_count{instance="1", type="smp"} => 200 @[%v]`,
`cpu_count{instance="0", type="smp"} => 100 @[%v]`,
`cpu_count{instance="0", type="numa"} => 300 @[%v]`,
},
},
{
@ -666,6 +672,191 @@ func TestExpressions(t *testing.T) {
`{job="api-server"} => 1000 @[%v]`,
},
},
{
expr: `http_requests{group="canary"} and http_requests{instance="0"}`,
output: []string{
`http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`,
`http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`,
},
},
{
expr: `(http_requests{group="canary"} + 1) and http_requests{instance="0"}`,
output: []string{
`{group="canary", instance="0", job="api-server"} => 301 @[%v]`,
`{group="canary", instance="0", job="app-server"} => 701 @[%v]`,
},
},
{
expr: `(http_requests{group="canary"} + 1) and on(instance, job) http_requests{instance="0", group="production"}`,
output: []string{
`{group="canary", instance="0", job="api-server"} => 301 @[%v]`,
`{group="canary", instance="0", job="app-server"} => 701 @[%v]`,
},
},
{
expr: `(http_requests{group="canary"} + 1) and on(instance) http_requests{instance="0", group="production"}`,
output: []string{
`{group="canary", instance="0", job="api-server"} => 301 @[%v]`,
`{group="canary", instance="0", job="app-server"} => 701 @[%v]`,
},
},
{
expr: `http_requests{group="canary"} or http_requests{group="production"}`,
output: []string{
`http_requests{group="canary", instance="0", job="api-server"} => 300 @[%v]`,
`http_requests{group="canary", instance="0", job="app-server"} => 700 @[%v]`,
`http_requests{group="canary", instance="1", job="api-server"} => 400 @[%v]`,
`http_requests{group="canary", instance="1", job="app-server"} => 800 @[%v]`,
`http_requests{group="production", instance="0", job="api-server"} => 100 @[%v]`,
`http_requests{group="production", instance="0", job="app-server"} => 500 @[%v]`,
`http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`,
`http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`,
},
},
{
// On overlap the rhs samples must be dropped.
expr: `(http_requests{group="canary"} + 1) or http_requests{instance="1"}`,
output: []string{
`{group="canary", instance="0", job="api-server"} => 301 @[%v]`,
`{group="canary", instance="0", job="app-server"} => 701 @[%v]`,
`{group="canary", instance="1", job="api-server"} => 401 @[%v]`,
`{group="canary", instance="1", job="app-server"} => 801 @[%v]`,
`http_requests{group="production", instance="1", job="api-server"} => 200 @[%v]`,
`http_requests{group="production", instance="1", job="app-server"} => 600 @[%v]`,
},
},
{
// Matching only on instance excludes everything that has instance=0/1 but includes
// entries without the instance label.
expr: `(http_requests{group="canary"} + 1) or on(instance) (http_requests or cpu_count or vector_matching_a)`,
output: []string{
`{group="canary", instance="0", job="api-server"} => 301 @[%v]`,
`{group="canary", instance="0", job="app-server"} => 701 @[%v]`,
`{group="canary", instance="1", job="api-server"} => 401 @[%v]`,
`{group="canary", instance="1", job="app-server"} => 801 @[%v]`,
`vector_matching_a{l="x"} => 10 @[%v]`,
`vector_matching_a{l="y"} => 20 @[%v]`,
},
},
{
expr: `http_requests{group="canary"} / on(instance,job) http_requests{group="production"}`,
output: []string{
`{instance="0", job="api-server"} => 3 @[%v]`,
`{instance="0", job="app-server"} => 1.4 @[%v]`,
`{instance="1", job="api-server"} => 2 @[%v]`,
`{instance="1", job="app-server"} => 1.3333333333333333 @[%v]`,
},
},
{
// Include labels must guarantee uniquely identifiable time series.
expr: `http_requests{group="production"} / on(instance) group_left(group) cpu_count{type="smp"}`,
output: []string{}, // Empty result returned on error (see TODOs).
},
{
// Many-to-many matching is not allowed.
expr: `http_requests{group="production"} / on(instance) group_left(job,type) cpu_count`,
output: []string{}, // Empty result returned on error (see TODOs).
},
{
// Many-to-one matching must be explicit.
expr: `http_requests{group="production"} / on(instance) cpu_count{type="smp"}`,
output: []string{}, // Empty result returned on error (see TODOs).
},
{
expr: `http_requests{group="production"} / on(instance) group_left(job) cpu_count{type="smp"}`,
output: []string{
`{instance="1", job="api-server"} => 1 @[%v]`,
`{instance="0", job="app-server"} => 5 @[%v]`,
`{instance="1", job="app-server"} => 3 @[%v]`,
`{instance="0", job="api-server"} => 1 @[%v]`,
},
},
{
// Ensure sidedness of grouping preserves operand sides.
expr: `cpu_count{type="smp"} / on(instance) group_right(job) http_requests{group="production"}`,
output: []string{
`{instance="1", job="app-server"} => 0.3333333333333333 @[%v]`,
`{instance="0", job="app-server"} => 0.2 @[%v]`,
`{instance="1", job="api-server"} => 1 @[%v]`,
`{instance="0", job="api-server"} => 1 @[%v]`,
},
},
{
// Include labels from both sides.
expr: `http_requests{group="production"} / on(instance) group_left(job) cpu_count{type="smp"}`,
output: []string{
`{instance="1", job="api-server"} => 1 @[%v]`,
`{instance="0", job="app-server"} => 5 @[%v]`,
`{instance="1", job="app-server"} => 3 @[%v]`,
`{instance="0", job="api-server"} => 1 @[%v]`,
},
},
{
expr: `http_requests{group="production"} < on(instance,job) http_requests{group="canary"}`,
output: []string{
`{instance="1", job="app-server"} => 600 @[%v]`,
`{instance="0", job="app-server"} => 500 @[%v]`,
`{instance="1", job="api-server"} => 200 @[%v]`,
`{instance="0", job="api-server"} => 100 @[%v]`,
},
},
{
expr: `http_requests{group="production"} > on(instance,job) http_requests{group="canary"}`,
output: []string{},
},
{
expr: `http_requests{group="production"} == on(instance,job) http_requests{group="canary"}`,
output: []string{},
},
{
expr: `http_requests > on(instance) group_left(group,job) cpu_count{type="smp"}`,
output: []string{
`{group="canary", instance="0", job="app-server"} => 700 @[%v]`,
`{group="canary", instance="1", job="app-server"} => 800 @[%v]`,
`{group="canary", instance="0", job="api-server"} => 300 @[%v]`,
`{group="canary", instance="1", job="api-server"} => 400 @[%v]`,
`{group="production", instance="0", job="app-server"} => 500 @[%v]`,
`{group="production", instance="1", job="app-server"} => 600 @[%v]`,
},
},
{
expr: `http_requests / on(instance) 3`,
shouldFail: true,
},
{
expr: `3 / on(instance) http_requests_total`,
shouldFail: true,
},
{
expr: `3 / on(instance) 3`,
shouldFail: true,
},
{
// Missing label list for grouping mod.
expr: `http_requests{group="production"} / on(instance) group_left cpu_count{type="smp"}`,
shouldFail: true,
},
{
// No group mod allowed for logical operations.
expr: `http_requests{group="production"} or on(instance) group_left(type) cpu_count{type="smp"}`,
shouldFail: true,
},
{
// No group mod allowed for logical operations.
expr: `http_requests{group="production"} and on(instance) group_left(type) cpu_count{type="smp"}`,
shouldFail: true,
},
{
// No duplicate use of label.
expr: `http_requests{group="production"} + on(instance) group_left(job,instance) cpu_count{type="smp"}`,
shouldFail: true,
},
{
expr: `{l="x"} + on(__name__) {l="y"}`,
output: []string{
`vector_matching_a => 30 @[%v]`,
},
},
{
expr: `absent(nonexistent)`,
output: []string{
@ -975,10 +1166,14 @@ func TestExpressions(t *testing.T) {
t.Errorf("%d. Test should fail, but didn't", i)
}
failed := false
resultStr := ast.EvalToString(testExpr, testEvalTime, ast.Text, storage, stats.NewTimerGroup())
resultLines := strings.Split(resultStr, "\n")
if len(exprTest.output) != len(resultLines) {
if len(exprTest.output) == 0 && strings.Trim(resultStr, "\n") == "" {
// expected and received empty vector, everything is fine
continue
} else if len(exprTest.output) != len(resultLines) {
t.Errorf("%d. Number of samples in expected and actual output don't match", i)
failed = true
}