Alerting: Support median in reduce expressions (#91119)

* Alerting: support median in reduce expressions
pull/91396/head
Alexander Akhmetov 12 months ago committed by GitHub
parent 66bfb31d8e
commit a32854549c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      packages/grafana-data/src/transformations/fieldReducer.ts
  2. 32
      pkg/expr/mathexp/reduce.go
  3. 157
      pkg/expr/mathexp/reduce_test.go
  4. 10
      pkg/expr/query.panel.schema.json
  5. 10
      pkg/expr/query.request.schema.json
  6. 14
      pkg/expr/query.types.json
  7. 25
      public/api-enterprise-spec.json
  8. 1
      public/app/features/expressions/types.ts

@ -14,6 +14,7 @@ export enum ReducerID {
variance = 'variance', variance = 'variance',
stdDev = 'stdDev', stdDev = 'stdDev',
last = 'last', last = 'last',
median = 'median',
first = 'first', first = 'first',
count = 'count', count = 'count',
range = 'range', range = 'range',
@ -278,6 +279,14 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
aliasIds: ['avg'], aliasIds: ['avg'],
preservesUnits: true, preservesUnits: true,
}, },
{
id: ReducerID.median,
name: 'Median',
description: 'Median Value',
standard: true,
aliasIds: ['median'],
preservesUnits: true,
},
{ {
id: ReducerID.variance, id: ReducerID.variance,
name: 'Variance', name: 'Variance',

@ -3,6 +3,7 @@ package mathexp
import ( import (
"fmt" "fmt"
"math" "math"
"sort"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
) )
@ -20,11 +21,12 @@ const (
ReducerMax ReducerID = "max" ReducerMax ReducerID = "max"
ReducerCount ReducerID = "count" ReducerCount ReducerID = "count"
ReducerLast ReducerID = "last" ReducerLast ReducerID = "last"
ReducerMedian ReducerID = "median"
) )
// GetSupportedReduceFuncs returns collection of supported function names // GetSupportedReduceFuncs returns collection of supported function names
func GetSupportedReduceFuncs() []ReducerID { 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 { func Sum(fv *Float64Field) *float64 {
@ -98,6 +100,32 @@ func Last(fv *Float64Field) *float64 {
return fv.GetValue(fv.Len() - 1) 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) { func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) {
switch rFunc { switch rFunc {
case ReducerSum: case ReducerSum:
@ -112,6 +140,8 @@ func GetReduceFunc(rFunc ReducerID) (ReducerFunc, error) {
return Count, nil return Count, nil
case ReducerLast: case ReducerLast:
return Last, nil return Last, nil
case ReducerMedian:
return Median, nil
default: default:
return nil, fmt.Errorf("reduction %v not implemented", rFunc) return nil, fmt.Errorf("reduction %v not implemented", rFunc)
} }

@ -3,6 +3,7 @@ package mathexp
import ( import (
"math" "math"
"math/rand" "math/rand"
"sort"
"testing" "testing"
"time" "time"
@ -90,6 +91,100 @@ func TestSeriesReduce(t *testing.T) {
resultsIs: require.Equal, resultsIs: require.Equal,
results: resultValuesNoErr(makeNumber("", nil, NaN)), 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", name: "min series with a nil value",
red: "min", red: "min",
@ -257,6 +352,27 @@ func TestSeriesReduceDropNN(t *testing.T) {
vars: seriesEmpty, vars: seriesEmpty,
results: resultValuesNoErr(makeNumber("", nil, nil)), 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", name: "DropNN: mean series that becomes empty after filtering non-number",
red: "mean", red: "mean",
@ -351,6 +467,41 @@ func TestSeriesReduceReplaceNN(t *testing.T) {
vars: seriesNonNumbers, vars: seriesNonNumbers,
results: resultValuesNoErr(makeNumber("", nil, float64Pointer(replaceWith))), 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", name: "replaceNN: count empty series",
red: "count", 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
}

@ -187,7 +187,7 @@
"type": "string" "type": "string"
}, },
"reducer": { "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", "type": "string",
"enum": [ "enum": [
"sum", "sum",
@ -195,7 +195,8 @@
"min", "min",
"max", "max",
"count", "count",
"last" "last",
"median"
], ],
"x-enum-description": {} "x-enum-description": {}
}, },
@ -341,7 +342,7 @@
"additionalProperties": false "additionalProperties": false
}, },
"downsampler": { "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", "type": "string",
"enum": [ "enum": [
"sum", "sum",
@ -349,7 +350,8 @@
"min", "min",
"max", "max",
"count", "count",
"last" "last",
"median"
], ],
"x-enum-description": {} "x-enum-description": {}
}, },

@ -213,7 +213,7 @@
"type": "string" "type": "string"
}, },
"reducer": { "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", "type": "string",
"enum": [ "enum": [
"sum", "sum",
@ -221,7 +221,8 @@
"min", "min",
"max", "max",
"count", "count",
"last" "last",
"median"
], ],
"x-enum-description": {} "x-enum-description": {}
}, },
@ -367,7 +368,7 @@
"additionalProperties": false "additionalProperties": false
}, },
"downsampler": { "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", "type": "string",
"enum": [ "enum": [
"sum", "sum",
@ -375,7 +376,8 @@
"min", "min",
"max", "max",
"count", "count",
"last" "last",
"median"
], ],
"x-enum-description": {} "x-enum-description": {}
}, },

