Skip to content
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
2 changes: 1 addition & 1 deletion pkg/ingester/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func newIngesterMetrics(r prometheus.Registerer,
Help: "Delay in seconds between sample ingestion time and sample timestamp.",
NativeHistogramBucketFactor: 1.1,
NativeHistogramMaxBucketNumber: 100,
NativeHistogramMinResetDuration: 1,
NativeHistogramMinResetDuration: 1 * time.Hour,
Buckets: []float64{1, 5, 10, 30, 60, 120, 300, 600}, // 1s, 5s, 10s, 30s, 1m, 2m, 5m, 10m
}, []string{"user"}),
oooLabelsTotal: promauto.With(r).NewCounterVec(prometheus.CounterOpts{
Expand Down
44 changes: 44 additions & 0 deletions pkg/ingester/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/testutil"
dto "github.com/prometheus/client_model/go"
"github.com/stretchr/testify/require"

util_math "github.com/cortexproject/cortex/pkg/util/math"
Expand Down Expand Up @@ -1298,3 +1299,46 @@ func populateTSDBMetrics(base float64) *prometheus.Registry {

return r
}

// TestIngestionDelaySecondsHistogram_DoesNotLoseObservationsOnNativeBucketLimit
// is a regression test for a bug where ingestionDelaySeconds was registered
// with NativeHistogramMinResetDuration effectively equal to zero (an untyped
// constant "1", i.e. 1 nanosecond, rather than 1 hour). Once the native
// histogram exceeds NativeHistogramMaxBucketNumber, client_golang's
// limitBuckets() first tries maybeReset(), which fully resets the histogram
// (both native and classic buckets, keeping only the latest observation) if
// at least NativeHistogramMinResetDuration has elapsed since the last reset.
// With a ~0 duration that condition is satisfied on essentially every call,
// so instead of gracefully reducing resolution (bucket width doubling / zero
// bucket widening), the histogram silently drops the vast majority of prior
// observations on every bucket-limit breach.
func TestIngestionDelaySecondsHistogram_DoesNotLoseObservationsOnNativeBucketLimit(t *testing.T) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Umm do we need this test? I think it is an obvious bug when defining the metric so maybe fine to omit it.

@ankit090701 ankit090701 Aug 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair to question — happy to drop it if you'd still rather not have it after this. My case for keeping it: the typo itself (1 vs 1 * time.Hour) is obvious once you're looking right at it, but the consequence isn't — I didn't expect "wrong reset duration" to mean "the histogram fully resets and throws away 86% of observations on every bucket-limit breach" until I actually read maybeReset() in the vendored client. Without a test pinning that behavior, a future refactor of this metric block (or someone copying it as a template for a new histogram, which is how the other ~60 correct instances of 1 * time.Hour presumably multiplied in the first place) could reintroduce the exact same bug and nothing would catch it — the type system doesn't, since an untyped 1 silently converts to time.Duration(1).

That said, it's a genuinely fine call to make either way, and I don't want to hold up the PR over it — let me know and I'll remove it if you still think it's not worth keeping.

ingestionRate := util_math.NewEWMARate(0.2, instanceIngestionRateTickInterval)
inflightPushRequests := util_math.MaxTracker{}
maxInflightQueryRequests := util_math.MaxTracker{}

reg := prometheus.NewRegistry()
m := newIngesterMetrics(reg, false, false, false, false,
func() *InstanceLimits { return &InstanceLimits{} },
ingestionRate, &inflightPushRequests, &maxInflightQueryRequests, false, false)

observer := m.ingestionDelaySeconds.WithLabelValues("user")

// Observe many widely-spread values so that the native histogram's
// bucket count exceeds NativeHistogramMaxBucketNumber (100) well before
// the loop ends, forcing limitBuckets()/maybeReset() to run repeatedly.
const numObservations = 500
value := 0.001
for range numObservations {
observer.Observe(value)
value *= 1.2 // bucket factor is 1.1, so each step lands in a new native bucket
}

metric := &dto.Metric{}
require.NoError(t, observer.(prometheus.Metric).Write(metric))

// With a correctly configured (non-trivial) minimum reset duration, no
// observations should be lost: the bucket count is reduced by merging
// buckets, not by discarding samples.
require.Equal(t, uint64(numObservations), metric.GetHistogram().GetSampleCount())
}