SSE: Add "Classic Condition" on backend (#31511)

This is a translation of services/alerting/conditions. Main Changes:

- Work with types in SSE (series/number) which are data frames (no more null.Float).
- The query part from alerting/conditions is handled by SSE logic
- Convey / simplejson removed.
- Time range no longer part of "query" in the condition
pull/31614/head
Kyle Brandt 4 years ago committed by GitHub
parent 8d39e6640c
commit a488ab8393
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      go.mod
  2. 4
      go.sum
  3. 160
      pkg/expr/classic/classic.go
  4. 199
      pkg/expr/classic/classic_test.go
  5. 98
      pkg/expr/classic/evaluator.go
  6. 143
      pkg/expr/classic/evaluator_test.go
  7. 205
      pkg/expr/classic/reduce.go
  8. 420
      pkg/expr/classic/reduce_test.go
  9. 6
      pkg/expr/commands.go
  10. 3
      pkg/expr/nodes.go

@ -67,6 +67,7 @@ require (
github.com/prometheus/client_golang v1.9.0
github.com/prometheus/client_model v0.2.0
github.com/prometheus/common v0.18.0
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 // indirect
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/robfig/cron/v3 v3.0.1
github.com/russellhaering/goxmldsig v1.1.0

@ -1226,6 +1226,10 @@ github.com/prometheus/prometheus v1.8.2-0.20200819132913-cb830b0a9c78/go.mod h1:
github.com/prometheus/prometheus v1.8.2-0.20200923143134-7e2db3d092f3/go.mod h1:9VNWoDFHOMovlubld5uKKxfCDcPBj2GMOCjcUFXkYaM=
github.com/prometheus/prometheus v1.8.2-0.20201014093524-73e2ce1bd643 h1:BDAexvKlOVjE5A8MlqRxzwkEpPl1/v6ydU1/J7kJtZc=
github.com/prometheus/prometheus v1.8.2-0.20201014093524-73e2ce1bd643/go.mod h1:XYjkJiog7fyQu3puQNivZPI2pNq1C/775EIoHfDvuvY=
github.com/quasilyte/go-ruleguard v0.3.1 h1:2KTXnHBCR4BUl8UAL2bCUorOBGC8RsmYncuDA9NEFW4=
github.com/quasilyte/go-ruleguard/dsl v0.3.1 h1:CHGOKP2LDz35P49TjW4Bx4BCfFI6ZZU/8zcneECD0q4=
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY=
github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM=
github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=

@ -0,0 +1,160 @@
package classic
import (
"context"
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/expr/mathexp"
)
// ConditionsCmd is command for the classic conditions
// expression operation.
type ConditionsCmd struct {
Conditions []condition
refID string
}
// classicConditionJSON is the JSON model for a single condition.
// It is based on services/alerting/conditions/query.go's newQueryCondition().
type classicConditionJSON struct {
Evaluator conditionEvalJSON `json:"evaluator"`
Operator struct {
Type string `json:"type"`
} `json:"operator"`
Query struct {
Params []string
} `json:"query"`
Reducer struct {
// Params []interface{} `json:"params"` (Unused)
Type string `json:"type"`
} `json:"reducer"`
}
type conditionEvalJSON struct {
Params []float64 `json:"params"`
Type string `json:"type"` // e.g. "gt"
}
// condition is a single condition within the ConditionsCmd.
type condition struct {
QueryRefID string
Reducer classicReducer
Evaluator evaluator
Operator string
}
type classicReducer string
// NeedsVars returns the variable names (refIds) that are dependencies
// to execute the command and allows the command to fulfill the Command interface.
func (ccc *ConditionsCmd) NeedsVars() []string {
vars := []string{}
for _, c := range ccc.Conditions {
vars = append(vars, c.QueryRefID)
}
return vars
}
// Execute runs the command and returns the results or an error if the command
// failed to execute.
func (ccc *ConditionsCmd) Execute(ctx context.Context, vars mathexp.Vars) (mathexp.Results, error) {
firing := true
newRes := mathexp.Results{}
noDataFound := true
for i, c := range ccc.Conditions {
querySeriesSet := vars[c.QueryRefID]
for _, val := range querySeriesSet.Values {
series, ok := val.(mathexp.Series)
if !ok {
return newRes, fmt.Errorf("can only reduce type series, got type %v", val.Type())
}
reducedNum := c.Reducer.Reduce(series)
// TODO handle error / no data signals
thisCondNoDataFound := reducedNum.GetFloat64Value() == nil
evalRes := c.Evaluator.Eval(reducedNum)
if i == 0 {
firing = evalRes
noDataFound = thisCondNoDataFound
}
if c.Operator == "or" {
firing = firing || evalRes
noDataFound = noDataFound || thisCondNoDataFound
} else {
firing = firing && evalRes
noDataFound = noDataFound && thisCondNoDataFound
}
}
}
num := mathexp.NewNumber("", nil)
var v float64
switch {
case noDataFound:
num.SetValue(nil)
case firing:
v = 1
num.SetValue(&v)
case !firing:
num.SetValue(&v)
}
newRes.Values = append(newRes.Values, num)
return newRes, nil
}
// UnmarshalConditionsCmd creates a new ConditionsCmd.
func UnmarshalConditionsCmd(rawQuery map[string]interface{}, refID string) (*ConditionsCmd, error) {
jsonFromM, err := json.Marshal(rawQuery["conditions"])
if err != nil {
return nil, fmt.Errorf("failed to remarshal classic condition body: %w", err)
}
var ccj []classicConditionJSON
if err = json.Unmarshal(jsonFromM, &ccj); err != nil {
return nil, fmt.Errorf("failed to unmarshal remarshaled classic condition body: %w", err)
}
c := &ConditionsCmd{
refID: refID,
}
for i, cj := range ccj {
cond := condition{}
if cj.Operator.Type != "and" && cj.Operator.Type != "or" {
return nil, fmt.Errorf("classic condition %v operator must be `and` or `or`", i+1)
}
cond.Operator = cj.Operator.Type
if len(cj.Query.Params) == 0 || cj.Query.Params[0] == "" {
return nil, fmt.Errorf("classic condition %v is missing the query refID argument", i+1)
}
cond.QueryRefID = cj.Query.Params[0]
cond.Reducer = classicReducer(cj.Reducer.Type)
if !cond.Reducer.ValidReduceFunc() {
return nil, fmt.Errorf("reducer '%v' in condition %v is not a valid reducer", cond.Reducer, i+1)
}
cond.Evaluator, err = newAlertEvaluator(cj.Evaluator)
if err != nil {
return nil, err
}
c.Conditions = append(c.Conditions, cond)
}
return c, nil
}

@ -0,0 +1,199 @@
package classic
import (
"context"
"encoding/json"
"testing"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/stretchr/testify/require"
ptr "github.com/xorcare/pointer"
)
func TestUnmarshalConditionCMD(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{
{
QueryRefID: "A",
Reducer: classicReducer("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{
{
QueryRefID: "A",
Reducer: classicReducer("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]interface{}
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())
})
}
}
func TestConditionsCmdExecute(t *testing.T) {
trueNumber := valBasedNumber(ptr.Float64(1))
falseNumber := valBasedNumber(ptr.Float64(0))
noDataNumber := valBasedNumber(nil)
tests := []struct {
name string
vars mathexp.Vars
conditionsCmd *ConditionsCmd
resultNumber mathexp.Number
}{
{
name: "single query and single condition",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
valBasedSeries(ptr.Float64(30), ptr.Float64(40)),
},
},
},
conditionsCmd: &ConditionsCmd{
Conditions: []condition{
{
QueryRefID: "A",
Reducer: classicReducer("avg"),
Operator: "and",
Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34},
},
}},
resultNumber: trueNumber,
},
{
name: "single query and single ranged condition",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{
valBasedSeries(ptr.Float64(30), ptr.Float64(40)),
},
},
},
conditionsCmd: &ConditionsCmd{
Conditions: []condition{
{
QueryRefID: "A",
Reducer: classicReducer("diff"),
Operator: "and",
Evaluator: &rangedEvaluator{Type: "within_range", Lower: 2, Upper: 3},
},
},
},
resultNumber: falseNumber,
},
{
name: "single query with no data",
vars: mathexp.Vars{
"A": mathexp.Results{
Values: []mathexp.Value{},
},
},
conditionsCmd: &ConditionsCmd{
Conditions: []condition{
{
QueryRefID: "A",
Reducer: classicReducer("avg"),
Operator: "and",
Evaluator: &thresholdEvaluator{"gt", 1},
},
},
},
resultNumber: noDataNumber,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
res, err := tt.conditionsCmd.Execute(context.Background(), tt.vars)
require.NoError(t, err)
require.Equal(t, 1, len(res.Values))
require.Equal(t, tt.resultNumber, res.Values[0])
})
}
}

