mirror of https://github.com/grafana/grafana
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 conditionpull/31614/head
parent
8d39e6640c
commit
a488ab8393
@ -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 |
||||
} |
Loading…
Reference in new issue