From a5fa9431d8dd7b692329806131d4170ef6f28899 Mon Sep 17 00:00:00 2001 From: Michael Hoffmann Date: Thu, 12 Jun 2025 10:49:39 +0000 Subject: [PATCH] promql: add ts_of_(max,min,last)_over_time functions This commit adds the ts_of_(max,min,last)_over_time functions behind the experimental feature flag. Signed-off-by: Michael Hoffmann --- docs/querying/functions.md | 6 +++ promql/functions.go | 48 +++++++++++++++++-- promql/parser/functions.go | 18 +++++++ promql/promqltest/testdata/functions.test | 22 +++++++++ .../src/complete/promql.terms.ts | 18 +++++++ .../src/parser/parser.test.ts | 15 ++++++ .../codemirror-promql/src/types/function.ts | 21 ++++++++ web/ui/module/lezer-promql/src/promql.grammar | 6 +++ 8 files changed, 151 insertions(+), 3 deletions(-) diff --git a/docs/querying/functions.md b/docs/querying/functions.md index 4a141b9220..d895207da6 100644 --- a/docs/querying/functions.md +++ b/docs/querying/functions.md @@ -847,6 +847,12 @@ additional functions are available: * `mad_over_time(range-vector)`: the median absolute deviation of all float samples in the specified interval. +* `ts_of_min_over_time(range-vector)`: the timestamp of the last float sample + that has the minimum value of all float samples in the specified interval. +* `ts_of_max_over_time(range-vector)`: the timestamp of the last float sample + that has the maximum value of all float samples in the specified interval. +* `ts_of_last_over_time(range-vector)`: the timestamp of last sample in the + 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/functions.go b/promql/functions.go index 4eb6dfb65e..786e5082e5 100644 --- a/promql/functions.go +++ b/promql/functions.go @@ -784,8 +784,42 @@ func funcMadOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNode }), annos } +// === ts_of_last_over_time(Matrix parser.ValueTypeMatrix) (Vector, Notes) === +func funcTsOfLastOverTime(vals []parser.Value, _ parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + el := vals[0].(Matrix)[0] + + var tf int64 + if len(el.Floats) > 0 { + tf = el.Floats[len(el.Floats)-1].T + } + + var th int64 + if len(el.Histograms) > 0 { + th = el.Floats[len(el.Floats)-1].T + } + + return append(enh.Out, Sample{ + Metric: el.Metric, + F: float64(max(tf, th)) / 1000, + }), nil +} + +// === ts_of_max_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) === +func funcTsOfMaxOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return compareOverTime(vals, args, enh, func(cur, maxVal float64) bool { + return (cur >= maxVal) || math.IsNaN(maxVal) + }, true) +} + +// === ts_of_min_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) === +func funcTsOfMinOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return compareOverTime(vals, args, enh, func(cur, maxVal float64) bool { + return (cur <= maxVal) || math.IsNaN(maxVal) + }, true) +} + // compareOverTime is a helper used by funcMaxOverTime and funcMinOverTime. -func compareOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper, compareFn func(float64, float64) bool) (Vector, annotations.Annotations) { +func compareOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper, compareFn func(float64, float64) bool, returnTimestamp bool) (Vector, annotations.Annotations) { samples := vals[0].(Matrix)[0] var annos annotations.Annotations if len(samples.Floats) == 0 { @@ -797,11 +831,16 @@ func compareOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNode } return aggrOverTime(vals, enh, func(s Series) float64 { maxVal := s.Floats[0].F + tsOfMax := s.Floats[0].T for _, f := range s.Floats { if compareFn(f.F, maxVal) { maxVal = f.F + tsOfMax = f.T } } + if returnTimestamp { + return float64(tsOfMax) / 1000 + } return maxVal }), annos } @@ -810,14 +849,14 @@ func compareOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNode func funcMaxOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { return compareOverTime(vals, args, enh, func(cur, maxVal float64) bool { return (cur > maxVal) || math.IsNaN(maxVal) - }) + }, false) } // === min_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) === func funcMinOverTime(vals []parser.Value, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { return compareOverTime(vals, args, enh, func(cur, maxVal float64) bool { return (cur < maxVal) || math.IsNaN(maxVal) - }) + }, false) } // === sum_over_time(Matrix parser.ValueTypeMatrix) (Vector, Annotations) === @@ -1723,6 +1762,9 @@ var FunctionCalls = map[string]FunctionCall{ "mad_over_time": funcMadOverTime, "max_over_time": funcMaxOverTime, "min_over_time": funcMinOverTime, + "ts_of_last_over_time": funcTsOfLastOverTime, + "ts_of_max_over_time": funcTsOfMaxOverTime, + "ts_of_min_over_time": funcTsOfMinOverTime, "minute": funcMinute, "month": funcMonth, "pi": funcPi, diff --git a/promql/parser/functions.go b/promql/parser/functions.go index aa65aca275..dfb181833f 100644 --- a/promql/parser/functions.go +++ b/promql/parser/functions.go @@ -283,6 +283,24 @@ var Functions = map[string]*Function{ ArgTypes: []ValueType{ValueTypeMatrix}, ReturnType: ValueTypeVector, }, + "ts_of_max_over_time": { + Name: "ts_of_max_over_time", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + Experimental: true, + }, + "ts_of_min_over_time": { + Name: "ts_of_min_over_time", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + Experimental: true, + }, + "ts_of_last_over_time": { + Name: "ts_of_last_over_time", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + Experimental: true, + }, "minute": { Name: "minute", ArgTypes: []ValueType{ValueTypeVector}, diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 1d7b7ed2d4..c367e7cc91 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -1186,6 +1186,28 @@ eval instant at 70s mad_over_time(metric_histogram{type="only_histogram"}[70s]) eval_info instant at 70s mad_over_time(metric_histogram{type="mix"}[70s]) {type="mix"} 0 +# Tests for ts_of_max_over_time and ts_of_min_over_time. Using odd scrape interval to test for rounding bugs. +clear +load 10s53ms + metric 1 2 3 0 5 6 2 1 4 + +eval instant at 90s ts_of_min_over_time(metric[90s]) + {} 30.159 + +eval instant at 90s ts_of_max_over_time(metric[90s]) + {} 50.265 + +# Tests for ts_of_last_over_time. Using odd load interval to test for rounding bugs. +clear +load 10s53ms + metric 1 2 3 _ _ + +eval instant at 90s ts_of_last_over_time(metric[90s]) + {} 20.106 + +eval instant at 95s ts_of_last_over_time(metric[90s]) + {} 20.106 + # Tests for quantile_over_time clear diff --git a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts index e2a1441fbc..2da92e034c 100644 --- a/web/ui/module/codemirror-promql/src/complete/promql.terms.ts +++ b/web/ui/module/codemirror-promql/src/complete/promql.terms.ts @@ -347,6 +347,24 @@ export const functionIdentifierTerms = [ info: 'Return the minimum value over time for input series', type: 'function', }, + { + label: 'ts_of_max_over_time', + detail: 'function', + info: 'Return the timestamp of the maximum value over time for input series', + type: 'function', + }, + { + label: 'ts_of_min_over_time', + detail: 'function', + info: 'Return the timestamp of the minimum value over time for input series', + type: 'function', + }, + { + label: 'ts_of_last_over_time', + detail: 'function', + info: 'Return the timestamp of the last value over time for input series', + type: 'function', + }, { label: 'minute', detail: 'function', diff --git a/web/ui/module/codemirror-promql/src/parser/parser.test.ts b/web/ui/module/codemirror-promql/src/parser/parser.test.ts index 74554a45e2..060aa2b18a 100644 --- a/web/ui/module/codemirror-promql/src/parser/parser.test.ts +++ b/web/ui/module/codemirror-promql/src/parser/parser.test.ts @@ -105,6 +105,21 @@ describe('promql operations', () => { expectedValueType: ValueType.vector, expectedDiag: [] as Diagnostic[], }, + { + expr: 'ts_of_max_over_time(rate(metric_name[5m])[1h:] offset 1m)', + expectedValueType: ValueType.vector, + expectedDiag: [] as Diagnostic[], + }, + { + expr: 'ts_of_min_over_time(rate(metric_name[5m])[1h:] offset 1m)', + expectedValueType: ValueType.vector, + expectedDiag: [] as Diagnostic[], + }, + { + expr: 'ts_of_last_over_time(rate(metric_name[5m])[1h:] offset 1m)', + expectedValueType: ValueType.vector, + expectedDiag: [] as Diagnostic[], + }, { expr: 'foo * bar', expectedValueType: ValueType.vector, diff --git a/web/ui/module/codemirror-promql/src/types/function.ts b/web/ui/module/codemirror-promql/src/types/function.ts index 56d5904121..d749220a76 100644 --- a/web/ui/module/codemirror-promql/src/types/function.ts +++ b/web/ui/module/codemirror-promql/src/types/function.ts @@ -61,6 +61,9 @@ import { MadOverTime, MaxOverTime, MinOverTime, + TsOfMaxOverTime, + TsOfMinOverTime, + TsOfLastOverTime, Minute, Month, Pi, @@ -403,6 +406,24 @@ const promqlFunctions: { [key: number]: PromQLFunction } = { variadic: 0, returnType: ValueType.vector, }, + [TsOfMaxOverTime]: { + name: 'ts_of_max_over_time', + argTypes: [ValueType.matrix], + variadic: 0, + returnType: ValueType.vector, + }, + [TsOfMinOverTime]: { + name: 'ts_of_min_over_time', + argTypes: [ValueType.matrix], + variadic: 0, + returnType: ValueType.vector, + }, + [TsOfLastOverTime]: { + name: 'ts_of_last_over_time', + argTypes: [ValueType.matrix], + variadic: 0, + returnType: ValueType.vector, + }, [Minute]: { name: 'minute', argTypes: [ValueType.vector], diff --git a/web/ui/module/lezer-promql/src/promql.grammar b/web/ui/module/lezer-promql/src/promql.grammar index f8850fc2b0..1daed63503 100644 --- a/web/ui/module/lezer-promql/src/promql.grammar +++ b/web/ui/module/lezer-promql/src/promql.grammar @@ -156,6 +156,9 @@ FunctionIdentifier { MadOverTime | MaxOverTime | MinOverTime | + TsOfMaxOverTime | + TsOfMinOverTime | + TsOfLastOverTime | Minute | Month | Pi | @@ -404,6 +407,9 @@ NumberDurationLiteralInDurationContext { MadOverTime { condFn<"mad_over_time"> } MaxOverTime { condFn<"max_over_time"> } MinOverTime { condFn<"min_over_time"> } + TsOfMaxOverTime { condFn<"ts_of_max_over_time"> } + TsOfMinOverTime { condFn<"ts_of_min_over_time"> } + TsOfLastOverTime { condFn<"ts_of_last_over_time"> } Minute { condFn<"minute"> } Month { condFn<"month"> } Pi { condFn<"pi">}