From 8d284e565a7fdf92311d8f27f28f0fba9b8193bc Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Wed, 8 Jul 2026 21:58:41 -0700 Subject: [PATCH 01/14] Port xrate/yrate family and REPLACE_RATE_FUNCS to Prometheus 3.13. Re-introduce ExtRange engine support and the x/y rate function implementations so staging can eventually move to 3.x with the same rate semantics as 2.55.1. Co-authored-by: Cursor --- promql/engine.go | 87 ++++- promql/parser/functions.go | 39 +++ promql/promqltest/testdata/functions.test | 370 ++++++++++++++++++++++ promql/replace_rate_funcs_test.go | 35 ++ promql/xrate_yrate_funcs.go | 284 +++++++++++++++++ 5 files changed, 799 insertions(+), 16 deletions(-) create mode 100644 promql/replace_rate_funcs_test.go create mode 100644 promql/xrate_yrate_funcs.go diff --git a/promql/engine.go b/promql/engine.go index fad85ff2c68..01977bbded1 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1026,6 +1026,11 @@ func getTimeRangesForSelector(s *parser.EvalStmt, n *parser.VectorSelector, path start -= offsetMilliseconds end -= offsetMilliseconds + f, ok := parser.Functions[extractFuncFromPath(path)] + if ok && f.ExtRange { + start -= durationMilliseconds(s.LookbackDelta) + } + return start, end } @@ -1065,6 +1070,13 @@ func (ng *Engine) populateSeries(ctx context.Context, querier storage.Querier, s } evalRange = 0 hints.By, hints.Grouping = extractGroupsFromPath(path) + // Include an extra lookbackDelta iff this is the argument to an + // extended range function. Extended ranges include one extra + // point, this is how far back we need to look for it. + f, ok := parser.Functions[hints.Func] + if ok && f.ExtRange { + hints.Start -= durationMilliseconds(ng.lookbackDelta) + } n.UnexpandedSeriesSet = querier.Select(ctx, false, hints, n.LabelMatchers...) case *parser.MatrixSelector: evalRange = n.Range @@ -1755,7 +1767,7 @@ func (ev *evaluator) smoothSeries(series []storage.Series, offset time.Duration, matrixStart := dataTS - lb matrixEnd := dataTS + lb - floats, hists, _ = ev.matrixIterSlice(it, matrixStart, matrixEnd, floats, hists, nil) + floats, hists, _ = ev.matrixIterSlice(it, matrixStart, matrixEnd, false, floats, hists, nil) if len(floats) == 0 && len(hists) == 0 { continue } @@ -2236,6 +2248,10 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value, case selVS.Smoothed: bufferRange += durationMilliseconds(2 * ev.lookbackDelta) } + if e.Func.ExtRange { + bufferRange += durationMilliseconds(ev.lookbackDelta) + stepRange += durationMilliseconds(ev.lookbackDelta) + } it := storage.NewBuffer(bufferRange) var chkIter chunkenc.Iterator @@ -2316,7 +2332,7 @@ func (ev *evaluator) eval(ctx context.Context, expr parser.Expr) (parser.Value, mint -= durationMilliseconds(ev.lookbackDelta) maxt += durationMilliseconds(ev.lookbackDelta) } - floats, histograms, startTimestamps = ev.matrixIterSlice(it, mint, maxt, floats, histograms, startTimestamps) + floats, histograms, startTimestamps = ev.matrixIterSlice(it, mint, maxt, e.Func.ExtRange, floats, histograms, startTimestamps) } if len(floats)+len(histograms) == 0 { continue @@ -2844,7 +2860,7 @@ func (ev *evaluator) matrixSelector(ctx context.Context, node *parser.MatrixSele Metric: series[i].Labels(), } - ss.Floats, ss.Histograms, _ = ev.matrixIterSlice(it, mint, maxt, nil, nil, nil) + ss.Floats, ss.Histograms, _ = ev.matrixIterSlice(it, mint, maxt, false, nil, nil, nil) switch { case vs.Anchored: if ss.Histograms != nil { @@ -2887,10 +2903,11 @@ func (ev *evaluator) matrixSelector(ctx context.Context, node *parser.MatrixSele // Typically this is accomplished by passing in either all empty slices or the // values returned by a previous call. func (ev *evaluator) matrixIterSlice( - it *storage.BufferedSeriesIterator, mint, maxt int64, + it *storage.BufferedSeriesIterator, mint, maxt int64, extRange bool, floats []FPoint, histograms []HPoint, startTimestamps *StartTimestamps, ) ([]FPoint, []HPoint, *StartTimestamps) { mintFloats, mintHistograms := mint, mint + extMint := mint - durationMilliseconds(ev.lookbackDelta) // First floats... if len(floats) > 0 && floats[len(floats)-1].T > mint { @@ -2900,7 +2917,17 @@ func (ev *evaluator) matrixIterSlice( // (b) the number of samples is relatively small. // so a linear search will be as fast as a binary search. var drop int - for drop = 0; floats[drop].T <= mint; drop++ { + if extRange { + // This is an argument to an extended range function: first go past mint. + for drop = 0; drop < len(floats) && floats[drop].T <= mint; drop++ { + } + // Then, go back one sample if within lookbackDelta of mint. + if drop > 0 && floats[drop-1].T >= extMint { + drop-- + } + } else { + for drop = 0; floats[drop].T <= mint; drop++ { + } } ev.currentSamples -= drop copy(floats, floats[drop:]) @@ -2976,6 +3003,7 @@ func (ev *evaluator) matrixIterSlice( } buf := it.Buffer() + appendedPointBeforeMint := len(floats) > 0 loop: for { switch buf.Next() { @@ -3013,19 +3041,46 @@ loop: if value.IsStaleNaN(f) { continue loop } - // Values in the buffer are guaranteed to be smaller than maxt. - if t > mintFloats { - ev.currentSamples++ - if ev.currentSamples > ev.maxSamples { - ev.error(ErrTooManySamples(env)) - } - if floats == nil { - floats = getFPointSlice(16) + if extRange { + // This is the argument to an extended range function: if any + // point exists at or before range start, add it and then keep + // replacing it with later points while not yet (strictly) + // inside the range. + if t > mintFloats || !appendedPointBeforeMint { + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + if floats == nil { + floats = getFPointSlice(16) + } + floats = append(floats, FPoint{T: t, F: f}) + appendedPointBeforeMint = true + + if startTimestamps != nil { + startTimestamps.Floats = append(startTimestamps.Floats, buf.AtST()) + } + } else if len(floats) > 0 { + floats[len(floats)-1] = FPoint{T: t, F: f} + if startTimestamps != nil { + startTimestamps.Floats[len(startTimestamps.Floats)-1] = buf.AtST() + } } - floats = append(floats, FPoint{T: t, F: f}) + } else { + // Values in the buffer are guaranteed to be smaller than maxt. + if t > mintFloats { + ev.currentSamples++ + if ev.currentSamples > ev.maxSamples { + ev.error(ErrTooManySamples(env)) + } + if floats == nil { + floats = getFPointSlice(16) + } + floats = append(floats, FPoint{T: t, F: f}) - if startTimestamps != nil { - startTimestamps.Floats = append(startTimestamps.Floats, buf.AtST()) + if startTimestamps != nil { + startTimestamps.Floats = append(startTimestamps.Floats, buf.AtST()) + } } } } diff --git a/promql/parser/functions.go b/promql/parser/functions.go index 3e34b717003..e5e1c3da2ff 100644 --- a/promql/parser/functions.go +++ b/promql/parser/functions.go @@ -21,6 +21,9 @@ type Function struct { Variadic int ReturnType ValueType Experimental bool + // ExtRange marks functions that need one extra sample before the range + // start (xrate/yrate family). + ExtRange bool } // Functions is a list of all functions supported by PromQL, including their types. @@ -495,6 +498,42 @@ var Functions = map[string]*Function{ ArgTypes: []ValueType{ValueTypeScalar}, ReturnType: ValueTypeVector, }, + "xdelta": { + Name: "xdelta", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + ExtRange: true, + }, + "xincrease": { + Name: "xincrease", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + ExtRange: true, + }, + "xrate": { + Name: "xrate", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + ExtRange: true, + }, + "ydelta": { + Name: "ydelta", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + ExtRange: true, + }, + "yincrease": { + Name: "yincrease", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + ExtRange: true, + }, + "yrate": { + Name: "yrate", + ArgTypes: []ValueType{ValueTypeMatrix}, + ReturnType: ValueTypeVector, + ExtRange: true, + }, "year": { Name: "year", ArgTypes: []ValueType{ValueTypeVector}, diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index fbe72022139..0ab767e797f 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -1,3 +1,373 @@ +# Comparison of rate vs xrate. + +load 5s + http_requests{path="/foo"} 1 1 1 2 2 2 2 2 3 3 3 + http_requests{path="/bar"} 1 2 3 4 5 6 7 8 9 10 11 + + +# +# Timeseries starts inside range, (presumably) goes on after range end. +# + +# 1. Reference eval, aligned with collection. +# Upstream rate() expectations vary by Prometheus major version; xrate/yrate +# behaviour is asserted below. + +eval instant at 25s xrate(http_requests[50s]) + {path="/foo"} .02 + {path="/bar"} .1 + +eval instant at 25s yrate(http_requests[50s]) + {path="/foo"} 0.04 + {path="/bar"} 0.12 + +# 2. Eval 1 second earlier compared to (1). +# * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase); +# * path="/bar" rate should be same or fractionally lower (80% the increase, 80/96% range covered by sample). +# XXX Seeing ~20% jump for path="/foo" +eval instant at 24s xrate(http_requests[50s]) + {path="/foo"} .02 + {path="/bar"} .08 + +eval instant at 24s yrate(http_requests[50s]) + {path="/foo"} 0.04 + {path="/bar"} 0.1 + +# 3. Eval 1 second later compared to (1). +# * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase). +# * path="/bar" rate should be same or fractionally lower ("longer" sample, same actual increase). +# XXX Higher instead of lower for both. +eval instant at 26s xrate(http_requests[50s]) + {path="/foo"} .02 + {path="/bar"} .1 + +eval instant at 26s yrate(http_requests[50s]) + {path="/foo"} 0.04 + {path="/bar"} 0.12 + + +# +# Timeseries starts before range, ends within range. +# + +# 4. Reference eval, aligned with collection. +eval instant at 75s xrate(http_requests[50s]) + {path="/foo"} .02 + {path="/bar"} .1 + +eval instant at 75s yrate(http_requests[50s]) + {path="/foo"} 0.02 + {path="/bar"} 0.1 + +# 5. Eval 1s earlier compared to (4). +# * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase). +# * path="/bar" rate should be same or fractionally lower ("longer" sample, same actual increase). +# XXX Higher instead of lower for both. +eval instant at 74s xrate(http_requests[50s]) + {path="/foo"} .02 + {path="/bar"} .12 + +eval instant at 74s yrate(http_requests[50s]) + {path="/foo"} 0.02 + {path="/bar"} 0.12 + +# 6. Eval 1s later compared to (4). Rate/increase (should be) fractionally smaller. +# * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase); +# * path="/bar" rate should be same or fractionally lower (80% the increase, 80/96% range covered by sample). +# XXX Seeing ~20% jump for path="/foo", decrease instead of increase for path="/bar". +eval instant at 76s xrate(http_requests[50s]) + {path="/foo"} .02 + {path="/bar"} .1 + +eval instant at 76s yrate(http_requests[50s]) + {path="/foo"} 0.02 + {path="/bar"} 0.1 + +# +# Evaluation of 10 second rate every 10 seconds, not aligned with collection. +# + +eval instant at 9s xrate(http_requests[10s]) + {path="/foo"} 0 + {path="/bar"} 0.1 + +eval instant at 19s xrate(http_requests[10s]) + {path="/foo"} 0.1 + {path="/bar"} 0.2 + +eval instant at 29s xrate(http_requests[10s]) + {path="/foo"} 0 + {path="/bar"} 0.2 + +eval instant at 39s xrate(http_requests[10s]) + {path="/foo"} 0 + {path="/bar"} 0.2 + +# XXX Sees the increase in path="/foo" between timestamps 35 and 40. +eval instant at 49s xrate(http_requests[10s]) + {path="/foo"} .1 + {path="/bar"} 0.2 + +clear + +# Tests for increase()/xincrease()/yincrease()/xrate()/yrate(). +# +# The counters start at 1000/2000 so yincrease/yrate (which treat every +# pre-origin value as 0) return wildly different results from the +# xrate / rate family (which only consider deltas inside the range). +# +# Eval times are 49s/48s rather than 50s/47s so that sample timestamps +# land strictly inside the range [start, end) rather than at its +# boundaries; this keeps the pre-range sample accessible to matrixIterSlice +# and makes yincrease's "counter-at-rangeStart" value observable. +load 5s + http_requests{path="/foo"} 1000+10x10 + http_requests{path="/bar"} 2000+10x5 5+10x4 + +# Tests for increase() (standard Prometheus, for reference). +eval instant at 49s increase(http_requests[50s]) + {path="/foo"} 100 + {path="/bar"} 94.44444444444444 + +eval instant at 49s increase(http_requests[100s]) + {path="/foo"} 103 + {path="/bar"} 97.27777777777777 + +# Tests for xincrease(). +eval instant at 49s xincrease(http_requests[50s]) + {path="/foo"} 90 + {path="/bar"} 85 + +eval instant at 49s xincrease(http_requests[100s]) + {path="/foo"} 90 + {path="/bar"} 85 + +eval instant at 49s xincrease(http_requests[5s]) + {path="/foo"} 10 + {path="/bar"} 10 + +eval instant at 49s xincrease(http_requests[3s]) + +eval instant at 48s xincrease(http_requests[3s]) + +# Tests for yincrease(). yrate always compares to a pre-origin of 0, +# so yincrease sees the full 1000/2000 offset in the first range. +eval instant at 49s yincrease(http_requests[50s]) + {path="/foo"} 1090 + {path="/bar"} 2085 + +eval instant at 49s yincrease(http_requests[100s]) + {path="/foo"} 1090 + {path="/bar"} 2085 + +eval instant at 49s yincrease(http_requests[5s]) + {path="/foo"} 10 + {path="/bar"} 10 + +eval instant at 49s yincrease(http_requests[3s]) + {path="/foo"} 0 + {path="/bar"} 0 + +# Tests for xrate(). +eval instant at 49s xrate(http_requests[50s]) + {path="/foo"} 1.8 + {path="/bar"} 1.7 + +eval instant at 49s xrate(http_requests[100s]) + {path="/foo"} 0.9 + {path="/bar"} 0.85 + +eval instant at 49s xrate(http_requests[5s]) + {path="/foo"} 2 + {path="/bar"} 2 + +eval instant at 49s xrate(http_requests[3s]) + +eval instant at 48s xrate(http_requests[3s]) + +# Tests for yrate(). +eval instant at 49s yrate(http_requests[50s]) + {path="/foo"} 21.8 + {path="/bar"} 41.7 + +eval instant at 49s yrate(http_requests[100s]) + {path="/foo"} 10.9 + {path="/bar"} 20.85 + +eval instant at 49s yrate(http_requests[5s]) + {path="/foo"} 2 + {path="/bar"} 2 + +eval instant at 49s yrate(http_requests[3s]) + {path="/foo"} 0 + {path="/bar"} 0 + +clear + +# Test for increase()/xincrease()/yincrease() with counter reset. +# When the counter is reset, it always starts at 0. +# So the sequence 1006 4 (decreasing counter = reset) is interpreted the +# same as 1006 0 1 2 3 4. Prometheus assumes it missed the intermediate +# values 0, 1, 2, 3. +load 5m + http_requests{path="/foo"} 1000 1001 1003 1006 4 9 16 + +eval instant at 29m increase(http_requests[30m]) + {path="/foo"} 18 + +eval instant at 29m xincrease(http_requests[30m]) + {path="/foo"} 15 + +eval instant at 29m yincrease(http_requests[30m]) + {path="/foo"} 1015 + +# Test counter reset inside the range, not spanning the range boundary. +eval instant at 19m xincrease(http_requests[5m]) + {path="/foo"} 3 + +eval instant at 19m yincrease(http_requests[5m]) + {path="/foo"} 3 + +eval instant at 19m xincrease(http_requests[10m]) + {path="/foo"} 5 + +eval instant at 19m yincrease(http_requests[10m]) + {path="/foo"} 5 + +eval instant at 24m xincrease(http_requests[5m]) + {path="/foo"} 4 + +eval instant at 24m yincrease(http_requests[5m]) + {path="/foo"} 4 + +eval instant at 24m xincrease(http_requests[10m]) + {path="/foo"} 7 + +eval instant at 24m yincrease(http_requests[10m]) + {path="/foo"} 7 + +clear + +# Tests for delta()/xdelta(). +load 5m + http_requests{path="/foo"} 0 50 300 150 200 + http_requests{path="/bar"} 200 150 300 50 0 + +eval instant at 20m delta(http_requests[20m]) + {path="/foo"} 200 + {path="/bar"} -200 + +eval instant at 20m xdelta(http_requests[20m]) + {path="/foo"} 200 + {path="/bar"} -200 + +eval instant at 20m xdelta(http_requests[19m]) + {path="/foo"} 190 + {path="/bar"} -190 + +eval instant at 20m xdelta(http_requests[1m]) + {path="/foo"} 10 + {path="/bar"} -10 + +clear + +# Tests for ydelta(). +# ydelta extends the value of the sample preceding rangeStart across +# every gap, so the answer is simply last-in-range minus +# last-before-range (no counter-reset correction: ydelta does not treat +# the series as a counter). +load 5m + http_requests{path="/foo"} 1 2 3 4 5 6 7 + http_requests{path="/bar"} 11 9 7 5 3 1 0 + +eval instant at 29m delta(http_requests[30m]) + {path="/foo"} 6 + {path="/bar"} -12 + +eval instant at 29m xdelta(http_requests[30m]) + {path="/foo"} 5 + {path="/bar"} -10 + +eval instant at 29m ydelta(http_requests[30m]) + {path="/foo"} 5 + {path="/bar"} -10 + +eval instant at 29m ydelta(http_requests[25m]) + {path="/foo"} 5 + {path="/bar"} -10 + +eval instant at 29m ydelta(http_requests[5m]) + {path="/foo"} 1 + {path="/bar"} -2 + +clear + +# Additivity invariant for the yrate/yincrease/ydelta family. +# +# These functions are additive over adjacent ranges -- which is what makes them +# composable across any partitioning of a wider range into contiguous sub-ranges. +# Because they evaluate over a half-open range (left-inclusive on the 2.53/2.55 +# add-yrate line, right-inclusive after align-yrate-to-3x-range-boundary), two +# adjacent windows partition a wider one without double-counting any sample. +# For any three timestamps T_0 < T_1 < T_2 and range durations r_1 = T_1 - T_0, +# r_2 = T_2 - T_1: +# +# yincrease(m[r_1]) @ T_1 + yincrease(m[r_2]) @ T_2 == yincrease(m[r_1 + r_2]) @ T_2 +# +# Each scenario below picks T_0, T_1, T_2 off-cadence (no sample lands on a range +# boundary) so the expected values are identical under both boundary conventions; +# this block should cherry-pick cleanly across the yrate branch stack. + +# Scenario 1: uniform counter, no resets. T_0=5s, T_1=35s, T_2=75s. +load 10s + additivity_uniform{job="api"} 0+10x10 + +eval instant at 35s yincrease(additivity_uniform[30s]) + {job="api"} 30 + +eval instant at 75s yincrease(additivity_uniform[40s]) + {job="api"} 40 + +eval instant at 75s yincrease(additivity_uniform[70s]) + {job="api"} 70 +# 30 + 40 == 70 + +clear + +# Scenario 2: counter reset in the earlier window. +# Reset between t=40s (value 40) and t=50s (value 0). T_0=5s, T_1=65s, T_2=95s. +load 10s + additivity_reset_early{job="api"} 0 10 20 30 40 0 10 20 30 40 50 + +eval instant at 65s yincrease(additivity_reset_early[60s]) + {job="api"} 50 + +eval instant at 95s yincrease(additivity_reset_early[30s]) + {job="api"} 30 + +eval instant at 95s yincrease(additivity_reset_early[90s]) + {job="api"} 80 +# 50 + 30 == 80 + +clear + +# Scenario 3: counter reset in the later window. +# Reset between t=60s (value 60) and t=70s (value 0). T_0=5s, T_1=45s, T_2=95s. +load 10s + additivity_reset_late{job="api"} 0 10 20 30 40 50 60 0 10 20 30 + +eval instant at 45s yincrease(additivity_reset_late[40s]) + {job="api"} 40 + +eval instant at 95s yincrease(additivity_reset_late[50s]) + {job="api"} 40 + +eval instant at 95s yincrease(additivity_reset_late[90s]) + {job="api"} 80 +# 40 + 40 == 80 + +clear + # Testdata for resets() and changes(). load 5m http_requests{path="/foo"} 1 2 3 0 1 0 0 1 2 0 diff --git a/promql/replace_rate_funcs_test.go b/promql/replace_rate_funcs_test.go new file mode 100644 index 00000000000..19012a36b92 --- /dev/null +++ b/promql/replace_rate_funcs_test.go @@ -0,0 +1,35 @@ +package promql + +import ( + "os" + "os/exec" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/prometheus/prometheus/promql/parser" +) + +func TestReplaceRateFuncs2(t *testing.T) { + if os.Getenv("PROMQL_TEST_REPLACE_RATE_FUNCS") != "1" { + cmd := exec.Command(os.Args[0], "-test.run=^TestReplaceRateFuncs2$") + cmd.Env = append(os.Environ(), + "REPLACE_RATE_FUNCS=2", + "PROMQL_TEST_REPLACE_RATE_FUNCS=1", + ) + out, err := cmd.CombinedOutput() + require.NoError(t, err, "subprocess failed:\n%s", out) + return + } + + require.NotNil(t, parser.Functions["rate"]) + require.NotNil(t, parser.Functions["yrate"]) + require.NotNil(t, parser.Functions["_rate"]) + require.NotNil(t, parser.Functions["xrate"]) + require.Equal(t, "rate", parser.Functions["rate"].Name) + require.Equal(t, "yrate", parser.Functions["yrate"].Name) + require.Equal(t, "_rate", parser.Functions["_rate"].Name) + require.True(t, rateFuncPointersEqual(FunctionCalls["rate"], FunctionCalls["yrate"])) + require.False(t, rateFuncPointersEqual(FunctionCalls["_rate"], FunctionCalls["rate"])) + require.False(t, rateFuncPointersEqual(FunctionCalls["rate"], FunctionCalls["xrate"])) +} diff --git a/promql/xrate_yrate_funcs.go b/promql/xrate_yrate_funcs.go new file mode 100644 index 00000000000..b72e1bbb26a --- /dev/null +++ b/promql/xrate_yrate_funcs.go @@ -0,0 +1,284 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promql + +import ( + "fmt" + "os" + "reflect" + + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +// preRangeExtrapolation is a utility function for xrate/xincrease/xdelta. +// It calculates the rate (allowing for counter resets if isCounter is true), +// taking into account the last sample before the range start, and returns +// the result as either per-second (if isRate is true) or overall. +// +// Do not confuse with extendedRate(), which implements anchored/smoothed +// selectors in upstream Prometheus 3.x. +func preRangeExtrapolation(matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper, isCounter, isRate bool) (Vector, annotations.Annotations) { + ms := args[0].(*parser.MatrixSelector) + vs := ms.VectorSelector.(*parser.VectorSelector) + + var ( + samples = matrixVals[0] + rangeStart = enh.Ts - durationMilliseconds(ms.Range+vs.Offset) + rangeEnd = enh.Ts - durationMilliseconds(vs.Offset) + ) + + points := samples.Floats + if len(points) < 2 { + return enh.Out, nil + } + sampledRange := float64(points[len(points)-1].T - points[0].T) + averageInterval := sampledRange / float64(len(points)-1) + + firstPoint := 0 + // If the point before the range is too far from rangeStart, drop it. + if float64(rangeStart-points[0].T) > averageInterval { + if len(points) < 3 { + return enh.Out, nil + } + firstPoint = 1 + sampledRange = float64(points[len(points)-1].T - points[firstPoint].T) + averageInterval = sampledRange / float64(len(points)-2) + } + + var ( + counterCorrection float64 + lastValue float64 + ) + if isCounter { + for i := firstPoint; i < len(points); i++ { + sample := points[i] + if sample.F < lastValue { + counterCorrection += lastValue + } + lastValue = sample.F + } + } + resultValue := points[len(points)-1].F - points[firstPoint].F + counterCorrection + + // Duration between last sample and boundary of range. + durationToEnd := float64(rangeEnd - points[len(points)-1].T) + + // If the points cover the whole range (i.e. they start just before the + // range start and end just before the range end) adjust the value from + // the sampled range to the requested range. + if points[firstPoint].T <= rangeStart && durationToEnd < averageInterval { + adjustToRange := float64(durationMilliseconds(ms.Range)) + resultValue *= (adjustToRange / sampledRange) + } + + if isRate { + resultValue /= ms.Range.Seconds() + } + + return append(enh.Out, Sample{F: resultValue}), nil +} + +// yIncrease is a utility function for yincrease/yrate/ydelta. +// It calculates the increase of the range (allowing for counter resets if isCounter is true), +// taking into account the sample at the end of the previous range (just before rangeStartMsec). +// It returns the result across the range (rangeStartMsec, rangeEndMsec]. The left-open, +// right-closed convention matches the Prometheus 3.x range-selector semantics +// (see prometheus/prometheus#13213) so that a sample whose timestamp lands exactly on +// a range boundary is attributed to the later range, never to both or neither. +// It always extends the preceding sample's value until the next sample, including the +// unwritten origin sample value at the start of every time series. +// +// It is additive over adjacent periods, and therefore composable across any +// partitioning of a wider range into contiguous sub-ranges. For adjacent periods +// p0 and p1 ("adjacent" means p0's rangeEndMsec == p1's rangeStartMsec): +// +// yIncrease(p0) + yIncrease(p1) == yIncrease(p0 + p1) +func yIncrease(points []FPoint, rangeStartMsec, rangeEndMsec int64, isCounter bool) float64 { + var lastBeforeRange, lastInRange, inRangeRestartSkew float64 + + if !isCounter && len(points) > 0 { + lastBeforeRange = points[0].F // Gauges don't start at 0. + } + + // The points are in time order, so we can just walk the list once and remember the last values + // seen "before" and "in" range. If there are no values in range, we use the last value before range + // so that the increase is 0. + for i := 0; i < len(points) && points[i].T <= rangeEndMsec; i++ { // Only consider points in (rangeStartMsec, rangeEndMsec]. + if points[i].T > rangeStartMsec { + if isCounter && points[i].F < lastInRange { // Counter reset (process restart). + inRangeRestartSkew += lastInRange + } + } else { + lastBeforeRange = points[i].F + } + lastInRange = points[i].F + } + + return lastInRange - lastBeforeRange + inRangeRestartSkew +} + +// rangeFromSelectors extracts points, rangeStartMsec, rangeEndMsec, and rangeSeconds +// from the common (Matrix, MatrixSelector) arguments supplied to yincrease/yrate/ydelta. +// The range is (rangeStartMsec, rangeEndMsec]. That is, every sample in range has the property: +// rangeStartMsec < sample.T <= rangeEndMsec. +func rangeFromSelectors(matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) ([]FPoint, int64, int64, float64) { + ms := args[0].(*parser.MatrixSelector) + vs := ms.VectorSelector.(*parser.VectorSelector) + + rangeStartMsec := enh.Ts - durationMilliseconds(ms.Range+vs.Offset) + rangeEndMsec := enh.Ts - durationMilliseconds(vs.Offset) + + points := matrixVals[0].Floats + + return points, rangeStartMsec, rangeEndMsec, ms.Range.Seconds() +} + +func funcXdelta(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return preRangeExtrapolation(matrixVals, args, enh, false, false) +} + +func funcXrate(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return preRangeExtrapolation(matrixVals, args, enh, true, true) +} + +func funcXincrease(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return preRangeExtrapolation(matrixVals, args, enh, true, false) +} + +func funcYdelta(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + points, rangeStartMsec, rangeEndMsec, _ := rangeFromSelectors(matrixVals, args, enh) + value := yIncrease(points, rangeStartMsec, rangeEndMsec, false) + return append(enh.Out, Sample{F: value}), nil +} + +func funcYincrease(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + points, rangeStartMsec, rangeEndMsec, _ := rangeFromSelectors(matrixVals, args, enh) + value := yIncrease(points, rangeStartMsec, rangeEndMsec, true) + return append(enh.Out, Sample{F: value}), nil +} + +func funcYrate(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + points, rangeStartMsec, rangeEndMsec, rangeSeconds := rangeFromSelectors(matrixVals, args, enh) + value := yIncrease(points, rangeStartMsec, rangeEndMsec, true) / rangeSeconds + return append(enh.Out, Sample{F: value}), nil +} + +func init() { + FunctionCalls["xdelta"] = funcXdelta + FunctionCalls["xincrease"] = funcXincrease + FunctionCalls["xrate"] = funcXrate + FunctionCalls["ydelta"] = funcYdelta + FunctionCalls["yincrease"] = funcYincrease + FunctionCalls["yrate"] = funcYrate + + // REPLACE_RATE_FUNCS lets operators swap the built-in rate extrapolation + // functions with the xrate or yrate family at process start, so + // Grafana auto-completion, Prometheus tooling, Thanos, etc. continue to + // work against queries that call the standard rate/increase/delta names. + // + // Values: + // "1" - replace rate/increase/delta with xrate/xincrease/xdelta + // AND remove the x* names (legacy behaviour). + // "x", "X" - point rate/increase/delta at xrate/xincrease/xdelta; + // keep the x* names; preserve upstream implementations as + // _rate/_increase/_delta. + // "2", - point rate/increase/delta at yrate/yincrease/ydelta; + // "y", "Y" keep the y* (and x*) names; preserve upstream + // implementations as _rate/_increase/_delta. + switch os.Getenv("REPLACE_RATE_FUNCS") { + case "1": + FunctionCalls["delta"] = FunctionCalls["xdelta"] + FunctionCalls["increase"] = FunctionCalls["xincrease"] + FunctionCalls["rate"] = FunctionCalls["xrate"] + delete(FunctionCalls, "xdelta") + delete(FunctionCalls, "xincrease") + delete(FunctionCalls, "xrate") + + parser.Functions["delta"] = parser.Functions["xdelta"] + parser.Functions["increase"] = parser.Functions["xincrease"] + parser.Functions["rate"] = parser.Functions["xrate"] + parser.Functions["delta"].Name = "delta" + parser.Functions["increase"].Name = "increase" + parser.Functions["rate"].Name = "rate" + delete(parser.Functions, "xdelta") + delete(parser.Functions, "xincrease") + delete(parser.Functions, "xrate") + fmt.Println("Successfully replaced rate & friends with xrate & friends (and removed xrate & friends function keys).") + + case "x", "X": + preserveOriginalRateFuncs() + repointParserFunctions("delta", "xdelta") + repointParserFunctions("increase", "xincrease") + repointParserFunctions("rate", "xrate") + repointFunction("delta", "xdelta") + repointFunction("increase", "xincrease") + repointFunction("rate", "xrate") + fmt.Println("Successfully replaced rate/increase/delta with xrate/xincrease/xdelta; originals available as _rate/_increase/_delta; x* names also available.") + + case "2", "y", "Y": + preserveOriginalRateFuncs() + repointParserFunctions("delta", "ydelta") + repointParserFunctions("increase", "yincrease") + repointParserFunctions("rate", "yrate") + repointFunction("delta", "ydelta") + repointFunction("increase", "yincrease") + repointFunction("rate", "yrate") + fmt.Println("Successfully replaced rate/increase/delta with yrate/yincrease/ydelta; originals available as _rate/_increase/_delta; y* and x* names also available.") + } +} + +// preserveOriginalRateFuncs copies the upstream rate/increase/delta parser and +// evaluator entries to _rate/_increase/_delta before repointing the standard +// names at the xrate or yrate family. +func preserveOriginalRateFuncs() { + copyParserFunction("delta", "_delta") + copyParserFunction("increase", "_increase") + copyParserFunction("rate", "_rate") + copyFunctionCall("delta", "_delta") + copyFunctionCall("increase", "_increase") + copyFunctionCall("rate", "_rate") +} + +func copyParserFunction(fromName, toName string) { + result := *parser.Functions[fromName] + result.Name = toName + parser.Functions[toName] = &result +} + +func copyFunctionCall(fromName, toName string) { + FunctionCalls[toName] = FunctionCalls[fromName] +} + +// repointParserFunctions makes name resolve to newName's implementation while +// keeping name as the displayed/parser function name. A copy is made so the +// newName entry is not mutated. +func repointParserFunctions(name, newName string) { + result := *parser.Functions[newName] + result.Name = name + parser.Functions[name] = &result +} + +// repointFunction makes the FunctionCalls entry for name dispatch to the +// implementation currently registered under newName. The newName entry +// is left in place. +func repointFunction(name, newName string) { + FunctionCalls[name] = FunctionCalls[newName] +} + +// rateFuncPointersEqual compares two FunctionCall implementations by function +// pointer. Used by tests only. +func rateFuncPointersEqual(a, b FunctionCall) bool { + return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer() +} From 5f2d3ea44412949742a0470a8cc1d3512db5da08 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 16:01:51 -0700 Subject: [PATCH 02/14] Use triangular showcase counter to expose boundary and origin bugs. Sync the rate-vs-xrate showcase /bar series with the 2.55 yrate stack: 11 points with deltas +1..+10 starting at 11, and refresh xrate/yrate expectations. Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 41 +++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 0ab767e797f..2c1bd228e2d 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -1,8 +1,13 @@ -# Comparison of rate vs xrate. +# Comparison of rate vs xrate vs yrate. +# +# /bar is an 11-point counter whose per-scrape delta grows by 1 (+1, +2, …, +10). +# It starts at 11 — one above the largest in-range step — so the "unwritten origin" +# offset (yrate baseline 0 → first sample 11) cannot be confused with any +k step +# inside the series (a plain 1 2 3 … ladder hides both off-by-one and origin bugs). load 5s http_requests{path="/foo"} 1 1 1 2 2 2 2 2 3 3 3 - http_requests{path="/bar"} 1 2 3 4 5 6 7 8 9 10 11 + http_requests{path="/bar"} 11 12 14 17 21 26 32 39 47 56 66 # @@ -15,11 +20,11 @@ load 5s eval instant at 25s xrate(http_requests[50s]) {path="/foo"} .02 - {path="/bar"} .1 + {path="/bar"} 0.3 eval instant at 25s yrate(http_requests[50s]) {path="/foo"} 0.04 - {path="/bar"} 0.12 + {path="/bar"} 0.52 # 2. Eval 1 second earlier compared to (1). # * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase); @@ -27,11 +32,11 @@ eval instant at 25s yrate(http_requests[50s]) # XXX Seeing ~20% jump for path="/foo" eval instant at 24s xrate(http_requests[50s]) {path="/foo"} .02 - {path="/bar"} .08 + {path="/bar"} 0.2 eval instant at 24s yrate(http_requests[50s]) {path="/foo"} 0.04 - {path="/bar"} 0.1 + {path="/bar"} 0.42 # 3. Eval 1 second later compared to (1). # * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase). @@ -39,11 +44,11 @@ eval instant at 24s yrate(http_requests[50s]) # XXX Higher instead of lower for both. eval instant at 26s xrate(http_requests[50s]) {path="/foo"} .02 - {path="/bar"} .1 + {path="/bar"} 0.3 eval instant at 26s yrate(http_requests[50s]) {path="/foo"} 0.04 - {path="/bar"} 0.12 + {path="/bar"} 0.52 # @@ -53,11 +58,11 @@ eval instant at 26s yrate(http_requests[50s]) # 4. Reference eval, aligned with collection. eval instant at 75s xrate(http_requests[50s]) {path="/foo"} .02 - {path="/bar"} .1 + {path="/bar"} 0.8 eval instant at 75s yrate(http_requests[50s]) {path="/foo"} 0.02 - {path="/bar"} 0.1 + {path="/bar"} 0.8 # 5. Eval 1s earlier compared to (4). # * path="/foo" rate should be same or fractionally lower ("longer" sample, same actual increase). @@ -65,11 +70,11 @@ eval instant at 75s yrate(http_requests[50s]) # XXX Higher instead of lower for both. eval instant at 74s xrate(http_requests[50s]) {path="/foo"} .02 - {path="/bar"} .12 + {path="/bar"} 0.9 eval instant at 74s yrate(http_requests[50s]) {path="/foo"} 0.02 - {path="/bar"} 0.12 + {path="/bar"} 0.9 # 6. Eval 1s later compared to (4). Rate/increase (should be) fractionally smaller. # * path="/foo" rate should be same or fractionally higher ("shorter" sample, same actual increase); @@ -77,11 +82,11 @@ eval instant at 74s yrate(http_requests[50s]) # XXX Seeing ~20% jump for path="/foo", decrease instead of increase for path="/bar". eval instant at 76s xrate(http_requests[50s]) {path="/foo"} .02 - {path="/bar"} .1 + {path="/bar"} 0.8 eval instant at 76s yrate(http_requests[50s]) {path="/foo"} 0.02 - {path="/bar"} 0.1 + {path="/bar"} 0.8 # # Evaluation of 10 second rate every 10 seconds, not aligned with collection. @@ -93,20 +98,20 @@ eval instant at 9s xrate(http_requests[10s]) eval instant at 19s xrate(http_requests[10s]) {path="/foo"} 0.1 - {path="/bar"} 0.2 + {path="/bar"} 0.5 eval instant at 29s xrate(http_requests[10s]) {path="/foo"} 0 - {path="/bar"} 0.2 + {path="/bar"} 0.9 eval instant at 39s xrate(http_requests[10s]) {path="/foo"} 0 - {path="/bar"} 0.2 + {path="/bar"} 1.3 # XXX Sees the increase in path="/foo" between timestamps 35 and 40. eval instant at 49s xrate(http_requests[10s]) {path="/foo"} .1 - {path="/bar"} 0.2 + {path="/bar"} 1.7 clear From a7f0ea3862e61761ca35e1628889a5c0fceb8246 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 16:29:22 -0700 Subject: [PATCH 03/14] Use incrementing-delta test series beyond the rate showcase. Replace linear additivity and ydelta fixtures with counters whose per-step delta grows by 1, starting at max_delta+1 so origin-offset bugs cannot hide behind a repeated +1 or +10 step. Refresh ydelta and yincrease expectations. Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 54 +++++++++++++---------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 2c1bd228e2d..11efe0d9ca6 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -281,28 +281,31 @@ clear # every gap, so the answer is simply last-in-range minus # last-before-range (no counter-reset correction: ydelta does not treat # the series as a counter). +# +# /foo uses the same incrementing-delta counter convention as the rate +# showcase (deltas +1..+6, start 7 = max step + 1). load 5m - http_requests{path="/foo"} 1 2 3 4 5 6 7 + http_requests{path="/foo"} 7 8 10 13 17 22 28 http_requests{path="/bar"} 11 9 7 5 3 1 0 eval instant at 29m delta(http_requests[30m]) - {path="/foo"} 6 + {path="/foo"} 18 {path="/bar"} -12 eval instant at 29m xdelta(http_requests[30m]) - {path="/foo"} 5 + {path="/foo"} 15 {path="/bar"} -10 eval instant at 29m ydelta(http_requests[30m]) - {path="/foo"} 5 + {path="/foo"} 15 {path="/bar"} -10 eval instant at 29m ydelta(http_requests[25m]) - {path="/foo"} 5 + {path="/foo"} 15 {path="/bar"} -10 eval instant at 29m ydelta(http_requests[5m]) - {path="/foo"} 1 + {path="/foo"} 5 {path="/bar"} -2 clear @@ -323,53 +326,56 @@ clear # boundary) so the expected values are identical under both boundary conventions; # this block should cherry-pick cleanly across the yrate branch stack. -# Scenario 1: uniform counter, no resets. T_0=5s, T_1=35s, T_2=75s. +# Scenario 1: counter with incrementing deltas, no resets. T_0=5s, T_1=35s, T_2=75s. +# Deltas +1..+9 at 10s scrape; start 10 (= max step + 1). load 10s - additivity_uniform{job="api"} 0+10x10 + additivity_uniform{job="api"} 10 11 13 16 20 25 31 38 46 55 eval instant at 35s yincrease(additivity_uniform[30s]) - {job="api"} 30 + {job="api"} 6 eval instant at 75s yincrease(additivity_uniform[40s]) - {job="api"} 40 + {job="api"} 22 eval instant at 75s yincrease(additivity_uniform[70s]) - {job="api"} 70 -# 30 + 40 == 70 + {job="api"} 28 +# 6 + 22 == 28 clear # Scenario 2: counter reset in the earlier window. -# Reset between t=40s (value 40) and t=50s (value 0). T_0=5s, T_1=65s, T_2=95s. +# Reset between t=40s (value 21) and t=50s (value 11). Deltas +1..+4 on +# each side; start 11 (= max step + 1). T_0=5s, T_1=65s, T_2=95s. load 10s - additivity_reset_early{job="api"} 0 10 20 30 40 0 10 20 30 40 50 + additivity_reset_early{job="api"} 11 12 14 17 21 11 12 14 17 21 eval instant at 65s yincrease(additivity_reset_early[60s]) - {job="api"} 50 + {job="api"} 22 eval instant at 95s yincrease(additivity_reset_early[30s]) - {job="api"} 30 + {job="api"} 9 eval instant at 95s yincrease(additivity_reset_early[90s]) - {job="api"} 80 -# 50 + 30 == 80 + {job="api"} 31 +# 22 + 9 == 31 clear # Scenario 3: counter reset in the later window. -# Reset between t=60s (value 60) and t=70s (value 0). T_0=5s, T_1=45s, T_2=95s. +# Reset between t=60s (value 29) and t=70s (value 11). Pre-reset deltas +# +1..+7 (start 8); post-reset +1..+3 (start 11). T_0=5s, T_1=45s, T_2=95s. load 10s - additivity_reset_late{job="api"} 0 10 20 30 40 50 60 0 10 20 30 + additivity_reset_late{job="api"} 8 9 11 14 18 23 29 11 12 14 17 eval instant at 45s yincrease(additivity_reset_late[40s]) - {job="api"} 40 + {job="api"} 10 eval instant at 95s yincrease(additivity_reset_late[50s]) - {job="api"} 40 + {job="api"} 25 eval instant at 95s yincrease(additivity_reset_late[90s]) - {job="api"} 80 -# 40 + 40 == 80 + {job="api"} 35 +# 10 + 25 == 35 clear From 0f15d8a74c2c1605a4a7c1020727a285a125ab84 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 17:21:29 -0700 Subject: [PATCH 04/14] Simplify additivity_reset_late scenario comment. Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 11efe0d9ca6..fd9a8b982ed 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -362,8 +362,7 @@ eval instant at 95s yincrease(additivity_reset_early[90s]) clear # Scenario 3: counter reset in the later window. -# Reset between t=60s (value 29) and t=70s (value 11). Pre-reset deltas -# +1..+7 (start 8); post-reset +1..+3 (start 11). T_0=5s, T_1=45s, T_2=95s. +# Reset between t=60s (value 29) and t=70s (value 11). T_0=5s, T_1=45s, T_2=95s. load 10s additivity_reset_late{job="api"} 8 9 11 14 18 23 29 11 12 14 17 From 709d3df2c0df7f58a80b2ab2f86a5b4c18bd0d86 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 17:38:58 -0700 Subject: [PATCH 05/14] Consolidate REPLACE_RATE_FUNCS parser/function repoint helpers Mirror the refactor from invoca-2.55.1/replace-rate-funcs-keep-orig: unify preserve/repoint helpers into replaceStandardRateFuncs with setParserFunctionFrom and setFunctionCallFrom. Co-authored-by: Cursor --- promql/xrate_yrate_funcs.go | 68 ++++++++++++------------------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/promql/xrate_yrate_funcs.go b/promql/xrate_yrate_funcs.go index b72e1bbb26a..c20f1719bf2 100644 --- a/promql/xrate_yrate_funcs.go +++ b/promql/xrate_yrate_funcs.go @@ -218,63 +218,39 @@ func init() { fmt.Println("Successfully replaced rate & friends with xrate & friends (and removed xrate & friends function keys).") case "x", "X": - preserveOriginalRateFuncs() - repointParserFunctions("delta", "xdelta") - repointParserFunctions("increase", "xincrease") - repointParserFunctions("rate", "xrate") - repointFunction("delta", "xdelta") - repointFunction("increase", "xincrease") - repointFunction("rate", "xrate") + replaceStandardRateFuncs("x") fmt.Println("Successfully replaced rate/increase/delta with xrate/xincrease/xdelta; originals available as _rate/_increase/_delta; x* names also available.") case "2", "y", "Y": - preserveOriginalRateFuncs() - repointParserFunctions("delta", "ydelta") - repointParserFunctions("increase", "yincrease") - repointParserFunctions("rate", "yrate") - repointFunction("delta", "ydelta") - repointFunction("increase", "yincrease") - repointFunction("rate", "yrate") + replaceStandardRateFuncs("y") fmt.Println("Successfully replaced rate/increase/delta with yrate/yincrease/ydelta; originals available as _rate/_increase/_delta; y* and x* names also available.") } } -// preserveOriginalRateFuncs copies the upstream rate/increase/delta parser and -// evaluator entries to _rate/_increase/_delta before repointing the standard -// names at the xrate or yrate family. -func preserveOriginalRateFuncs() { - copyParserFunction("delta", "_delta") - copyParserFunction("increase", "_increase") - copyParserFunction("rate", "_rate") - copyFunctionCall("delta", "_delta") - copyFunctionCall("increase", "_increase") - copyFunctionCall("rate", "_rate") -} - -func copyParserFunction(fromName, toName string) { - result := *parser.Functions[fromName] - result.Name = toName - parser.Functions[toName] = &result -} - -func copyFunctionCall(fromName, toName string) { - FunctionCalls[toName] = FunctionCalls[fromName] +// replaceStandardRateFuncs preserves upstream delta/increase/rate as +// _delta/_increase/_rate and repoints the standard names at the x* or y* family +// (per replacementPrefix). +func replaceStandardRateFuncs(replacementPrefix string) { + for _, name := range []string{"delta", "increase", "rate"} { + setParserFunctionFrom("_"+name, name) + setFunctionCallFrom("_"+name, name) + replacement := replacementPrefix + name + setParserFunctionFrom(name, replacement) + setFunctionCallFrom(name, replacement) + } } -// repointParserFunctions makes name resolve to newName's implementation while -// keeping name as the displayed/parser function name. A copy is made so the -// newName entry is not mutated. -func repointParserFunctions(name, newName string) { - result := *parser.Functions[newName] - result.Name = name - parser.Functions[name] = &result +// setParserFunctionFrom registers targetName as a copy of sourceName's parser +// metadata, with Name set to targetName. +func setParserFunctionFrom(targetName, sourceName string) { + result := *parser.Functions[sourceName] + result.Name = targetName + parser.Functions[targetName] = &result } -// repointFunction makes the FunctionCalls entry for name dispatch to the -// implementation currently registered under newName. The newName entry -// is left in place. -func repointFunction(name, newName string) { - FunctionCalls[name] = FunctionCalls[newName] +// setFunctionCallFrom makes targetName dispatch to sourceName's implementation. +func setFunctionCallFrom(targetName, sourceName string) { + FunctionCalls[targetName] = FunctionCalls[sourceName] } // rateFuncPointersEqual compares two FunctionCall implementations by function From 26192198b62fc60c3dd702c207fb17cdf2637473 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 20:05:45 -0700 Subject: [PATCH 06/14] Split xrate and yrate into separate promql files Rename preRangeExtrapolation to extendedXRate (2.55.1 extendedRate lineage) in extended_xrate_funcs.go. Move yrate helpers and REPLACE_RATE_FUNCS init to yrate_funcs.go so xrate support can be dropped independently. Co-authored-by: Cursor --- promql/extended_xrate_funcs.go | 106 ++++++++++++++++++ .../{xrate_yrate_funcs.go => yrate_funcs.go} | 83 -------------- 2 files changed, 106 insertions(+), 83 deletions(-) create mode 100644 promql/extended_xrate_funcs.go rename promql/{xrate_yrate_funcs.go => yrate_funcs.go} (72%) diff --git a/promql/extended_xrate_funcs.go b/promql/extended_xrate_funcs.go new file mode 100644 index 00000000000..620fff6fb0c --- /dev/null +++ b/promql/extended_xrate_funcs.go @@ -0,0 +1,106 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promql + +import ( + "github.com/prometheus/prometheus/promql/parser" + "github.com/prometheus/prometheus/util/annotations" +) + +// extendedXRate is a utility function for xrate/xincrease/xdelta. +// It calculates the rate (allowing for counter resets if isCounter is true), +// taking into account the last sample before the range start, and returns +// the result as either per-second (if isRate is true) or overall. +// +// On the 2.55.1 line this was named extendedRate. Renamed here to avoid +// collision with extendedRate() in functions.go, which implements upstream +// Prometheus 3.x anchored/smoothed selectors. +func extendedXRate(matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper, isCounter, isRate bool) (Vector, annotations.Annotations) { + ms := args[0].(*parser.MatrixSelector) + vs := ms.VectorSelector.(*parser.VectorSelector) + + var ( + samples = matrixVals[0] + rangeStart = enh.Ts - durationMilliseconds(ms.Range+vs.Offset) + rangeEnd = enh.Ts - durationMilliseconds(vs.Offset) + ) + + points := samples.Floats + if len(points) < 2 { + return enh.Out, nil + } + sampledRange := float64(points[len(points)-1].T - points[0].T) + averageInterval := sampledRange / float64(len(points)-1) + + firstPoint := 0 + // If the point before the range is too far from rangeStart, drop it. + if float64(rangeStart-points[0].T) > averageInterval { + if len(points) < 3 { + return enh.Out, nil + } + firstPoint = 1 + sampledRange = float64(points[len(points)-1].T - points[firstPoint].T) + averageInterval = sampledRange / float64(len(points)-2) + } + + var ( + counterCorrection float64 + lastValue float64 + ) + if isCounter { + for i := firstPoint; i < len(points); i++ { + sample := points[i] + if sample.F < lastValue { + counterCorrection += lastValue + } + lastValue = sample.F + } + } + resultValue := points[len(points)-1].F - points[firstPoint].F + counterCorrection + + // Duration between last sample and boundary of range. + durationToEnd := float64(rangeEnd - points[len(points)-1].T) + + // If the points cover the whole range (i.e. they start just before the + // range start and end just before the range end) adjust the value from + // the sampled range to the requested range. + if points[firstPoint].T <= rangeStart && durationToEnd < averageInterval { + adjustToRange := float64(durationMilliseconds(ms.Range)) + resultValue *= (adjustToRange / sampledRange) + } + + if isRate { + resultValue /= ms.Range.Seconds() + } + + return append(enh.Out, Sample{F: resultValue}), nil +} + +func funcXdelta(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return extendedXRate(matrixVals, args, enh, false, false) +} + +func funcXrate(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return extendedXRate(matrixVals, args, enh, true, true) +} + +func funcXincrease(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { + return extendedXRate(matrixVals, args, enh, true, false) +} + +func init() { + FunctionCalls["xdelta"] = funcXdelta + FunctionCalls["xincrease"] = funcXincrease + FunctionCalls["xrate"] = funcXrate +} diff --git a/promql/xrate_yrate_funcs.go b/promql/yrate_funcs.go similarity index 72% rename from promql/xrate_yrate_funcs.go rename to promql/yrate_funcs.go index c20f1719bf2..b067ae38c98 100644 --- a/promql/xrate_yrate_funcs.go +++ b/promql/yrate_funcs.go @@ -22,74 +22,6 @@ import ( "github.com/prometheus/prometheus/util/annotations" ) -// preRangeExtrapolation is a utility function for xrate/xincrease/xdelta. -// It calculates the rate (allowing for counter resets if isCounter is true), -// taking into account the last sample before the range start, and returns -// the result as either per-second (if isRate is true) or overall. -// -// Do not confuse with extendedRate(), which implements anchored/smoothed -// selectors in upstream Prometheus 3.x. -func preRangeExtrapolation(matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper, isCounter, isRate bool) (Vector, annotations.Annotations) { - ms := args[0].(*parser.MatrixSelector) - vs := ms.VectorSelector.(*parser.VectorSelector) - - var ( - samples = matrixVals[0] - rangeStart = enh.Ts - durationMilliseconds(ms.Range+vs.Offset) - rangeEnd = enh.Ts - durationMilliseconds(vs.Offset) - ) - - points := samples.Floats - if len(points) < 2 { - return enh.Out, nil - } - sampledRange := float64(points[len(points)-1].T - points[0].T) - averageInterval := sampledRange / float64(len(points)-1) - - firstPoint := 0 - // If the point before the range is too far from rangeStart, drop it. - if float64(rangeStart-points[0].T) > averageInterval { - if len(points) < 3 { - return enh.Out, nil - } - firstPoint = 1 - sampledRange = float64(points[len(points)-1].T - points[firstPoint].T) - averageInterval = sampledRange / float64(len(points)-2) - } - - var ( - counterCorrection float64 - lastValue float64 - ) - if isCounter { - for i := firstPoint; i < len(points); i++ { - sample := points[i] - if sample.F < lastValue { - counterCorrection += lastValue - } - lastValue = sample.F - } - } - resultValue := points[len(points)-1].F - points[firstPoint].F + counterCorrection - - // Duration between last sample and boundary of range. - durationToEnd := float64(rangeEnd - points[len(points)-1].T) - - // If the points cover the whole range (i.e. they start just before the - // range start and end just before the range end) adjust the value from - // the sampled range to the requested range. - if points[firstPoint].T <= rangeStart && durationToEnd < averageInterval { - adjustToRange := float64(durationMilliseconds(ms.Range)) - resultValue *= (adjustToRange / sampledRange) - } - - if isRate { - resultValue /= ms.Range.Seconds() - } - - return append(enh.Out, Sample{F: resultValue}), nil -} - // yIncrease is a utility function for yincrease/yrate/ydelta. // It calculates the increase of the range (allowing for counter resets if isCounter is true), // taking into account the sample at the end of the previous range (just before rangeStartMsec). @@ -145,18 +77,6 @@ func rangeFromSelectors(matrixVals Matrix, args parser.Expressions, enh *EvalNod return points, rangeStartMsec, rangeEndMsec, ms.Range.Seconds() } -func funcXdelta(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - return preRangeExtrapolation(matrixVals, args, enh, false, false) -} - -func funcXrate(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - return preRangeExtrapolation(matrixVals, args, enh, true, true) -} - -func funcXincrease(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { - return preRangeExtrapolation(matrixVals, args, enh, true, false) -} - func funcYdelta(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *EvalNodeHelper) (Vector, annotations.Annotations) { points, rangeStartMsec, rangeEndMsec, _ := rangeFromSelectors(matrixVals, args, enh) value := yIncrease(points, rangeStartMsec, rangeEndMsec, false) @@ -176,9 +96,6 @@ func funcYrate(_ []Vector, matrixVals Matrix, args parser.Expressions, enh *Eval } func init() { - FunctionCalls["xdelta"] = funcXdelta - FunctionCalls["xincrease"] = funcXincrease - FunctionCalls["xrate"] = funcXrate FunctionCalls["ydelta"] = funcYdelta FunctionCalls["yincrease"] = funcYincrease FunctionCalls["yrate"] = funcYrate From 90d6738266afab047c92a67e99d1b0ab2d2d03b7 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 20:28:32 -0700 Subject: [PATCH 07/14] Update yrate vs xrate showcase comment wording Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index fd9a8b982ed..a27936d99bc 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -118,7 +118,7 @@ clear # Tests for increase()/xincrease()/yincrease()/xrate()/yrate(). # # The counters start at 1000/2000 so yincrease/yrate (which treat every -# pre-origin value as 0) return wildly different results from the +# pre-origin value as 0) return significantly different results from the # xrate / rate family (which only consider deltas inside the range). # # Eval times are 49s/48s rather than 50s/47s so that sample timestamps From aa074e01d108fb09fcc8b077c17610a5faa8eb4a Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 20:59:11 -0700 Subject: [PATCH 08/14] Fix punctuation in ExtRange lookback comment Co-authored-by: Cursor --- promql/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promql/engine.go b/promql/engine.go index 01977bbded1..f569349a2ca 100644 --- a/promql/engine.go +++ b/promql/engine.go @@ -1072,7 +1072,7 @@ func (ng *Engine) populateSeries(ctx context.Context, querier storage.Querier, s hints.By, hints.Grouping = extractGroupsFromPath(path) // Include an extra lookbackDelta iff this is the argument to an // extended range function. Extended ranges include one extra - // point, this is how far back we need to look for it. + // point; this is how far back we need to look for it. f, ok := parser.Functions[hints.Func] if ok && f.ExtRange { hints.Start -= durationMilliseconds(ng.lookbackDelta) From a76890d8ebdd795521a03bb77f62357164f56ec5 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 21:04:52 -0700 Subject: [PATCH 09/14] Move REPLACE_RATE_FUNCS init to replace_rate_funcs.go Pair with replace_rate_funcs_test.go. yrate_funcs init registers y* names then calls initReplaceRateFuncs so x* and y* are both available first. Co-authored-by: Cursor --- promql/replace_rate_funcs.go | 102 +++++++++++++++++++++++++++++++++++ promql/yrate_funcs.go | 80 +-------------------------- 2 files changed, 103 insertions(+), 79 deletions(-) create mode 100644 promql/replace_rate_funcs.go diff --git a/promql/replace_rate_funcs.go b/promql/replace_rate_funcs.go new file mode 100644 index 00000000000..cb5d7fb79ab --- /dev/null +++ b/promql/replace_rate_funcs.go @@ -0,0 +1,102 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package promql + +import ( + "fmt" + "os" + "reflect" + + "github.com/prometheus/prometheus/promql/parser" +) + +// initReplaceRateFuncs swaps built-in rate/increase/delta for the x* or y* family +// when REPLACE_RATE_FUNCS is set. Called from yrate_funcs init after x* and y* +// FunctionCalls are registered. +func initReplaceRateFuncs() { + // REPLACE_RATE_FUNCS lets operators swap the built-in rate extrapolation + // functions with the xrate or yrate family at process start, so + // Grafana auto-completion, Prometheus tooling, Thanos, etc. continue to + // work against queries that call the standard rate/increase/delta names. + // + // Values: + // "1" - replace rate/increase/delta with xrate/xincrease/xdelta + // AND remove the x* names (legacy behaviour). + // "x", "X" - point rate/increase/delta at xrate/xincrease/xdelta; + // keep the x* names; preserve upstream implementations as + // _rate/_increase/_delta. + // "2", - point rate/increase/delta at yrate/yincrease/ydelta; + // "y", "Y" keep the y* (and x*) names; preserve upstream + // implementations as _rate/_increase/_delta. + switch os.Getenv("REPLACE_RATE_FUNCS") { + case "1": + FunctionCalls["delta"] = FunctionCalls["xdelta"] + FunctionCalls["increase"] = FunctionCalls["xincrease"] + FunctionCalls["rate"] = FunctionCalls["xrate"] + delete(FunctionCalls, "xdelta") + delete(FunctionCalls, "xincrease") + delete(FunctionCalls, "xrate") + + parser.Functions["delta"] = parser.Functions["xdelta"] + parser.Functions["increase"] = parser.Functions["xincrease"] + parser.Functions["rate"] = parser.Functions["xrate"] + parser.Functions["delta"].Name = "delta" + parser.Functions["increase"].Name = "increase" + parser.Functions["rate"].Name = "rate" + delete(parser.Functions, "xdelta") + delete(parser.Functions, "xincrease") + delete(parser.Functions, "xrate") + fmt.Println("Successfully replaced rate & friends with xrate & friends (and removed xrate & friends function keys).") + + case "x", "X": + replaceStandardRateFuncs("x") + fmt.Println("Successfully replaced rate/increase/delta with xrate/xincrease/xdelta; originals available as _rate/_increase/_delta; x* names also available.") + + case "2", "y", "Y": + replaceStandardRateFuncs("y") + fmt.Println("Successfully replaced rate/increase/delta with yrate/yincrease/ydelta; originals available as _rate/_increase/_delta; y* and x* names also available.") + } +} + +// replaceStandardRateFuncs preserves upstream delta/increase/rate as +// _delta/_increase/_rate and repoints the standard names at the x* or y* family +// (per replacementPrefix). +func replaceStandardRateFuncs(replacementPrefix string) { + for _, name := range []string{"delta", "increase", "rate"} { + setParserFunctionFrom("_"+name, name) + setFunctionCallFrom("_"+name, name) + replacement := replacementPrefix + name + setParserFunctionFrom(name, replacement) + setFunctionCallFrom(name, replacement) + } +} + +// setParserFunctionFrom registers targetName as a copy of sourceName's parser +// metadata, with Name set to targetName. +func setParserFunctionFrom(targetName, sourceName string) { + result := *parser.Functions[sourceName] + result.Name = targetName + parser.Functions[targetName] = &result +} + +// setFunctionCallFrom makes targetName dispatch to sourceName's implementation. +func setFunctionCallFrom(targetName, sourceName string) { + FunctionCalls[targetName] = FunctionCalls[sourceName] +} + +// rateFuncPointersEqual compares two FunctionCall implementations by function +// pointer. Used by tests only. +func rateFuncPointersEqual(a, b FunctionCall) bool { + return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer() +} diff --git a/promql/yrate_funcs.go b/promql/yrate_funcs.go index b067ae38c98..c7db4cddb44 100644 --- a/promql/yrate_funcs.go +++ b/promql/yrate_funcs.go @@ -14,10 +14,6 @@ package promql import ( - "fmt" - "os" - "reflect" - "github.com/prometheus/prometheus/promql/parser" "github.com/prometheus/prometheus/util/annotations" ) @@ -99,79 +95,5 @@ func init() { FunctionCalls["ydelta"] = funcYdelta FunctionCalls["yincrease"] = funcYincrease FunctionCalls["yrate"] = funcYrate - - // REPLACE_RATE_FUNCS lets operators swap the built-in rate extrapolation - // functions with the xrate or yrate family at process start, so - // Grafana auto-completion, Prometheus tooling, Thanos, etc. continue to - // work against queries that call the standard rate/increase/delta names. - // - // Values: - // "1" - replace rate/increase/delta with xrate/xincrease/xdelta - // AND remove the x* names (legacy behaviour). - // "x", "X" - point rate/increase/delta at xrate/xincrease/xdelta; - // keep the x* names; preserve upstream implementations as - // _rate/_increase/_delta. - // "2", - point rate/increase/delta at yrate/yincrease/ydelta; - // "y", "Y" keep the y* (and x*) names; preserve upstream - // implementations as _rate/_increase/_delta. - switch os.Getenv("REPLACE_RATE_FUNCS") { - case "1": - FunctionCalls["delta"] = FunctionCalls["xdelta"] - FunctionCalls["increase"] = FunctionCalls["xincrease"] - FunctionCalls["rate"] = FunctionCalls["xrate"] - delete(FunctionCalls, "xdelta") - delete(FunctionCalls, "xincrease") - delete(FunctionCalls, "xrate") - - parser.Functions["delta"] = parser.Functions["xdelta"] - parser.Functions["increase"] = parser.Functions["xincrease"] - parser.Functions["rate"] = parser.Functions["xrate"] - parser.Functions["delta"].Name = "delta" - parser.Functions["increase"].Name = "increase" - parser.Functions["rate"].Name = "rate" - delete(parser.Functions, "xdelta") - delete(parser.Functions, "xincrease") - delete(parser.Functions, "xrate") - fmt.Println("Successfully replaced rate & friends with xrate & friends (and removed xrate & friends function keys).") - - case "x", "X": - replaceStandardRateFuncs("x") - fmt.Println("Successfully replaced rate/increase/delta with xrate/xincrease/xdelta; originals available as _rate/_increase/_delta; x* names also available.") - - case "2", "y", "Y": - replaceStandardRateFuncs("y") - fmt.Println("Successfully replaced rate/increase/delta with yrate/yincrease/ydelta; originals available as _rate/_increase/_delta; y* and x* names also available.") - } -} - -// replaceStandardRateFuncs preserves upstream delta/increase/rate as -// _delta/_increase/_rate and repoints the standard names at the x* or y* family -// (per replacementPrefix). -func replaceStandardRateFuncs(replacementPrefix string) { - for _, name := range []string{"delta", "increase", "rate"} { - setParserFunctionFrom("_"+name, name) - setFunctionCallFrom("_"+name, name) - replacement := replacementPrefix + name - setParserFunctionFrom(name, replacement) - setFunctionCallFrom(name, replacement) - } -} - -// setParserFunctionFrom registers targetName as a copy of sourceName's parser -// metadata, with Name set to targetName. -func setParserFunctionFrom(targetName, sourceName string) { - result := *parser.Functions[sourceName] - result.Name = targetName - parser.Functions[targetName] = &result -} - -// setFunctionCallFrom makes targetName dispatch to sourceName's implementation. -func setFunctionCallFrom(targetName, sourceName string) { - FunctionCalls[targetName] = FunctionCalls[sourceName] -} - -// rateFuncPointersEqual compares two FunctionCall implementations by function -// pointer. Used by tests only. -func rateFuncPointersEqual(a, b FunctionCall) bool { - return reflect.ValueOf(a).Pointer() == reflect.ValueOf(b).Pointer() + initReplaceRateFuncs() } From 1798106c74ba99443a697c963073fc66fbfeed2e Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 22:04:48 -0700 Subject: [PATCH 10/14] Trim showcase comment in functions.test Remove parenthetical about plain 1 2 3 ladder from /bar series comment. Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index a27936d99bc..97aeee6fbd9 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -3,7 +3,7 @@ # /bar is an 11-point counter whose per-scrape delta grows by 1 (+1, +2, …, +10). # It starts at 11 — one above the largest in-range step — so the "unwritten origin" # offset (yrate baseline 0 → first sample 11) cannot be confused with any +k step -# inside the series (a plain 1 2 3 … ladder hides both off-by-one and origin bugs). +# inside the series. load 5s http_requests{path="/foo"} 1 1 1 2 2 2 2 2 3 3 3 From 67667e544877fd344270a10ac9b7e2ff8670930f Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Thu, 9 Jul 2026 22:29:43 -0700 Subject: [PATCH 11/14] Update test comments Co-authored-by: Cursor --- promql/promqltest/testdata/functions.test | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/promql/promqltest/testdata/functions.test b/promql/promqltest/testdata/functions.test index 97aeee6fbd9..316e460bf6b 100644 --- a/promql/promqltest/testdata/functions.test +++ b/promql/promqltest/testdata/functions.test @@ -122,7 +122,7 @@ clear # xrate / rate family (which only consider deltas inside the range). # # Eval times are 49s/48s rather than 50s/47s so that sample timestamps -# land strictly inside the range [start, end) rather than at its +# land strictly inside the range (start, end] rather than at its # boundaries; this keeps the pre-range sample accessible to matrixIterSlice # and makes yincrease's "counter-at-rangeStart" value observable. load 5s @@ -314,8 +314,7 @@ clear # # These functions are additive over adjacent ranges -- which is what makes them # composable across any partitioning of a wider range into contiguous sub-ranges. -# Because they evaluate over a half-open range (left-inclusive on the 2.53/2.55 -# add-yrate line, right-inclusive after align-yrate-to-3x-range-boundary), two +# Because they evaluate over a half-open range (start, end], two # adjacent windows partition a wider one without double-counting any sample. # For any three timestamps T_0 < T_1 < T_2 and range durations r_1 = T_1 - T_0, # r_2 = T_2 - T_1: @@ -323,8 +322,7 @@ clear # yincrease(m[r_1]) @ T_1 + yincrease(m[r_2]) @ T_2 == yincrease(m[r_1 + r_2]) @ T_2 # # Each scenario below picks T_0, T_1, T_2 off-cadence (no sample lands on a range -# boundary) so the expected values are identical under both boundary conventions; -# this block should cherry-pick cleanly across the yrate branch stack. +# boundary). # Scenario 1: counter with incrementing deltas, no resets. T_0=5s, T_1=35s, T_2=75s. # Deltas +1..+9 at 10s scrape; start 10 (= max step + 1). From cb1f36569f4d066562b7694047b2232e9f9060a9 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Fri, 10 Jul 2026 12:10:36 -0700 Subject: [PATCH 12/14] Add license header to replace_rate_funcs_test.go Satisfy the required Apache header check in GHA. Co-authored-by: Cursor --- promql/replace_rate_funcs_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/promql/replace_rate_funcs_test.go b/promql/replace_rate_funcs_test.go index 19012a36b92..85f5bc7f2ea 100644 --- a/promql/replace_rate_funcs_test.go +++ b/promql/replace_rate_funcs_test.go @@ -1,3 +1,16 @@ +// Copyright 2015 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package promql import ( From 6b1262b14485ae40d79d9e8a8bdf920d3c6d0151 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Sat, 11 Jul 2026 13:41:30 -0700 Subject: [PATCH 13/14] Update features.json golden for xrate/yrate family. Co-authored-by: Cursor --- cmd/prometheus/testdata/features.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/prometheus/testdata/features.json b/cmd/prometheus/testdata/features.json index ae7490c5b7d..3e6d319fe4c 100644 --- a/cmd/prometheus/testdata/features.json +++ b/cmd/prometheus/testdata/features.json @@ -138,7 +138,13 @@ "ts_of_max_over_time": false, "ts_of_min_over_time": false, "vector": true, - "year": true + "xdelta": true, + "xincrease": true, + "xrate": true, + "ydelta": true, + "year": true, + "yincrease": true, + "yrate": true }, "promql_operators": { "!=": true, From fbfe2c2b4ec296db50bb702e646d891db6c062f7 Mon Sep 17 00:00:00 2001 From: Colin Kelley Date: Sat, 11 Jul 2026 13:53:22 -0700 Subject: [PATCH 14/14] Regenerate PromQL UI function signatures for xrate/yrate family. Co-authored-by: Cursor --- web/ui/mantine-ui/src/promql/functionSignatures.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web/ui/mantine-ui/src/promql/functionSignatures.ts b/web/ui/mantine-ui/src/promql/functionSignatures.ts index 3fe9167ce7b..6769196f341 100644 --- a/web/ui/mantine-ui/src/promql/functionSignatures.ts +++ b/web/ui/mantine-ui/src/promql/functionSignatures.ts @@ -204,5 +204,11 @@ export const functionSignatures: Record = { returnType: valueType.vector, }, vector: { name: "vector", argTypes: [valueType.scalar], variadic: 0, returnType: valueType.vector }, + xdelta: { name: "xdelta", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector }, + xincrease: { name: "xincrease", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector }, + xrate: { name: "xrate", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector }, + ydelta: { name: "ydelta", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector }, year: { name: "year", argTypes: [valueType.vector], variadic: 1, returnType: valueType.vector }, + yincrease: { name: "yincrease", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector }, + yrate: { name: "yrate", argTypes: [valueType.matrix], variadic: 0, returnType: valueType.vector }, };