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_golang v1.9.0
|
||||||
github.com/prometheus/client_model v0.2.0
|
github.com/prometheus/client_model v0.2.0
|
||||||
github.com/prometheus/common v0.18.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 v0.0.0-20180505203441-b41be1df6967
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/russellhaering/goxmldsig v1.1.0
|
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.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 h1:BDAexvKlOVjE5A8MlqRxzwkEpPl1/v6ydU1/J7kJtZc=
|
||||||
github.com/prometheus/prometheus v1.8.2-0.20201014093524-73e2ce1bd643/go.mod h1:XYjkJiog7fyQu3puQNivZPI2pNq1C/775EIoHfDvuvY=
|
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/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 h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
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
|
TypeReduce
|
||||||
// TypeResample is the CMDType for a resampling expression.
|
// TypeResample is the CMDType for a resampling expression.
|
||||||
TypeResample
|
TypeResample
|
||||||
|
// TypeClassicConditions is the CMDType for the classic condition operation.
|
||||||
|
TypeClassicConditions
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gt CommandType) String() string {
|
func (gt CommandType) String() string {
|
||||||
@ -249,6 +251,8 @@ func (gt CommandType) String() string {
|
|||||||
return "reduce"
|
return "reduce"
|
||||||
case TypeResample:
|
case TypeResample:
|
||||||
return "resample"
|
return "resample"
|
||||||
|
case TypeClassicConditions:
|
||||||
|
return "classic_conditions"
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
@ -263,6 +267,8 @@ func ParseCommandType(s string) (CommandType, error) {
|
|||||||
return TypeReduce, nil
|
return TypeReduce, nil
|
||||||
case "resample":
|
case "resample":
|
||||||
return TypeResample, nil
|
return TypeResample, nil
|
||||||
|
case "classic_conditions":
|
||||||
|
return TypeClassicConditions, nil
|
||||||
default:
|
default:
|
||||||
return TypeUnknown, fmt.Errorf("'%v' is not a recognized expression type", s)
|
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/backend"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/grafana/grafana/pkg/expr/classic"
|
||||||
"github.com/grafana/grafana/pkg/expr/mathexp"
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
||||||
|
|
||||||
"gonum.org/v1/gonum/graph/simple"
|
"gonum.org/v1/gonum/graph/simple"
|
||||||
@ -105,6 +106,8 @@ func buildCMDNode(dp *simple.DirectedGraph, rn *rawNode) (*CMDNode, error) {
|
|||||||
node.Command, err = UnmarshalReduceCommand(rn)
|
node.Command, err = UnmarshalReduceCommand(rn)
|
||||||
case TypeResample:
|
case TypeResample:
|
||||||
node.Command, err = UnmarshalResampleCommand(rn)
|
node.Command, err = UnmarshalResampleCommand(rn)
|
||||||
|
case TypeClassicConditions:
|
||||||
|
node.Command, err = classic.UnmarshalConditionsCmd(rn.Query, rn.RefID)
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("expression command type '%v' in '%v' not implemented", commandType, rn.RefID)
|
return nil, fmt.Errorf("expression command type '%v' in '%v' not implemented", commandType, rn.RefID)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user