@ -0,0 +1,98 @@
package classic
import (
"fmt"
"github.com/grafana/grafana/pkg/expr/mathexp"
)
type evaluator interface {
Eval(mathexp.Number) bool
}
type noValueEvaluator struct{}
type thresholdEvaluator struct {
Type string
Threshold float64
}
type rangedEvaluator struct {
Type string
Lower float64
Upper float64
}
// newAlertEvaluator is a factory function for returning
// an AlertEvaluator depending on evaluation operator.
func newAlertEvaluator(model conditionEvalJSON) (evaluator, error) {
switch model.Type {
case "gt", "lt":
return newThresholdEvaluator(model)
case "within_range", "outside_range":
return newRangedEvaluator(model)
case "no_value":
return &noValueEvaluator{}, nil
}
return nil, fmt.Errorf("evaluator invalid evaluator type: %s", model.Type)
}
func (e *thresholdEvaluator) Eval(reducedValue mathexp.Number) bool {
fv := reducedValue.GetFloat64Value()
if fv == nil {
return false
}
switch e.Type {
case "gt":
return *fv > e.Threshold
case "lt":
return *fv < e.Threshold
}
return false
}
func newThresholdEvaluator(model conditionEvalJSON) (*thresholdEvaluator, error) {
if len(model.Params) == 0 {
return nil, fmt.Errorf("evaluator '%v' is missing the threshold parameter", model.Type)
}
return &thresholdEvaluator{
Type: model.Type,
Threshold: model.Params[0],
}, nil
}
func (e *noValueEvaluator) Eval(reducedValue mathexp.Number) bool {
return reducedValue.GetFloat64Value() == nil
}
func newRangedEvaluator(model conditionEvalJSON) (*rangedEvaluator, error) {
if len(model.Params) != 2 {
return nil, fmt.Errorf("ranged evaluator requires 2 parameters")
}
return &rangedEvaluator{
Type: model.Type,
Lower: model.Params[0],
Upper: model.Params[1],
}, nil
}
func (e *rangedEvaluator) Eval(reducedValue mathexp.Number) bool {
fv := reducedValue.GetFloat64Value()
if fv == nil {
return false
}
switch e.Type {
case "within_range":
return (e.Lower < *fv && e.Upper > *fv) || (e.Upper < *fv && e.Lower > *fv)
case "outside_range":
return (e.Upper < *fv && e.Lower < *fv) || (e.Upper > *fv && e.Lower > *fv)
}
return false
}

