Alerting: Support median in reduce expressions (#91119)

* Alerting: support median in reduce expressions
This commit is contained in:
Alexander Akhmetov
2024-08-01 15:04:17 +02:00
committed by GitHub
parent 66bfb31d8e
commit a32854549c
8 changed files with 248 additions and 22 deletions

View File

@@ -3,6 +3,7 @@ package mathexp
import (
"fmt"
"math"
"sort"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
@@ -14,17 +15,18 @@ type ReducerFunc = func(fv *Float64Field) *float64
type ReducerID string
const (
ReducerSum ReducerID = "sum"
ReducerMean ReducerID = "mean"
ReducerMin ReducerID = "min"
ReducerMax ReducerID = "max"
ReducerCount ReducerID = "count"
ReducerLast ReducerID = "last"
ReducerSum ReducerID = "sum"
ReducerMean ReducerID = "mean"
ReducerMin ReducerID = "min"
ReducerMax ReducerID = "max"
ReducerCount ReducerID = "count"
ReducerLast ReducerID = "last"
ReducerMedian ReducerID = "median"
)
// GetSupportedReduceFuncs returns collection of supported function names
func GetSupportedReduceFuncs() []ReducerID {
return []ReducerID{ReducerSum, ReducerMean, ReducerMin, ReducerMax, ReducerCount, ReducerLast}
return []ReducerID{ReducerSum, ReducerMean, ReducerMin, ReducerMax, ReducerCount, ReducerLast, ReducerMedian}
}
func Sum(fv *Float64Field) *float64 {
@@ -98,6 +100,32 @@ func Last(fv *Float64Field) *float64 {
return fv.GetValue(fv.Len() - 1)
}
func Median(fv *Float64Field) *float64 {
values := make([]float64, 0, fv.Len())
for i := 0; i < fv.Len(); i++ {
v := fv.GetValue(i)
if v == nil || math.IsNaN(*v) {
nan := math.NaN()
return &nan
}
values = append(values, *v)
}
if len(values) == 0 {
nan := math.NaN()
return &nan
}
sort.Float64s(values)
mid := len(values) / 2
if len(values)%2 == 0 {
v := (values[mid-1] + values[mid]) / 2
return &v
} else {
return &values[mid]
}
}
func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) {
switch rFunc {
case ReducerSum:
@@ -112,6 +140,8 @@ func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) {
return Count, nil
case ReducerLast:
return Last, nil
case ReducerMedian:
return Median, nil
default:
return nil, fmt.Errorf("reduction %v not implemented", rFunc)
}

View File

@@ -3,6 +3,7 @@ package mathexp
import (
"math"
"math/rand"
"sort"
"testing"
"time"
@@ -90,6 +91,100 @@ func TestSeriesReduce(t *testing.T) {
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, NaN)),
},
{
name: "median empty series",
red: "median",
varToReduce: "A",
vars: seriesEmpty,
errIs: require.NoError,
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, NaN)),
},
{
name: "median series with a nil value",
red: "median",
varToReduce: "A",
vars: seriesWithNil,
errIs: require.NoError,
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, NaN)),
},
{
name: "median series even number of elements",
red: "median",
varToReduce: "A",
vars: Vars{
"A": resultValuesNoErr(
makeSeries("temp", nil, tp{
time.Unix(5, 0), float64Pointer(20),
}, tp{
time.Unix(10, 0), float64Pointer(10),
}),
),
},
errIs: require.NoError,
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(15))),
},
{
name: "median series odd number of elements",
red: "median",
varToReduce: "A",
vars: Vars{
"A": resultValuesNoErr(
makeSeries("temp", nil, tp{
time.Unix(5, 0), float64Pointer(20),
}, tp{
time.Unix(10, 0), float64Pointer(10),
}, tp{
time.Unix(15, 0), float64Pointer(5),
}),
),
},
errIs: require.NoError,
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(10))),
},
{
name: "median series with repeated values",
red: "median",
varToReduce: "A",
vars: Vars{
"A": resultValuesNoErr(
makeSeries("temp", nil, tp{
time.Unix(5, 0), float64Pointer(5),
}, tp{
time.Unix(10, 0), float64Pointer(5),
}, tp{
time.Unix(15, 0), float64Pointer(5),
}),
),
},
errIs: require.NoError,
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(5))),
},
{
name: "median series with negative values",
red: "median",
varToReduce: "A",
vars: Vars{
"A": resultValuesNoErr(
makeSeries("temp", nil, tp{
time.Unix(5, 0), float64Pointer(-1),
}, tp{
time.Unix(10, 0), float64Pointer(-3),
}, tp{
time.Unix(10, 0), float64Pointer(-4),
}, tp{
time.Unix(15, 0), float64Pointer(-2),
}),
),
},
errIs: require.NoError,
resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(-2.5))),
},
{
name: "min series with a nil value",
red: "min",
@@ -257,6 +352,27 @@ func TestSeriesReduceDropNN(t *testing.T) {
vars: seriesEmpty,
results: resultValuesNoErr(makeNumber("", nil, nil)),
},
{
name: "DropNN: median series with a nil value and real value",
red: "median",
varToReduce: "A",
vars: seriesWithNil,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(2))),
},
{
name: "DropNN: median empty series",
red: "median",
varToReduce: "A",
vars: seriesEmpty,
results: resultValuesNoErr(makeNumber("", nil, nil)),
},
{
name: "DropNN: median series that becomes empty after filtering non-number",
red: "median",
varToReduce: "A",
vars: seriesNonNumbers,
results: resultValuesNoErr(makeNumber("", nil, nil)),
},
{
name: "DropNN: mean series that becomes empty after filtering non-number",
red: "mean",
@@ -351,6 +467,41 @@ func TestSeriesReduceReplaceNN(t *testing.T) {
vars: seriesNonNumbers,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))),
},
{
name: "replaceNN: median series with a nil value and real value",
red: "median",
varToReduce: "A",
vars: Vars{
"A": resultValuesNoErr(
makeSeries("temp", nil, tp{
time.Unix(5, 0), float64Pointer(2),
}, tp{
time.Unix(10, 0), nil,
}, tp{
time.Unix(15, 0), float64Pointer(5),
}),
),
},
results: resultValuesNoErr(
makeNumber("", nil, float64Pointer(
sortedFloat64([]float64{2, 5, replaceWith})[1]),
),
),
},
{
name: "replaceNN: median empty series",
red: "median",
varToReduce: "A",
vars: seriesEmpty,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))),
},
{
name: "replaceNN: median series that becomes empty after filtering non-number",
red: "median",
varToReduce: "A",
vars: seriesNonNumbers,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))),
},
{
name: "replaceNN: count empty series",
red: "count",
@@ -386,3 +537,9 @@ func TestSeriesReduceReplaceNN(t *testing.T) {
})
}
}
func sortedFloat64(f []float64) []float64 {
f = append([]float64(nil), f...)
sort.Float64s(f)
return f
}

