Merge pull request #17988 from roidelapluie/roidelapluie/fixsmoothing

promql: fix smoothed interpolation across counter resets
This commit is contained in:
Julien 2026-02-02 13:40:41 +01:00 committed by GitHub
commit 717d37bbca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 20 additions and 17 deletions

View file

@ -1667,7 +1667,7 @@ func (ev *evaluator) smoothSeries(series []storage.Series, offset time.Duration)
// Interpolate between prev and next.
// TODO: detect if the sample is a counter, based on __type__ or metadata.
prev, next := floats[i-1], floats[i]
val := interpolate(prev, next, ts, false, false)
val := interpolate(prev, next, ts, false)
ss.Floats = append(ss.Floats, FPoint{F: val, T: ts})
case i > 0:

View file

@ -70,7 +70,7 @@ func funcTime(_ []Vector, _ Matrix, _ parser.Expressions, enh *EvalNodeHelper) (
// it returns the interpolated value at the left boundary; otherwise, it returns the first sample's value.
func pickOrInterpolateLeft(floats []FPoint, first int, rangeStart int64, smoothed, isCounter bool) float64 {
if smoothed && floats[first].T < rangeStart {
return interpolate(floats[first], floats[first+1], rangeStart, isCounter, true)
return interpolate(floats[first], floats[first+1], rangeStart, isCounter)
}
return floats[first].F
}
@ -80,25 +80,20 @@ func pickOrInterpolateLeft(floats []FPoint, first int, rangeStart int64, smoothe
// it returns the interpolated value at the right boundary; otherwise, it returns the last sample's value.
func pickOrInterpolateRight(floats []FPoint, last int, rangeEnd int64, smoothed, isCounter bool) float64 {
if smoothed && last > 0 && floats[last].T > rangeEnd {
return interpolate(floats[last-1], floats[last], rangeEnd, isCounter, false)
return interpolate(floats[last-1], floats[last], rangeEnd, isCounter)
}
return floats[last].F
}
// interpolate performs linear interpolation between two points.
// If isCounter is true and there is a counter reset:
// - on the left edge, it sets the value to 0.
// - on the right edge, it adds the left value to the right value.
// If isCounter is true and there is a counter reset, it models the counter
// as starting from 0 (post-reset) by setting y1 to 0.
// It then calculates the interpolated value at the given timestamp.
func interpolate(p1, p2 FPoint, t int64, isCounter, leftEdge bool) float64 {
func interpolate(p1, p2 FPoint, t int64, isCounter bool) float64 {
y1 := p1.F
y2 := p2.F
if isCounter && y2 < y1 {
if leftEdge {
y1 = 0
} else {
y2 += y1
}
y1 = 0
}
return y1 + (y2-y1)*float64(t-p1.T)/float64(p2.T-p1.T)

View file

@ -108,13 +108,13 @@ func TestInterpolate(t *testing.T) {
{FPoint{T: 1, F: 100}, FPoint{T: 2, F: 200}, 1, false, 100},
{FPoint{T: 0, F: 100}, FPoint{T: 2, F: 200}, 1, false, 150},
{FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, false, 150},
{FPoint{T: 0, F: 200}, FPoint{T: 2, F: 0}, 1, true, 200},
{FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, true, 250},
{FPoint{T: 0, F: 500}, FPoint{T: 2, F: 100}, 1, true, 550},
{FPoint{T: 0, F: 500}, FPoint{T: 10, F: 0}, 1, true, 500},
{FPoint{T: 0, F: 200}, FPoint{T: 2, F: 0}, 1, true, 0},
{FPoint{T: 0, F: 200}, FPoint{T: 2, F: 100}, 1, true, 50},
{FPoint{T: 0, F: 500}, FPoint{T: 2, F: 100}, 1, true, 50},
{FPoint{T: 0, F: 500}, FPoint{T: 10, F: 0}, 1, true, 0},
}
for _, test := range tests {
result := interpolate(test.p1, test.p2, test.t, test.isCounter, false)
result := interpolate(test.p1, test.p2, test.t, test.isCounter)
require.Equal(t, test.expected, result)
}
}

View file

@ -358,6 +358,14 @@ load 1m
eval instant at 2m15s increase(metric[2m] smoothed)
{} 12
# Smoothed rate interpolation across a counter reset.
clear
load 15s
metric 100 10
eval instant at 12s rate(metric[10s] smoothed)
{} 0.666666666666667
clear
eval instant at 1m deriv(foo[3m] smoothed)
expect fail msg: smoothed modifier can only be used with: delta, increase, rate - not with deriv