@ -0,0 +1,143 @@
package classic
import (
"testing"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/stretchr/testify/require"
ptr "github.com/xorcare/pointer"
)
func TestThresholdEvaluator(t *testing.T) {
var tests = []struct {
name string
evaluator evaluator
inputNumber mathexp.Number
expected bool
}{
{
name: "value 3 is gt 1: true",
evaluator: &thresholdEvaluator{"gt", 1},
inputNumber: valBasedNumber(ptr.Float64(3)),
expected: true,
},
{
name: "value 1 is gt 3: false",
evaluator: &thresholdEvaluator{"gt", 3},
inputNumber: valBasedNumber(ptr.Float64(1)),
expected: false,
},
{
name: "value 3 is lt 1: true",
evaluator: &thresholdEvaluator{"lt", 1},
inputNumber: valBasedNumber(ptr.Float64(3)),
expected: false,
},
{
name: "value 1 is lt 3: false",
evaluator: &thresholdEvaluator{"lt", 3},
inputNumber: valBasedNumber(ptr.Float64(1)),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := tt.evaluator.Eval(tt.inputNumber)
require.Equal(t, tt.expected, b)
})
}
}
func TestRangedEvaluator(t *testing.T) {
var tests = []struct {
name string
evaluator evaluator
inputNumber mathexp.Number
expected bool
}{
// within
{
name: "value 3 is within range 1, 100: true",
evaluator: &rangedEvaluator{"within_range", 1, 100},
inputNumber: valBasedNumber(ptr.Float64(3)),
expected: true,
},
{
name: "value 300 is within range 1, 100: false",
evaluator: &rangedEvaluator{"within_range", 1, 100},
inputNumber: valBasedNumber(ptr.Float64(300)),
expected: false,
},
{
name: "value 3 is within range 100, 1: true",
evaluator: &rangedEvaluator{"within_range", 100, 1},
inputNumber: valBasedNumber(ptr.Float64(3)),
expected: true,
},
{
name: "value 300 is within range 100, 1: false",
evaluator: &rangedEvaluator{"within_range", 100, 1},
inputNumber: valBasedNumber(ptr.Float64(300)),
expected: false,
},
// outside
{
name: "value 1000 is outside range 1, 100: true",
evaluator: &rangedEvaluator{"outside_range", 1, 100},
inputNumber: valBasedNumber(ptr.Float64(1000)),
expected: true,
},
{
name: "value 50 is outside range 1, 100: false",
evaluator: &rangedEvaluator{"outside_range", 1, 100},
inputNumber: valBasedNumber(ptr.Float64(50)),
expected: false,
},
{
name: "value 1000 is outside range 100, 1: true",
evaluator: &rangedEvaluator{"outside_range", 100, 1},
inputNumber: valBasedNumber(ptr.Float64(1000)),
expected: true,
},
{
name: "value 50 is outside range 100, 1: false",
evaluator: &rangedEvaluator{"outside_range", 100, 1},
inputNumber: valBasedNumber(ptr.Float64(50)),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := tt.evaluator.Eval(tt.inputNumber)
require.Equal(t, tt.expected, b)
})
}
}
func TestNoValueEvaluator(t *testing.T) {
var tests = []struct {
name string
evaluator evaluator
inputNumber mathexp.Number
expected bool
}{
{
name: "value 50 is no_value: false",
evaluator: &noValueEvaluator{},
inputNumber: valBasedNumber(ptr.Float64(50)),
expected: false,
},
{
name: "value nil is no_value: true",
evaluator: &noValueEvaluator{},
inputNumber: valBasedNumber(nil),
expected: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := tt.evaluator.Eval(tt.inputNumber)
require.Equal(t, tt.expected, b)
})
}
}

