Merge pull request #16722 from MichaHoffmann/mhoffmann/add-tmin-tmax-tlast-over-time-functions

promql: add ts_of_(min|max|last)_over_time
This commit is contained in:
Björn Rabenstein 2025-06-12 19:40:18 +02:00 committed by GitHub
commit 4aee718013
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 151 additions and 3 deletions

View File

@ -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.

View File

@ -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,

View File

@ -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},

View File

@ -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

View File

@ -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',

View File

@ -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,

View File

@ -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],

View File

@ -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">}