View File

@@ -187,7 +187,7 @@
"type": "string"
},
"reducer": {
"description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ",
"description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ",
"type": "string",
"enum": [
"sum",
@@ -195,7 +195,8 @@
"min",
"max",
"count",
"last"
"last",
"median"
],
"x-enum-description": {}
},
@@ -341,7 +342,7 @@
"additionalProperties": false
},
"downsampler": {
"description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ",
"description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ",
"type": "string",
"enum": [
"sum",
@@ -349,7 +350,8 @@
"min",
"max",
"count",
"last"
"last",
"median"
],
"x-enum-description": {}
},

View File

@@ -213,7 +213,7 @@
"type": "string"
},
"reducer": {
"description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ",
"description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ",
"type": "string",
"enum": [
"sum",
@@ -221,7 +221,8 @@
"min",
"max",
"count",
"last"
"last",
"median"
],
"x-enum-description": {}
},
@@ -367,7 +368,7 @@
"additionalProperties": false
},
"downsampler": {
"description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ",
"description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ",
"type": "string",
"enum": [
"sum",
@@ -375,7 +376,8 @@
"min",
"max",
"count",
"last"
"last",
"median"
],
"x-enum-description": {}
},

View File

@@ -56,7 +56,7 @@
{
"metadata": {
"name": "reduce",
"resourceVersion": "1709915979242",
"resourceVersion": "1722250145266",
"creationTimestamp": "2024-02-21T22:09:26Z"
},
"spec": {
@@ -79,14 +79,15 @@
"type": "string"
},
"reducer": {
"description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ",
"description": "The reducer\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ",
"enum": [
"sum",
"mean",
"min",
"max",
"count",
"last"
"last",
"median"
],
"type": "string",
"x-enum-description": {}
@@ -141,7 +142,7 @@
{
"metadata": {
"name": "resample",
"resourceVersion": "1709915973363",
"resourceVersion": "1722250145266",
"creationTimestamp": "2024-02-21T22:09:26Z"
},
"spec": {
@@ -157,14 +158,15 @@
"description": "QueryType = resample",
"properties": {
"downsampler": {
"description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` ",
"description": "The downsample function\n\n\nPossible enum values:\n - `\"sum\"` \n - `\"mean\"` \n - `\"min\"` \n - `\"max\"` \n - `\"count\"` \n - `\"last\"` \n - `\"median\"` ",
"enum": [
"sum",
"mean",
"min",
"max",
"count",
"last"
"last",
"median"
],
"type": "string",
"x-enum-description": {}