@ -0,0 +1,205 @@
package classic
import (
"math"
"sort"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/mathexp"
)
func nilOrNaN(f *float64) bool {
return f == nil || math.IsNaN(*f)
}
func (cr classicReducer) ValidReduceFunc() bool {
switch cr {
case "avg", "sum", "min", "max", "count", "last", "median":
return true
case "diff", "diff_abs", "percent_diff", "percent_diff_abs", "count_not_null":
return true
}
return false
}
//nolint: gocyclo
func (cr classicReducer) Reduce(series mathexp.Series) mathexp.Number {
num := mathexp.NewNumber("", nil)
num.SetValue(nil)
if series.Len() == 0 {
return num
}
value := float64(0)
allNull := true
vF := series.Frame.Fields[series.ValueIdx]
switch cr {
case "avg":
validPointsCount := 0
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if nilOrNaN(f) {
continue
}
value += *f
validPointsCount++
allNull = false
}
}
if validPointsCount > 0 {
value /= float64(validPointsCount)
}
case "sum":
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if nilOrNaN(f) {
continue
}
value += *f
allNull = false
}
}
case "min":
value = math.MaxFloat64
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if nilOrNaN(f) {
continue
}
allNull = false
if value > *f {
value = *f
}
}
}
if allNull {
value = 0
}
case "max":
value = -math.MaxFloat64
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if nilOrNaN(f) {
continue
}
allNull = false
if value < *f {
value = *f
}
}
}
if allNull {
value = 0
}
case "count":
value = float64(vF.Len())
allNull = false
case "last":
for i := vF.Len() - 1; i >= 0; i-- {
if f, ok := vF.At(i).(*float64); ok {
if !nilOrNaN(f) {
value = *f
allNull = false
break
}
}
}
case "median":
var values []float64
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if nilOrNaN(f) {
continue
}
allNull = false
values = append(values, *f)
}
}
if len(values) >= 1 {
sort.Float64s(values)
length := len(values)
if length%2 == 1 {
value = values[(length-1)/2]
} else {
value = (values[(length/2)-1] + values[length/2]) / 2
}
}
case "diff":
allNull, value = calculateDiff(vF, allNull, value, diff)
case "diff_abs":
allNull, value = calculateDiff(vF, allNull, value, diffAbs)
case "percent_diff":
allNull, value = calculateDiff(vF, allNull, value, percentDiff)
case "percent_diff_abs":
allNull, value = calculateDiff(vF, allNull, value, percentDiffAbs)
case "count_non_null":
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if nilOrNaN(f) {
continue
}
value++
}
}
if value > 0 {
allNull = false
}
}
if allNull {
return num
}
num.SetValue(&value)
return num
}
func calculateDiff(vF *data.Field, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) {
var (
first float64
i int
)
// get the newest point
for i = vF.Len() - 1; i >= 0; i-- {
if f, ok := vF.At(i).(*float64); ok {
if !nilOrNaN(f) {
first = *f
allNull = false
break
}
}
}
if i >= 1 {
// get the oldest point
for i := 0; i < vF.Len(); i++ {
if f, ok := vF.At(i).(*float64); ok {
if !nilOrNaN(f) {
value = fn(first, *f)
allNull = false
break
}
}
}
}
return allNull, value
}
var diff = func(newest, oldest float64) float64 {
return newest - oldest
}
var diffAbs = func(newest, oldest float64) float64 {
return math.Abs(newest - oldest)
}
var percentDiff = func(newest, oldest float64) float64 {
return (newest - oldest) / math.Abs(oldest) * 100
}
var percentDiffAbs = func(newest, oldest float64) float64 {
return math.Abs((newest - oldest) / oldest * 100)
}

