The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/pkg/expr/classic/classic_test.go

751 lines
18 KiB

package classic
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/util"
)
func TestConditionsCmd(t *testing.T) {
tests := []struct {
name string
cmd *ConditionsCmd
vars mathexp.Vars
expected func() mathexp.Results
}{{
// This test asserts that a single query with condition returns 0 and no matches as the condition
// is not met
name: "single query with condition when condition is not met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(0.0))
v.SetMeta([]EvalMatch{})
return newResults(v)
},
}, {
// This test asserts that a single query with condition returns 1 and the average in the meta as
// the condition is met
name: "single query with condition when condition is met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("avg"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(3.0)}})
return newResults(v)
},
}, {
name: "single query with ranged condition when condition is not met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("diff"),
Operator: "and",
Evaluator: &rangedEvaluator{Type: "within_range", Lower: 2, Upper: 4},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(0.0))
v.SetMeta([]EvalMatch{})
return newResults(v)
},
}, {
name: "single query with ranged condition when condition is met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("diff"),
Operator: "and",
Evaluator: &rangedEvaluator{Type: "within_range", Lower: 0, Upper: 10},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(4.0)}})
return newResults(v)
},
}, {
name: "single no data query with condition is No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{mathexp.NoData{}.New()},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(nil)
v.SetMeta([]EvalMatch{{Metric: "NoData"}})
return newResults(v)
},
}, {
name: "single no values query with condition is No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(nil)
v.SetMeta([]EvalMatch{{Metric: "NoData"}})
return newResults(v)
},
}, {
name: "single series no points query with condition returns No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(nil),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(nil)
v.SetMeta([]EvalMatch{{Metric: "NoData"}})
return newResults(v)
},
}, {
name: "single no data query with condition is met has no value",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{mathexp.NoData{}.New()},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &noValueEvaluator{},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: nil}})
return newResults(v)
},
}, {
name: "single no values query with condition is met has no value",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &noValueEvaluator{},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: nil}})
return newResults(v)
},
}, {
name: "single series no points query with condition is met has no value",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(nil),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &noValueEvaluator{},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: nil}})
return newResults(v)
},
}, {
// This test asserts that a single query with condition returns 1 and the average of the second
// series in the meta because while the first series is No Data the second series contains valid points
name: "single query with condition returns average when one series is no data and the other contains valid points",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(),
newSeries(util.Pointer(2.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 1},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(2.0)}})
return newResults(v)
},
}, {
name: "single query with condition and no series matches condition",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
newSeries(util.Pointer(2.0), util.Pointer(10.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 15},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(0.0))
v.SetMeta([]EvalMatch{})
return mathexp.Results{Values: mathexp.Values{v}}
},
}, {
name: "single query with condition and one of two series matches condition",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
newSeriesWithLabels(data.Labels{"foo": "bar"}, util.Pointer(2.0), util.Pointer(10.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 1},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(2.0), Labels: data.Labels{"foo": "bar"}}})
return newResults(v)
},
}, {
name: "single query with condition and both series matches condition",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
newSeriesWithLabels(data.Labels{"foo": "bar"}, util.Pointer(2.0), util.Pointer(10.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 0},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{
Value: util.Pointer(1.0),
}, {
Value: util.Pointer(2.0),
Labels: data.Labels{"foo": "bar"},
}})
return newResults(v)
},
}, {
name: "single query with two conditions where left hand side is met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("max"),
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2},
},
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 1},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(5.0)}})
return newResults(v)
},
}, {
name: "single query with two conditions where right hand side is met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("max"),
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 10},
},
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 0},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(1.0)}})
return newResults(v)
},
}, {
name: "single query with two conditions where both are met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("max"),
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2},
},
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 0},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(5.0)}, {Value: util.Pointer(1.0)}})
return newResults(v)
},
}, {
name: "single instant query with condition where condition is met",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newNumber(util.Pointer(5.0)),
newNumber(util.Pointer(10.0)),
newNumber(util.Pointer(15.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("avg"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{
{Value: util.Pointer(5.0)},
{Value: util.Pointer(10.0)},
{Value: util.Pointer(15.0)},
})
return newResults(v)
},
}, {
name: "two queries with two conditions using and operator and first is No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{mathexp.NoData{}.New()},
},
"B": mathexp.Results{
Values: []mathexp.Value{newSeries(util.Pointer(5.0))},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
{
InputRefID: "B",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(0.0))
v.SetMeta([]EvalMatch{{Metric: "NoData"}, {Value: util.Pointer(5.0)}})
return newResults(v)
},
}, {
name: "two queries with two conditions using and operator and last is No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{newSeries(util.Pointer(5.0))},
},
"B": mathexp.Results{
Values: []mathexp.Value{mathexp.NoData{}.New()},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
{
InputRefID: "B",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(0.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(5.0)}, {Metric: "NoData"}})
return newResults(v)
},
}, {
name: "two queries with two conditions using or operator and first is No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{mathexp.NoData{}.New()},
},
"B": mathexp.Results{
Values: []mathexp.Value{newSeries(util.Pointer(5.0))},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{"gt", 1},
},
{
InputRefID: "B",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(nil)
v.SetMeta([]EvalMatch{{Metric: "NoData"}, {Value: util.Pointer(5.0)}})
return newResults(v)
},
}, {
name: "two queries with two conditions using or operator and last is No Data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{newSeries(util.Pointer(5.0))},
},
"B": mathexp.Results{
Values: []mathexp.Value{mathexp.NoData{}.New()},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{"gt", 1},
},
{
InputRefID: "B",
Reducer: reducer("min"),
Operator: "or",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
expected: func() mathexp.Results {
v := newNumber(nil)
v.SetMeta([]EvalMatch{{Value: util.Pointer(5.0)}, {Metric: "NoData"}})
return newResults(v)
},
}, {
name: "LogicOr will stop subsequent logic checks in condition: true AND true LogicOr false AND false",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
newSeries(util.Pointer(1.0), util.Pointer(5.0)),
},
},
},
cmd: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("max"),
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2},
},
{
InputRefID: "A",
Reducer: reducer("min"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 0},
},
{
InputRefID: "A",
Reducer: reducer("avg"),
Operator: "logic-or",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 6},
},
{
InputRefID: "A",
Reducer: reducer("last"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 6},
},
}},
expected: func() mathexp.Results {
v := newNumber(util.Pointer(1.0))
v.SetMeta([]EvalMatch{{Value: util.Pointer(5.0)}, {Value: util.Pointer(1.0)}})
return newResults(v)
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := tt.cmd.Execute(context.Background(), time.Now(), tt.vars, tracing.InitializeTracerForTest(), nil)
require.NoError(t, err)
require.Equal(t, tt.expected(), res)
})
}
}
func TestUnmarshalConditionsCmd(t *testing.T) {
var tests = []struct {
name string
rawJSON string
expectedCommand *ConditionsCmd
needsVars []string
}{
{
name: "basic threshold condition",
rawJSON: `{
"conditions": [
{
"evaluator": {
"params": [
2
],
"type": "gt"
},
"operator": {
"type": "and"
},
"query": {
"params": [
"A"
]
},
"reducer": {
"params": [],
"type": "avg"
},
"type": "query"
}
]
}`,
expectedCommand: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("avg"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2},
},
},
},
needsVars: []string{"A"},
},
{
name: "ranged condition",
rawJSON: `{
"conditions": [
{
"evaluator": {
"params": [
2,
3
],
"type": "within_range"
},
"operator": {
"type": "or"
},
"query": {
"params": [
"A"
]
},
"reducer": {
"params": [],
"type": "diff"
},
"type": "query"
}
]
}`,
expectedCommand: &ConditionsCmd{
Conditions: []condition{
{
InputRefID: "A",
Reducer: reducer("diff"),
Operator: "or",
Evaluator: &rangedEvaluator{Type: "within_range", Lower: 2, Upper: 3},
},
},
},
needsVars: []string{"A"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var rq map[string]any
err := json.Unmarshal([]byte(tt.rawJSON), &rq)
require.NoError(t, err)
cmd, err := UnmarshalConditionsCmd(rq, "")
require.NoError(t, err)
require.Equal(t, tt.expectedCommand, cmd)
require.Equal(t, tt.needsVars, cmd.NeedsVars())
})
}
}