8000 Promql: Prevent extrapolation of NH below zero by gen1321 · Pull Request #16192 · prometheus/prometheus · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Promql: Prevent extrapolation of NH below zero #16192

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion promql/functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ func extrapolatedRate(vals []parser.Value, args parser.Expressions, enh *EvalNod
rangeEnd = enh.Ts - durationMilliseconds(vs.Offset)
resultFloat float64
resultHistogram *histogram.FloatHistogram
firstH *histogram.FloatHistogram
firstT, lastT int64
numSamplesMinusOne int
annos annotations.Annotations
Expand All @@ -103,6 +104,13 @@ func extrapolatedRate(vals []parser.Value, args parser.Expressions, enh *EvalNod
// The histograms are not compatible with each other.
return enh.Out, annos
}
// Find the first histogram sample compatible with our result so we can extrapolate correctly.
for _, h := range samples.Histograms {
if h.H.UsesCustomBuckets() == resultHistogram.UsesCustomBuckets() {
firstH = h.H
break
}
}
case len(samples.Floats) > 1:
numSamplesMinusOne = len(samples.Floats) - 1
firstT = samples.Floats[0].T
Expand Down Expand Up @@ -156,12 +164,24 @@ func extrapolatedRate(vals []parser.Value, args parser.Expressions, enh *EvalNod
// than the durationToStart, we take the zero point as the start
// of the series, thereby avoiding extrapolation to negative
// counter values.
// TODO(beorn7): Do this for histograms, too.
durationToZero := sampledInterval * (samples.Floats[0].F / resultFloat)
if durationToZero < durationToStart {
durationToStart = durationToZero
}
}

// Native histogram counter zero-cutoff logic.
var durationToZeroCount float64
if isCounter && resultHistogram != nil &&
samples.Histograms[0].H.Count > 0 && resultHistogram.Count > 0 {
// Find when the total sample count would hit zero (durationToZeroCount)
// If that instant lies inside the query range, shift durationToStart to it
durationToZeroCount = sampledInterval * (firstH.Count / resultHistogram.Count)
if durationToZeroCount < durationToStart {
durationToStart = durationToZeroCount
}
}

extrapolateToInterval += durationToStart

if durationToEnd >= extrapolationThreshold {
Expand All @@ -179,6 +199,44 @@ func extrapolatedRate(vals []parser.Value, args parser.Expressions, enh *EvalNod
resultHistogram.Mul(factor)
}

if isCounter && resultHistogram != nil && firstH != nil && resultHistogram.Count > 0 {
rangeSeconds := ms.Range.Seconds()
// How much we "scaled" sampled interval.
deltaScale := sampledInterval / extrapolateToInterval
if isRate {
deltaScale *= rangeSeconds
}
for bucketIndex, bucketVal := range resultHistogram.PositiveBuckets {
if bucketVal <= 0 {
continue
}
// Get bucket value from the first sample.
bucketStartVal := firstH.PositiveBuckets[bucketIndex]
// Raw change in bucket value between last and first samples.
bucketDelta := bucketVal * deltaScale
// Predict bucket value at the lower boundary of the range.
predictedAtStart := bucketStartVal - (bucketDelta / sampledInterval * durationToStart)
if durationToStart == durationToZeroCount || (predictedAtStart < 0 && bucketStartVal > 0) {
// Predict bucket value at the upper boundary of the range.
predictedAtEnd := bucketStartVal + (bucketDelta * (1 + durationToEnd/sampledInterval))
var countDiff float64
if isRate {
// Calculate the adjusted per-second rate based on the predicted count at the range end.
// This rate reflects the increase from the estimated zero point (clamped at range start)
// to the range end, preventing negative extrapolation artifacts.
adjustedRate := predictedAtEnd / rangeSeconds
countDiff = adjustedRate - bucketVal
resultHistogram.PositiveBuckets[bucketIndex] = adjustedRate
} else {
// For non-rate functions, calculate the total increase from start to end
// and use the predicted end value as the adjusted value
countDiff = predictedAtEnd - bucketVal
resultHistogram.PositiveBuckets[bucketIndex] = predictedAtEnd
}
resultHistogram.Count += countDiff
}
}
}
return append(enh.Out, Sample{F: resultFloat, H: resultHistogram}), annos
}

Expand Down
2 changes: 1 addition & 1 deletion promql/promqltest/testdata/histograms.test
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ load_with_nhcb 5m
histogram_with_reset_sum{} 36 16 61

eval instant at 10m increase(histogram_with_reset[15m])
{} {{schema:-53 count:27 sum:91.5 custom_values:[1 2 4 8] counter_reset_hint:gauge buckets:[13.5 0 4.5 9]}}
{} {{schema:-53 count:23.5 sum:91.5 custom_values:[1 2 4 8] counter_reset_hint:gauge buckets:[10 0 4.5 9]}}

eval instant at 10m resets(histogram_with_reset[15m])
{} 1
Expand Down
31 changes: 30 additions & 1 deletion promql/promqltest/testdata/native_histograms.test
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,7 @@ load 5m
reset_in_bucket {{schema:0 count:4 sum:5 buckets:[1 2 1]}} {{schema:0 count:5 sum:6 buckets:[1 1 3]}} {{schema:0 count:6 sum:7 buckets:[1 2 3]}}

eval instant at 10m increase(reset_in_bucket[15m])
{} {{count:9 sum:10.5 buckets:[1.5 3 4.5]}}
{} {{count:8.5 sum:10.5 buckets:[1.5 3 4]}}

# The following two test the "fast path" where only sum and count is decoded.
eval instant at 10m histogram_count(increase(reset_in_bucket[15m]))
Expand Down Expand Up @@ -1319,3 +1319,32 @@ eval instant at 10m histogram_sub_3{idx="0"} - ignoring(idx) histogram_sub_3{idx
{} {{schema:0 count:-30 sum:-1111.1 z_bucket:-2 z_bucket_w:0.001 buckets:[-1 0 -1 -2 -1 -1 -1] n_buckets:[0 2 -2 -2 -7 0 0 0 0 -5 -5 -2]}}

clear

# Test prevent extrapolation of total count below zero for native histograms
# This tests the logic where an increasing counter, if extrapolated linearly
# backwards to rangeStart, would have a negative count. The clamping of
# durationToStart to the point where the counter would have been zero
# affects the extrapolation window and thus the final rate.
load 10s
histo_total_count_clamp_positive_rate {{schema:0 sum:1 count:1 buckets:[1]}} {{schema:0 sum:11 count:11 buckets:[11]}}

eval instant at 10s rate(histo_total_count_clamp_positive_rate[20s])
{} {{schema:0 sum:0.55 count:0.55 buckets:[0.55]}}

eval instant at 10s histogram_count(rate(histo_total_count_clamp_positive_rate[20s]))
{} 0.55

eval instant at 10s histogram_sum(rate(histo_total_count_clamp_positive_rate[20s]))
{} 0.55

clear

# Per‑bucket clamp: bucket 0 would go negative without the fix.
load 10s
histogram_bucket_extrapolation _ _ {{schema:0 sum:0.1 count:1 buckets:[0.1]}} {{schema:0 sum:5.1 count:6 buckets:[5.1]}}

eval instant at 32s rate(histogram_bucket_extrapolation[20s])
{} {{schema:0 sum:0.35 count:0.305 buckets:[0.305]}}

eval instant at 32s histogram_count(rate(histogram_bucket_extrapolation[20s]))
{} 0.35
Loading
0