mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
8d39e6640c
commit
a488ab8393
1
go.mod
1
go.mod
@ -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
|
||||
|
4
go.sum
4
go.sum
@ -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=
|
||||
|
160
pkg/expr/classic/classic.go
Normal file
160
pkg/expr/classic/classic.go
Normal file
@ -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
|
||||
}
|
199
pkg/expr/classic/classic_test.go
Normal file
199
pkg/expr/classic/classic_test.go
Normal file
@ -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])
|
||||
})
|
||||
}
|
||||
}
|
98
pkg/expr/classic/evaluator.go
Normal file
98
pkg/expr/classic/evaluator.go
Normal file
@ -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
|
||||
}
|
143
pkg/expr/classic/evaluator_test.go
Normal file
143
pkg/expr/classic/evaluator_test.go
Normal file
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
205
pkg/expr/classic/reduce.go
Normal file
205
pkg/expr/classic/reduce.go
Normal file
@ -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)
|
||||
}
|
420
pkg/expr/classic/reduce_test.go
Normal file
420
pkg/expr/classic/reduce_test.go
Normal file
@ -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…
Reference in New Issue
Block a user