@ -56,7 +56,7 @@
{ {
"metadata": { "metadata": {
"name": "reduce", "name": "reduce",
"resourceVersion": "1709915979242", "resourceVersion": "1722250145266",
"creationTimestamp": "2024-02-21T22:09:26Z" "creationTimestamp": "2024-02-21T22:09:26Z"
}, },
"spec": { "spec": {
@ -79,14 +79,15 @@
"type": "string" "type": "string"
}, },
"reducer": { "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": [ "enum": [
"sum", "sum",
"mean", "mean",
"min", "min",
"max", "max",
"count", "count",
"last" "last",
"median"
], ],
"type": "string", "type": "string",
"x-enum-description": {} "x-enum-description": {}
@ -141,7 +142,7 @@
{ {
"metadata": { "metadata": {
"name": "resample", "name": "resample",
"resourceVersion": "1709915973363", "resourceVersion": "1722250145266",
"creationTimestamp": "2024-02-21T22:09:26Z" "creationTimestamp": "2024-02-21T22:09:26Z"
}, },
"spec": { "spec": {
@ -157,14 +158,15 @@
"description": "QueryType = resample", "description": "QueryType = resample",
"properties": { "properties": {
"downsampler": { "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": [ "enum": [
"sum", "sum",
"mean", "mean",
"min", "min",
"max", "max",
"count", "count",
"last" "last",
"median"
], ],
"type": "string", "type": "string",
"x-enum-description": {} "x-enum-description": {}

@ -4806,6 +4806,7 @@
"PENDING_PROCESSING", "PENDING_PROCESSING",
"PROCESSING", "PROCESSING",
"FINISHED", "FINISHED",
"CANCELED",
"ERROR", "ERROR",
"UNKNOWN" "UNKNOWN"
] ]
@ -5490,7 +5491,7 @@
"type": "object", "type": "object",
"title": "NavbarPreference defines model for NavbarPreference.", "title": "NavbarPreference defines model for NavbarPreference.",
"properties": { "properties": {
"savedItemIds": { "bookmarkIds": {
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
@ -7217,6 +7218,7 @@
"PENDING_PROCESSING", "PENDING_PROCESSING",
"PROCESSING", "PROCESSING",
"FINISHED", "FINISHED",
"CANCELED",
"ERROR", "ERROR",
"UNKNOWN" "UNKNOWN"
] ]
@ -7247,6 +7249,10 @@
"format": "int64" "format": "int64"
} }
}, },
"total": {
"type": "integer",
"format": "int64"
},
"types": { "types": {
"type": "object", "type": "object",
"additionalProperties": { "additionalProperties": {
@ -8380,6 +8386,23 @@
} }
} }
}, },
"healthResponse": {
"type": "object",
"properties": {
"commit": {
"type": "string"
},
"database": {
"type": "string"
},
"enterpriseCommit": {
"type": "string"
},
"version": {
"type": "string"
}
}
},
"publicError": { "publicError": {
"description": "PublicError is derived from Error and only contains information\navailable to the end user.", "description": "PublicError is derived from Error and only contains information\navailable to the end user.",
"type": "object", "type": "object",

@ -79,6 +79,7 @@ export const reducerTypes: Array<SelectableValue<string>> = [
{ value: ReducerID.min, label: 'Min', description: 'Get the minimum value' }, { value: ReducerID.min, label: 'Min', description: 'Get the minimum value' },
{ value: ReducerID.max, label: 'Max', description: 'Get the maximum value' }, { value: ReducerID.max, label: 'Max', description: 'Get the maximum value' },
{ value: ReducerID.mean, label: 'Mean', description: 'Get the average value' }, { value: ReducerID.mean, label: 'Mean', description: 'Get the average value' },
{ value: ReducerID.median, label: 'Median', description: 'Get the median value' },
{ value: ReducerID.sum, label: 'Sum', description: 'Get the sum of all values' }, { value: ReducerID.sum, label: 'Sum', description: 'Get the sum of all values' },
{ value: ReducerID.count, label: 'Count', description: 'Get the number of values' }, { value: ReducerID.count, label: 'Count', description: 'Get the number of values' },
{ value: ReducerID.last, label: 'Last', description: 'Get the last value' }, { value: ReducerID.last, label: 'Last', description: 'Get the last value' },

Loading…
Cancel
Save