From aff3c702abbf13010ce4bd5688938b2ae5d14914 Mon Sep 17 00:00:00 2001 From: pschou Date: Sat, 20 Feb 2021 10:34:52 -0500 Subject: [PATCH] promql: Add sgn, clamp and last_over_time functions (#8457) * Add sgn, clamp and last_over_time functions Signed-off-by: schou --- docs/querying/functions.md | 14 +++++++++++ promql/engine.go | 13 ++++++---- promql/functions.go | 43 ++++++++++++++++++++++++++++++++- promql/parser/functions.go | 15 ++++++++++++ promql/testdata/functions.test | 44 +++++++++++++++++++++++++++++++++- 5 files changed, 123 insertions(+), 6 deletions(-) diff --git a/docs/querying/functions.md b/docs/querying/functions.md index 2adaecf00b..16a440165f 100644 --- a/docs/querying/functions.md +++ b/docs/querying/functions.md @@ -73,6 +73,15 @@ For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector. +## `clamp()` + +`clamp(v instant-vector, min scalar, max scalar)` +clamps the sample values of all elements in `v` to have a lower limit of `min` and an upper limit of `max`. + +Special cases: +- Return an empty vector if `min > max` +- Return `NaN` if `min` or `max` is `NaN` + ## `clamp_max()` `clamp_max(v instant-vector, max scalar)` clamps the sample values of all @@ -370,6 +379,10 @@ Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`. +## `sgn()` + +`sgn(v instant-vector)` returns a vector with all sample values converted to their sign, defined as this: 1 if v is positive, -1 if v is negative and 0 if v is equal to zero. + ## `sort()` `sort(v instant-vector)` returns vector elements sorted by their sample values, @@ -418,6 +431,7 @@ over time and return an instant vector with per-series aggregation results: * `quantile_over_time(scalar, range-vector)`: the φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval. * `stddev_over_time(range-vector)`: the population standard deviation of the values in the specified interval. * `stdvar_over_time(range-vector)`: the population standard variance of the values in the specified interval. +* `last_over_time(range-vector)`: the most recent point value in specified interval. Note that all values in the specified interval have the same weight in the aggregation even if the values are not equally spaced throughout the interval. diff --git a/promql/engine.go b/promql/engine.go index 2ed1014419..0923bedbc5 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1216,11 +1216,16 @@ func (ev *evaluator) eval(expr parser.Expr) (parser.Value, storage.Warnings) { ev.currentSamples -= len(points) points = points[:0] it.Reset(s.Iterator()) + metric := selVS.Series[i].Labels() + // The last_over_time function acts like offset; thus, it + // should keep the metric name. For all the other range + // vector functions, the only change needed is to drop the + // metric name in the output. + if e.Func.Name != "last_over_time" { + metric = dropMetricName(metric) + } ss := Series{ - // For all range vector functions, the only change to the - // output labels is dropping the metric name so just do - // it once here. - Metric: dropMetricName(selVS.Series[i].Labels()), + Metric: metric, Points: getPointSlice(numSteps), } inMatrix[0].Metric = selVS.Series[i].Labels() diff --git a/promql/functions.go b/promql/functions.go index 3a96a9ecec..d96d625752 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -278,6 +278,23 @@ func funcSortDesc(vals []parser.Value, args parser.Expressions, enh *EvalNodeHel return Vector(byValueSorter) } +// === clamp(Vector parser.ValueTypeVector, min, max Scalar) Vector === +func funcClamp(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector { + vec := vals[0].(Vector) + min := vals[1].(Vector)[0].Point.V + max := vals[2].(Vector)[0].Point.V + if max < min { + return enh.Out + } + for _, el := range vec { + enh.Out = append(enh.Out, Sample{ + Metric: enh.DropMetricName(el.Metric), + Point: Point{V: math.Max(min, math.Min(max, el.V))}, + }) + } + return enh.Out +} + // === clamp_max(Vector parser.ValueTypeVector, max Scalar) Vector === func funcClampMax(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector { vec := vals[0].(Vector) @@ -383,7 +400,16 @@ func funcCountOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNo }) } -// === floor(Vector parser.ValueTypeVector) Vector === +// === last_over_time(Matrix parser.ValueTypeMatrix) Vector === +func funcLastOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector { + el := vals[0].(Matrix)[0] + + return append(enh.Out, Sample{ + Metric: el.Metric, + Point: Point{V: el.Points[len(el.Points)-1].V}, + }) +} + // === max_over_time(Matrix parser.ValueTypeMatrix) Vector === func funcMaxOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector { return aggrOverTime(vals, enh, func(values []Point) float64 { @@ -537,6 +563,18 @@ func funcLog10(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper return simpleFunc(vals, enh, math.Log10) } +// === sgn(Vector parser.ValueTypeVector) Vector === +func funcSgn(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector { + return simpleFunc(vals, enh, func(v float64) float64 { + if v < 0 { + return -1 + } else if v > 0 { + return 1 + } + return v + }) +} + // === timestamp(Vector parser.ValueTypeVector) Vector === func funcTimestamp(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) Vector { vec := vals[0].(Vector) @@ -893,6 +931,7 @@ var FunctionCalls = map[string]FunctionCall{ "avg_over_time": funcAvgOverTime, "ceil": funcCeil, "changes": funcChanges, + "clamp": funcClamp, "clamp_max": funcClampMax, "clamp_min": funcClampMin, "count_over_time": funcCountOverTime, @@ -914,6 +953,7 @@ var FunctionCalls = map[string]FunctionCall{ "ln": funcLn, "log10": funcLog10, "log2": funcLog2, + "last_over_time": funcLastOverTime, "max_over_time": funcMaxOverTime, "min_over_time": funcMinOverTime, "minute": funcMinute, @@ -924,6 +964,7 @@ var FunctionCalls = map[string]FunctionCall{ "resets": funcResets, "round": funcRound, "scalar": funcScalar, + "sgn": funcSgn, "sort": funcSort, "sort_desc": funcSortDesc, "sqrt": funcSqrt, diff --git a/promql/parser/functions.go b/promql/parser/functions.go index 4516829e55..a127cd28a4 100644 --- a/promql/parser/functions.go +++ b/promql/parser/functions.go @@ -54,6 +54,11 @@ var Functions = map[string]*Function{ ArgTypes: []ValueType{ValueTypeMatrix}, ReturnType: ValueTypeVector, }, + "clamp": { + Name: "clamp", + ArgTypes: []ValueType{ValueTypeVector, ValueTypeScalar, ValueTypeScalar}, + ReturnType: ValueTypeVector, + }, "clamp_max": { Name: "clamp_max", ArgTypes: []ValueType{ValueTypeVector, ValueTypeScalar}, @@ -149,6 +154,11 @@ var Functions = map[string]*Function{ Variadic: -1, ReturnType: ValueTypeVector, }, + "last_over_time": { + Name: "last_over_time", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + }, "ln": { Name: "ln", ArgTypes: []ValueType{ValueTypeVector}, @@ -217,6 +227,11 @@ var Functions = map[string]*Function{ ArgTypes: []ValueType{ValueTypeVector}, ReturnType: ValueTypeScalar, }, + "sgn": { + Name: "sgn", + ArgTypes: []ValueType{ValueTypeVector}, + ReturnType: ValueTypeVector, + }, "sort": { Name: "sort", ArgTypes: []ValueType{ValueTypeVector}, diff --git a/promql/testdata/functions.test b/promql/testdata/functions.test index b9dc27cecb..63b67d181b 100644 --- a/promql/testdata/functions.test +++ b/promql/testdata/functions.test @@ -372,7 +372,7 @@ eval instant at 60m vector(time()) {} 3600 -# Tests for clamp_max and clamp_min(). +# Tests for clamp_max, clamp_min(), and clamp(). load 5m test_clamp{src="clamp-a"} -50 test_clamp{src="clamp-b"} 0 @@ -388,6 +388,11 @@ eval instant at 0m clamp_min(test_clamp, -25) {src="clamp-b"} 0 {src="clamp-c"} 100 +eval instant at 0m clamp(test_clamp, -25, 75) + {src="clamp-a"} -25 + {src="clamp-b"} 0 + {src="clamp-c"} 75 + eval instant at 0m clamp_max(clamp_min(test_clamp, -20), 70) {src="clamp-a"} -20 {src="clamp-b"} 0 @@ -398,6 +403,36 @@ eval instant at 0m clamp_max((clamp_min(test_clamp, (-20))), (70)) {src="clamp-b"} 0 {src="clamp-c"} 70 +eval instant at 0m clamp(test_clamp, 0, NaN) + {src="clamp-a"} NaN + {src="clamp-b"} NaN + {src="clamp-c"} NaN + +eval instant at 0m clamp(test_clamp, NaN, 0) + {src="clamp-a"} NaN + {src="clamp-b"} NaN + {src="clamp-c"} NaN + +eval instant at 0m clamp(test_clamp, 5, -5) + +# Test cases for sgn. +clear +load 5m + test_sgn{src="sgn-a"} -Inf + test_sgn{src="sgn-b"} Inf + test_sgn{src="sgn-c"} NaN + test_sgn{src="sgn-d"} -50 + test_sgn{src="sgn-e"} 0 + test_sgn{src="sgn-f"} 100 + +eval instant at 0m sgn(test_sgn) + {src="sgn-a"} -1 + {src="sgn-b"} 1 + {src="sgn-c"} NaN + {src="sgn-d"} -1 + {src="sgn-e"} 0 + {src="sgn-f"} 1 + # Tests for sort/sort_desc. clear @@ -745,6 +780,13 @@ eval instant at 1m max_over_time(data[1m]) {type="some_nan3"} 1 {type="only_nan"} NaN +eval instant at 1m last_over_time(data[1m]) + data{type="numbers"} 3 + data{type="some_nan"} NaN + data{type="some_nan2"} 1 + data{type="some_nan3"} 1 + data{type="only_nan"} NaN + clear # Test for absent()