mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Support median in reduce expressions (#91119)
* Alerting: support median in reduce expressions
This commit is contained in:
parent
66bfb31d8e
commit
a32854549c
@ -14,6 +14,7 @@ export enum ReducerID {
|
||||
variance = 'variance',
|
||||
stdDev = 'stdDev',
|
||||
last = 'last',
|
||||
median = 'median',
|
||||
first = 'first',
|
||||
count = 'count',
|
||||
range = 'range',
|
||||
@ -278,6 +279,14 @@ export const fieldReducers = new Registry<FieldReducerInfo>(() => [
|
||||
aliasIds: ['avg'],
|
||||
preservesUnits: true,
|
||||
},
|
||||
{
|
||||
id: ReducerID.median,
|
||||
name: 'Median',
|
||||
description: 'Median Value',
|
||||
standard: true,
|
||||
aliasIds: ['median'],
|
||||
preservesUnits: true,
|
||||
},
|
||||
{
|
||||
id: ReducerID.variance,
|
||||
name: 'Variance',
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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": {}
|
||||
},
|
||||
|
@ -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": {}
|
||||
},
|
||||
|
@ -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": {}
|
||||
|
@ -4806,6 +4806,7 @@
|
||||
"PENDING_PROCESSING",
|
||||
"PROCESSING",
|
||||
"FINISHED",
|
||||
"CANCELED",
|
||||
"ERROR",
|
||||
"UNKNOWN"
|
||||
]
|
||||
@ -5490,7 +5491,7 @@
|
||||
"type": "object",
|
||||
"title": "NavbarPreference defines model for NavbarPreference.",
|
||||
"properties": {
|
||||
"savedItemIds": {
|
||||
"bookmarkIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
@ -7217,6 +7218,7 @@
|
||||
"PENDING_PROCESSING",
|
||||
"PROCESSING",
|
||||
"FINISHED",
|
||||
"CANCELED",
|
||||
"ERROR",
|
||||
"UNKNOWN"
|
||||
]
|
||||
@ -7247,6 +7249,10 @@
|
||||
"format": "int64"
|
||||
}
|
||||
},
|
||||
"total": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"types": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
@ -8380,6 +8386,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"healthResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"commit": {
|
||||
"type": "string"
|
||||
},
|
||||
"database": {
|
||||
"type": "string"
|
||||
},
|
||||
"enterpriseCommit": {
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"publicError": {
|
||||
"description": "PublicError is derived from Error and only contains information\navailable to the end user.",
|
||||
"type": "object",
|
||||
|
@ -79,6 +79,7 @@ export const reducerTypes: Array<SelectableValue<string>> = [
|
||||
{ value: ReducerID.min, label: 'Min', description: 'Get the minimum value' },
|
||||
{ value: ReducerID.max, label: 'Max', description: 'Get the maximum 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.count, label: 'Count', description: 'Get the number of values' },
|
||||
{ value: ReducerID.last, label: 'Last', description: 'Get the last value' },
|
||||
|
Loading…
Reference in New Issue
Block a user