@ -0,0 +1,420 @@
package classic
import (
"math"
"testing"
"time"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/stretchr/testify/require"
ptr "github.com/xorcare/pointer"
)
func TestReducer(t *testing.T) {
var tests = []struct {
name string
reducer classicReducer
inputSeries mathexp.Series
expectedNumber mathexp.Number
}{
{
name: "sum",
reducer: classicReducer("sum"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3)),
expectedNumber: valBasedNumber(ptr.Float64(6)),
},
{
name: "min",
reducer: classicReducer("min"),
inputSeries: valBasedSeries(ptr.Float64(3), ptr.Float64(2), ptr.Float64(1)),
expectedNumber: valBasedNumber(ptr.Float64(1)),
},
{
name: "min with NaNs only",
reducer: classicReducer("min"),
inputSeries: valBasedSeries(ptr.Float64(math.NaN()), ptr.Float64(math.NaN()), ptr.Float64(math.NaN())),
expectedNumber: valBasedNumber(nil),
},
{
name: "max",
reducer: classicReducer("max"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3)),
expectedNumber: valBasedNumber(ptr.Float64(3)),
},
{
name: "count",
reducer: classicReducer("count"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3000)),
expectedNumber: valBasedNumber(ptr.Float64(3)),
},
{
name: "last",
reducer: classicReducer("last"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3000)),
expectedNumber: valBasedNumber(ptr.Float64(3000)),
},
{
name: "median with odd amount of numbers",
reducer: classicReducer("median"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3000)),
expectedNumber: valBasedNumber(ptr.Float64(2)),
},
{
name: "median with even amount of numbers",
reducer: classicReducer("median"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(4), ptr.Float64(3000)),
expectedNumber: valBasedNumber(ptr.Float64(3)),
},
{
name: "median with one value",
reducer: classicReducer("median"),
inputSeries: valBasedSeries(ptr.Float64(1)),
expectedNumber: valBasedNumber(ptr.Float64(1)),
},
{
name: "median should ignore null values",
reducer: classicReducer("median"),
inputSeries: valBasedSeries(nil, nil, nil, ptr.Float64(1), ptr.Float64(2), ptr.Float64(3)),
expectedNumber: valBasedNumber(ptr.Float64(2)),
},
{
name: "avg",
reducer: classicReducer("avg"),
inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3)),
expectedNumber: valBasedNumber(ptr.Float64(2)),
},
{
name: "avg with only nulls",
reducer: classicReducer("avg"),
inputSeries: valBasedSeries(nil),
expectedNumber: valBasedNumber(nil),
},
{
name: "avg of number values and null values should ignore nulls",
reducer: classicReducer("avg"),
inputSeries: valBasedSeries(ptr.Float64(3), nil, nil, ptr.Float64(3)),
expectedNumber: valBasedNumber(ptr.Float64(3)),
},
{
name: "count_non_null with mixed null/real values",
reducer: classicReducer("count_non_null"),
inputSeries: valBasedSeries(nil, nil, ptr.Float64(3), ptr.Float64(4)),
expectedNumber: valBasedNumber(ptr.Float64(2)),
},
{
name: "count_non_null with no values",
reducer: classicReducer("count_non_null"),
inputSeries: valBasedSeries(nil, nil),
expectedNumber: valBasedNumber(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
num := tt.reducer.Reduce(tt.inputSeries)
require.Equal(t, tt.expectedNumber, num)
})
}
}
func TestDiffReducer(t *testing.T) {
var tests = []struct {
name string
inputSeries mathexp.Series
expectedNumber mathexp.Number
}{
{
name: "diff of one positive point",
inputSeries: valBasedSeries(ptr.Float64(30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "diff of one negative point",
inputSeries: valBasedSeries(ptr.Float64(-30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "diff two positive points [1]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(10)),
},
{
name: "diff two positive points [2]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(20)),
expectedNumber: valBasedNumber(ptr.Float64(-10)),
},
{
name: "diff two negative points [1]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(-10)),
},
{
name: "diff two negative points [2]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-10)),
expectedNumber: valBasedNumber(ptr.Float64(20)),
},
{
name: "diff of one positive and one negative point",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(-70)),
},
{
name: "diff of one negative and one positive point",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(70)),
},
{
name: "diff of three positive points",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40), ptr.Float64(50)),
expectedNumber: valBasedNumber(ptr.Float64(20)),
},
{
name: "diff of three negative points",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40), ptr.Float64(-50)),
expectedNumber: valBasedNumber(ptr.Float64(-20)),
},
{
name: "diff with only nulls",
inputSeries: valBasedSeries(nil, nil),
expectedNumber: valBasedNumber(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
num := classicReducer("diff").Reduce(tt.inputSeries)
require.Equal(t, tt.expectedNumber, num)
})
}
}
func TestDiffAbsReducer(t *testing.T) {
var tests = []struct {
name string
inputSeries mathexp.Series
expectedNumber mathexp.Number
}{
{
name: "diff_abs of one positive point",
inputSeries: valBasedSeries(ptr.Float64(30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "diff_abs of one negative point",
inputSeries: valBasedSeries(ptr.Float64(-30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "diff_abs two positive points [1]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(10)),
},
{
name: "diff_abs two positive points [2]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(20)),
expectedNumber: valBasedNumber(ptr.Float64(10)),
},
{
name: "diff_abs two negative points [1]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(10)),
},
{
name: "diff_abs two negative points [2]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-10)),
expectedNumber: valBasedNumber(ptr.Float64(20)),
},
{
name: "diff_abs of one positive and one negative point",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(70)),
},
{
name: "diff_abs of one negative and one positive point",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(70)),
},
{
name: "diff_abs of three positive points",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40), ptr.Float64(50)),
expectedNumber: valBasedNumber(ptr.Float64(20)),
},
{
name: "diff_abs of three negative points",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40), ptr.Float64(-50)),
expectedNumber: valBasedNumber(ptr.Float64(20)),
},
{
name: "diff_abs with only nulls",
inputSeries: valBasedSeries(nil, nil),
expectedNumber: valBasedNumber(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
num := classicReducer("diff_abs").Reduce(tt.inputSeries)
require.Equal(t, tt.expectedNumber, num)
})
}
}
func TestPercentDiffReducer(t *testing.T) {
var tests = []struct {
name string
inputSeries mathexp.Series
expectedNumber mathexp.Number
}{
{
name: "percent_diff of one positive point",
inputSeries: valBasedSeries(ptr.Float64(30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "percent_diff of one negative point",
inputSeries: valBasedSeries(ptr.Float64(-30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "percent_diff two positive points [1]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(33.33333333333333)),
},
{
name: "percent_diff two positive points [2]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(20)),
expectedNumber: valBasedNumber(ptr.Float64(-33.33333333333333)),
},
{
name: "percent_diff two negative points [1]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(-33.33333333333333)),
},
{
name: "percent_diff two negative points [2]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-10)),
expectedNumber: valBasedNumber(ptr.Float64(66.66666666666666)),
},
{
name: "percent_diff of one positive and one negative point",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(-233.33333333333334)),
},
{
name: "percent_diff of one negative and one positive point",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(233.33333333333334)),
},
{
name: "percent_diff of three positive points",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40), ptr.Float64(50)),
expectedNumber: valBasedNumber(ptr.Float64(66.66666666666666)),
},
{
name: "percent_diff of three negative points",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40), ptr.Float64(-50)),
expectedNumber: valBasedNumber(ptr.Float64(-66.66666666666666)),
},
{
name: "percent_diff with only nulls",
inputSeries: valBasedSeries(nil, nil),
expectedNumber: valBasedNumber(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
num := classicReducer("percent_diff").Reduce(tt.inputSeries)
require.Equal(t, tt.expectedNumber, num)
})
}
}
func TestPercentDiffAbsReducer(t *testing.T) {
var tests = []struct {
name string
inputSeries mathexp.Series
expectedNumber mathexp.Number
}{
{
name: "percent_diff_abs of one positive point",
inputSeries: valBasedSeries(ptr.Float64(30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "percent_diff_abs of one negative point",
inputSeries: valBasedSeries(ptr.Float64(-30)),
expectedNumber: valBasedNumber(ptr.Float64(0)),
},
{
name: "percent_diff_abs two positive points [1]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(33.33333333333333)),
},
{
name: "percent_diff_abs two positive points [2]",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(20)),
expectedNumber: valBasedNumber(ptr.Float64(33.33333333333333)),
},
{
name: "percent_diff_abs two negative points [1]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(33.33333333333333)),
},
{
name: "percent_diff_abs two negative points [2]",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-10)),
expectedNumber: valBasedNumber(ptr.Float64(66.66666666666666)),
},
{
name: "percent_diff_abs of one positive and one negative point",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(-40)),
expectedNumber: valBasedNumber(ptr.Float64(233.33333333333334)),
},
{
name: "percent_diff_abs of one negative and one positive point",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(40)),
expectedNumber: valBasedNumber(ptr.Float64(233.33333333333334)),
},
{
name: "percent_diff_abs of three positive points",
inputSeries: valBasedSeries(ptr.Float64(30), ptr.Float64(40), ptr.Float64(50)),
expectedNumber: valBasedNumber(ptr.Float64(66.66666666666666)),
},
{
name: "percent_diff_abs of three negative points",
inputSeries: valBasedSeries(ptr.Float64(-30), ptr.Float64(-40), ptr.Float64(-50)),
expectedNumber: valBasedNumber(ptr.Float64(66.66666666666666)),
},
{
name: "percent_diff_abs with only nulls",
inputSeries: valBasedSeries(nil, nil),
expectedNumber: valBasedNumber(nil),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
num := classicReducer("percent_diff_abs").Reduce(tt.inputSeries)
require.Equal(t, tt.expectedNumber, num)
})
}
}
func valBasedSeries(vals ...*float64) mathexp.Series {
newSeries := mathexp.NewSeries("", nil, 0, false, 1, true, len(vals))
for idx, f := range vals {
err := newSeries.SetPoint(idx, unixTimePointer(int64(idx)), f)
if err != nil {
panic(err)
}
}
return newSeries
}
func unixTimePointer(sec int64) *time.Time {
t := time.Unix(sec, 0)
return &t
}
func valBasedNumber(f *float64) mathexp.Number {
newNumber := mathexp.NewNumber("", nil)
newNumber.SetValue(f)
return newNumber
}

