diff --git a/pkg/expr/classic/classic.go b/pkg/expr/classic/classic.go index 2fd26fb0b5e..f0969c33ad7 100644 --- a/pkg/expr/classic/classic.go +++ b/pkg/expr/classic/classic.go @@ -11,92 +11,71 @@ import ( "github.com/grafana/grafana/pkg/expr/mathexp" ) -// ConditionsCmd is command for the classic conditions -// expression operation. +// ConditionsCmd is a command that supports the reduction and comparison of conditions. +// +// A condition in ConditionsCmd can reduce a time series, contain an instant metric, or the +// result of another expression; and checks if it exceeds a threshold, falls within a range, +// or does not contain a value. +// +// If ConditionsCmd contains more than one condition, it reduces the boolean outcomes of the +// threshold, range or value checks using the logical operator of the right hand side condition +// until all conditions have been reduced to a single boolean outcome. ConditionsCmd does not +// follow operator precedence. +// +// For example if we have the following classic condition: +// +// min(A) > 5 OR max(B) < 10 AND C = 1 +// +// which reduces to the following boolean outcomes: +// +// false OR true AND true +// +// then the outcome of ConditionsCmd is true. type ConditionsCmd struct { Conditions []condition - refID string + 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:"params"` - } `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. +// condition is a single condition in ConditionsCmd. type condition struct { - QueryRefID string - Reducer classicReducer - Evaluator evaluator - Operator string -} + InputRefID string -type classicReducer string + // Reducer reduces a series of data into a single result. An example of a reducer is the avg, + // min and max functions. + Reducer reducer + + // Evaluator evaluates the reduced time series, instant metric, or result of another expression + // against an evaluator. An example of an evaluator is checking if it exceeds a threshold, + // falls within a range, or does not contain a value. + Evaluator evaluator + + // Operator is the logical operator to use when there are two conditions in ConditionsCmd. + // If there are more than two conditions in ConditionsCmd then operator is used to compare + // the outcome of this condition with that of the condition before it. + Operator 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 { +func (cmd *ConditionsCmd) NeedsVars() []string { vars := []string{} - for _, c := range ccc.Conditions { - vars = append(vars, c.QueryRefID) + for _, c := range cmd.Conditions { + vars = append(vars, c.InputRefID) } return vars } -// EvalMatch represents the series violating the threshold. -// It goes into the metadata of data frames so it can be extracted. -type EvalMatch struct { - Value *float64 `json:"value"` - Metric string `json:"metric"` - Labels data.Labels `json:"labels"` -} - -func (em EvalMatch) MarshalJSON() ([]byte, error) { - fs := "" - if em.Value != nil { - fs = strconv.FormatFloat(*em.Value, 'f', -1, 64) - } - return json.Marshal(struct { - Value string `json:"value"` - Metric string `json:"metric"` - Labels data.Labels `json:"labels"` - }{ - fs, - em.Metric, - em.Labels, - }) -} - // 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) { +func (cmd *ConditionsCmd) Execute(_ context.Context, vars mathexp.Vars) (mathexp.Results, error) { firing := true newRes := mathexp.Results{} noDataFound := true matches := []EvalMatch{} - for i, c := range ccc.Conditions { - querySeriesSet := vars[c.QueryRefID] + for i, c := range cmd.Conditions { + querySeriesSet := vars[c.InputRefID] nilReducedCount := 0 firingCount := 0 for _, val := range querySeriesSet.Values { @@ -184,19 +163,70 @@ func (ccc *ConditionsCmd) Execute(ctx context.Context, vars mathexp.Vars) (mathe return newRes, nil } +// EvalMatch represents the series violating the threshold. +// It goes into the metadata of data frames so it can be extracted. +type EvalMatch struct { + Value *float64 `json:"value"` + Metric string `json:"metric"` + Labels data.Labels `json:"labels"` +} + +func (em EvalMatch) MarshalJSON() ([]byte, error) { + fs := "" + if em.Value != nil { + fs = strconv.FormatFloat(*em.Value, 'f', -1, 64) + } + return json.Marshal(struct { + Value string `json:"value"` + Metric string `json:"metric"` + Labels data.Labels `json:"labels"` + }{ + fs, + em.Metric, + em.Labels, + }) +} + +// ConditionJSON is the JSON model for a single condition in ConditionsCmd. +// It is based on services/alerting/conditions/query.go's newQueryCondition(). +type ConditionJSON struct { + Evaluator ConditionEvalJSON `json:"evaluator"` + Operator ConditionOperatorJSON `json:"operator"` + Query ConditionQueryJSON `json:"query"` + Reducer ConditionReducerJSON `json:"reducer"` +} + +type ConditionEvalJSON struct { + Params []float64 `json:"params"` + Type string `json:"type"` // e.g. "gt" +} + +type ConditionOperatorJSON struct { + Type string `json:"type"` +} + +type ConditionQueryJSON struct { + Params []string `json:"params"` +} + +type ConditionReducerJSON struct { + Type string `json:"type"` + // Params []interface{} `json:"params"` (Unused) +} + // 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 + var ccj []ConditionJSON 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, + RefID: refID, } for i, cj := range ccj { @@ -208,12 +238,12 @@ func UnmarshalConditionsCmd(rawQuery map[string]interface{}, refID string) (*Con cond.Operator = cj.Operator.Type if len(cj.Query.Params) == 0 || cj.Query.Params[0] == "" { - return nil, fmt.Errorf("condition %v is missing the query refID argument", i+1) + return nil, fmt.Errorf("condition %v is missing the query RefID argument", i+1) } - cond.QueryRefID = cj.Query.Params[0] + cond.InputRefID = cj.Query.Params[0] - cond.Reducer = classicReducer(cj.Reducer.Type) + cond.Reducer = reducer(cj.Reducer.Type) if !cond.Reducer.ValidReduceFunc() { return nil, fmt.Errorf("invalid reducer '%v' in condition %v", cond.Reducer, i+1) } diff --git a/pkg/expr/classic/classic_test.go b/pkg/expr/classic/classic_test.go index 6b670ec39e8..c5a895ae35e 100644 --- a/pkg/expr/classic/classic_test.go +++ b/pkg/expr/classic/classic_test.go @@ -49,8 +49,8 @@ func TestUnmarshalConditionCMD(t *testing.T) { expectedCommand: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 2}, }, @@ -89,8 +89,8 @@ func TestUnmarshalConditionCMD(t *testing.T) { expectedCommand: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("diff"), + InputRefID: "A", + Reducer: reducer("diff"), Operator: "or", Evaluator: &rangedEvaluator{Type: "within_range", Lower: 2, Upper: 3}, }, @@ -134,8 +134,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34}, }, @@ -158,8 +158,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34}, }, @@ -183,8 +183,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: .5}, }, @@ -207,13 +207,13 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("max"), + InputRefID: "A", + Reducer: reducer("max"), Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34}, }, { - QueryRefID: "A", - Reducer: classicReducer("min"), + InputRefID: "A", + Reducer: reducer("min"), Operator: "or", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 12}, }, @@ -237,8 +237,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34}, }, @@ -262,8 +262,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34}, }, @@ -287,8 +287,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{Type: "gt", Threshold: 34}, }, @@ -311,8 +311,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("diff"), + InputRefID: "A", + Reducer: reducer("diff"), Operator: "and", Evaluator: &rangedEvaluator{Type: "within_range", Lower: 2, Upper: 3}, }, @@ -334,8 +334,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{"gt", 1}, }, @@ -361,8 +361,8 @@ func TestConditionsCmdExecute(t *testing.T) { conditionsCmd: &ConditionsCmd{ Conditions: []condition{ { - QueryRefID: "A", - Reducer: classicReducer("avg"), + InputRefID: "A", + Reducer: reducer("avg"), Operator: "and", Evaluator: &thresholdEvaluator{"gt", 1}, }, diff --git a/pkg/expr/classic/reduce.go b/pkg/expr/classic/reduce.go index 6b80c3a703f..967d45ae9ce 100644 --- a/pkg/expr/classic/reduce.go +++ b/pkg/expr/classic/reduce.go @@ -7,11 +7,9 @@ import ( "github.com/grafana/grafana/pkg/expr/mathexp" ) -func nilOrNaN(f *float64) bool { - return f == nil || math.IsNaN(*f) -} +type reducer string -func (cr classicReducer) ValidReduceFunc() bool { +func (cr reducer) ValidReduceFunc() bool { switch cr { case "avg", "sum", "min", "max", "count", "last", "median": return true @@ -22,7 +20,7 @@ func (cr classicReducer) ValidReduceFunc() bool { } //nolint:gocyclo -func (cr classicReducer) Reduce(series mathexp.Series) mathexp.Number { +func (cr reducer) Reduce(series mathexp.Series) mathexp.Number { num := mathexp.NewNumber("", nil) if series.GetLabels() != nil { @@ -184,6 +182,10 @@ func calculateDiff(ff mathexp.Float64Field, allNull bool, value float64, fn func return allNull, value } +func nilOrNaN(f *float64) bool { + return f == nil || math.IsNaN(*f) +} + var diff = func(newest, oldest float64) float64 { return newest - oldest } diff --git a/pkg/expr/classic/reduce_test.go b/pkg/expr/classic/reduce_test.go index ea2bc7cbbb3..8e146215a2b 100644 --- a/pkg/expr/classic/reduce_test.go +++ b/pkg/expr/classic/reduce_test.go @@ -14,103 +14,103 @@ import ( func TestReducer(t *testing.T) { var tests = []struct { name string - reducer classicReducer + reducer reducer inputSeries mathexp.Series expectedNumber mathexp.Number }{ { name: "sum", - reducer: classicReducer("sum"), + reducer: reducer("sum"), inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3)), expectedNumber: valBasedNumber(ptr.Float64(6)), }, { name: "min", - reducer: classicReducer("min"), + reducer: reducer("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"), + reducer: reducer("min"), inputSeries: valBasedSeries(ptr.Float64(math.NaN()), ptr.Float64(math.NaN()), ptr.Float64(math.NaN())), expectedNumber: valBasedNumber(nil), }, { name: "max", - reducer: classicReducer("max"), + reducer: reducer("max"), inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3)), expectedNumber: valBasedNumber(ptr.Float64(3)), }, { name: "count", - reducer: classicReducer("count"), + reducer: reducer("count"), inputSeries: valBasedSeries(ptr.Float64(1), ptr.Float64(2), ptr.Float64(3000)), expectedNumber: valBasedNumber(ptr.Float64(3)), }, { name: "last", - reducer: classicReducer("last"), + reducer: reducer("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"), + reducer: reducer("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"), + reducer: reducer("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"), + reducer: reducer("median"), inputSeries: valBasedSeries(ptr.Float64(1)), expectedNumber: valBasedNumber(ptr.Float64(1)), }, { name: "median should ignore null values", - reducer: classicReducer("median"), + reducer: reducer("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"), + reducer: reducer("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"), + reducer: reducer("avg"), inputSeries: valBasedSeries(nil), expectedNumber: valBasedNumber(nil), }, { name: "avg of number values and null values should ignore nulls", - reducer: classicReducer("avg"), + reducer: reducer("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"), + reducer: reducer("count_non_null"), inputSeries: valBasedSeries(nil, nil, ptr.Float64(3), ptr.Float64(4)), expectedNumber: valBasedNumber(ptr.Float64(2)), }, { name: "count_non_null with mixed null/real values", - reducer: classicReducer("count_non_null"), + reducer: reducer("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"), + reducer: reducer("count_non_null"), inputSeries: valBasedSeries(nil, nil), expectedNumber: valBasedNumber(nil), }, @@ -189,7 +189,7 @@ func TestDiffReducer(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - num := classicReducer("diff").Reduce(tt.inputSeries) + num := reducer("diff").Reduce(tt.inputSeries) require.Equal(t, tt.expectedNumber, num) }) } @@ -259,7 +259,7 @@ func TestDiffAbsReducer(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - num := classicReducer("diff_abs").Reduce(tt.inputSeries) + num := reducer("diff_abs").Reduce(tt.inputSeries) require.Equal(t, tt.expectedNumber, num) }) } @@ -329,7 +329,7 @@ func TestPercentDiffReducer(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - num := classicReducer("percent_diff").Reduce(tt.inputSeries) + num := reducer("percent_diff").Reduce(tt.inputSeries) require.Equal(t, tt.expectedNumber, num) }) } @@ -399,7 +399,7 @@ func TestPercentDiffAbsReducer(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - num := classicReducer("percent_diff_abs").Reduce(tt.inputSeries) + num := reducer("percent_diff_abs").Reduce(tt.inputSeries) require.Equal(t, tt.expectedNumber, num) }) }