@ -239,6 +239,8 @@ const (
TypeReduce
// TypeResample is the CMDType for a resampling expression.
TypeResample
// TypeClassicConditions is the CMDType for the classic condition operation.
TypeClassicConditions
)
func (gt CommandType) String() string {
@ -249,6 +251,8 @@ func (gt CommandType) String() string {
return "reduce"
case TypeResample:
return "resample"
case TypeClassicConditions:
return "classic_conditions"
default:
return "unknown"
}
@ -263,6 +267,8 @@ func ParseCommandType(s string) (CommandType, error) {
return TypeReduce, nil
case "resample":
return TypeResample, nil
case "classic_conditions":
return TypeClassicConditions, nil
default:
return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s)
}

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/expr/classic"
"github.com/grafana/grafana/pkg/expr/mathexp"
"gonum.org/v1/gonum/graph/simple"
@ -105,6 +106,8 @@ func buildCMDNode(dp *simple.DirectedGraph, rn *rawNode) (*CMDNode, error) {
node.Command, err = UnmarshalReduceCommand(rn)
case TypeResample:
node.Command, err = UnmarshalResampleCommand(rn)
case TypeClassicConditions:
node.Command, err = classic.UnmarshalConditionsCmd(rn.Query, rn.RefID)
default:
return nil, fmt.Errorf("expression command type '%v' in '%v' not implemented", commandType, rn.RefID)
}

Loading…
Cancel
Save