From 53ba2941ef0d29fcfef85cd4300ac84ac0731204 Mon Sep 17 00:00:00 2001 From: Siddarth Gundu Date: Tue, 30 Dec 2025 09:40:13 +0530 Subject: [PATCH] util: add native histogram support to SendSumOfHistograms Extend HistogramData struct with native histogram fields. Convert input spans+deltas to map[int]int64 for aggregation. On output, MustNewConstNativeHistogram calls makeBucketsFromMap() to reconstruct spans+deltas for wire format. --- pkg/util/metrics_helper.go | 116 ++++++++++++++++++++++++++++- pkg/util/metrics_helper_test.go | 128 ++++++++++++++++++++++---------- 2 files changed, 203 insertions(+), 41 deletions(-) diff --git a/pkg/util/metrics_helper.go b/pkg/util/metrics_helper.go index 6678c478754..ebb26047e84 100644 --- a/pkg/util/metrics_helper.go +++ b/pkg/util/metrics_helper.go @@ -7,6 +7,7 @@ import ( "math" "strings" "sync" + "time" "github.com/go-kit/log/level" "github.com/prometheus/client_golang/prometheus" @@ -460,7 +461,69 @@ func (s *SummaryData) Metric(desc *prometheus.Desc, labelValues ...string) prome type HistogramData struct { sampleCount uint64 sampleSum float64 - buckets map[float64]uint64 + + buckets map[float64]uint64 + + // Native histogram fields + nativeHistogram bool + Schema int32 + ZeroThreshold float64 + ZeroCount uint64 + PositiveBuckets map[int]int64 // bucket index -> count + NegativeBuckets map[int]int64 +} + +// A histogram is considered native if it has positive/negative spans or a non-zero zero bucket. +func isNative(histo *dto.Histogram) bool { + return len(histo.GetPositiveSpan()) > 0 || + len(histo.GetNegativeSpan()) > 0 || + histo.GetZeroCount() > 0 || + histo.GetZeroThreshold() > 0 +} + +func (d *HistogramData) hasNative() bool { + return d.ZeroCount > 0 || + len(d.PositiveBuckets) > 0 || + len(d.NegativeBuckets) > 0 +} + +func spansCountsToBucketMap(spans []*dto.BucketSpan, counts []int64) map[int]int64 { + if len(spans) == 0 { + return nil + } + bucketMap := make(map[int]int64, len(counts)) + var idx int32 + bucketIdx := 0 + for _, sp := range spans { + idx += sp.GetOffset() + for j := 0; j < int(sp.GetLength()) && bucketIdx < len(counts); j++ { + bucketMap[int(idx)] += counts[bucketIdx] + idx++ + bucketIdx++ + } + } + return bucketMap +} + +func deltasToCountsInt(deltas []int64) []int64 { + counts := make([]int64, len(deltas)) + var cur int64 + for i, d := range deltas { + cur += int64(d) + counts[i] = cur + } + return counts +} + +// mergeBucketMaps merges src bucket map into dst bucket map by summing counts for each bucket index. +func mergeBucketMaps(dst, src map[int]int64) map[int]int64 { + if dst == nil { + dst = make(map[int]int64) + } + for idx, count := range src { + dst[idx] += count + } + return dst } // AddHistogram adds histogram from gathered metrics to this histogram data. @@ -471,6 +534,28 @@ func (d *HistogramData) AddHistogram(histo *dto.Histogram) { d.sampleCount += histo.GetSampleCount() d.sampleSum += histo.GetSampleSum() + if isNative(histo) { + + // Initialize schema/threshold once + if !d.hasNative() { + d.Schema = histo.GetSchema() + d.ZeroThreshold = histo.GetZeroThreshold() + } + d.ZeroCount += histo.GetZeroCount() + + // Decode spans+deltas -> index->population maps and merge. + posCounts := deltasToCountsInt(histo.GetPositiveDelta()) + negCounts := deltasToCountsInt(histo.GetNegativeDelta()) + + posMap := spansCountsToBucketMap(histo.GetPositiveSpan(), posCounts) + negMap := spansCountsToBucketMap(histo.GetNegativeSpan(), negCounts) + + d.PositiveBuckets = mergeBucketMaps(d.PositiveBuckets, posMap) + d.NegativeBuckets = mergeBucketMaps(d.NegativeBuckets, negMap) + return + } + + // Handle classic histogram histoBuckets := histo.GetBucket() if len(histoBuckets) > 0 && d.buckets == nil { d.buckets = map[float64]uint64{} @@ -490,6 +575,18 @@ func (d *HistogramData) AddHistogramData(histo HistogramData) { d.sampleCount += histo.sampleCount d.sampleSum += histo.sampleSum + if histo.hasNative() { + if !d.hasNative() { + d.Schema = histo.Schema + d.ZeroThreshold = histo.ZeroThreshold + } + d.ZeroCount += histo.ZeroCount + d.PositiveBuckets = mergeBucketMaps(d.PositiveBuckets, histo.PositiveBuckets) + d.NegativeBuckets = mergeBucketMaps(d.NegativeBuckets, histo.NegativeBuckets) + return + } + + // Handle classic histogram if len(histo.buckets) > 0 && d.buckets == nil { d.buckets = map[float64]uint64{} } @@ -504,7 +601,24 @@ func (d *HistogramData) AddHistogramData(histo HistogramData) { // // Note that returned metric shares bucket with this HistogramData, so avoid // doing more modifications to this HistogramData after calling Metric. +// +// For native histograms, this returns a metric created via MustNewConstNativeHistogram. +// For classic histograms, this returns a metric created via MustNewConstHistogram. func (d *HistogramData) Metric(desc *prometheus.Desc, labelValues ...string) prometheus.Metric { + if d.hasNative() { + return prometheus.MustNewConstNativeHistogram( + desc, + d.sampleCount, + d.sampleSum, + d.PositiveBuckets, + d.NegativeBuckets, + d.ZeroCount, + d.Schema, + d.ZeroThreshold, + time.Time{}, // No timestamp for aggregated histograms + labelValues..., + ) + } return prometheus.MustNewConstHistogram(desc, d.sampleCount, d.sampleSum, d.buckets, labelValues...) } diff --git a/pkg/util/metrics_helper_test.go b/pkg/util/metrics_helper_test.go index 85d9895389b..ec1cad14c35 100644 --- a/pkg/util/metrics_helper_test.go +++ b/pkg/util/metrics_helper_test.go @@ -647,12 +647,12 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { # TYPE counter counter # No change in counter counter 75 - + # HELP counter_labels help # TYPE counter_labels counter # No change in counter per label. counter_labels{label_one="a"} 75 - + # HELP counter_user help # TYPE counter_user counter # User 3 is now missing. @@ -660,17 +660,17 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { counter_user{user="2"} 10 counter_user{user="4"} 20 counter_user{user="5"} 25 - + # HELP gauge help # TYPE gauge gauge # Drop in the gauge (value 3, counted 5 times) gauge 60 - + # HELP gauge_labels help # TYPE gauge_labels gauge # Drop in the gauge (value 3, counted 5 times) gauge_labels{label_one="a"} 60 - + # HELP gauge_user help # TYPE gauge_user gauge # User 3 is now missing. @@ -678,7 +678,7 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { gauge_user{user="2"} 10 gauge_user{user="4"} 20 gauge_user{user="5"} 25 - + # HELP histogram help # TYPE histogram histogram # No change in the histogram @@ -688,7 +688,7 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { histogram_bucket{le="+Inf"} 25 histogram_sum 75 histogram_count 25 - + # HELP histogram_labels help # TYPE histogram_labels histogram # No change in the histogram per label @@ -698,19 +698,19 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { histogram_labels_bucket{label_one="a",le="+Inf"} 25 histogram_labels_sum{label_one="a"} 75 histogram_labels_count{label_one="a"} 25 - + # HELP summary help # TYPE summary summary # No change in the summary summary_sum 75 summary_count 25 - + # HELP summary_labels help # TYPE summary_labels summary # No change in the summary per label summary_labels_sum{label_one="a"} 75 summary_labels_count{label_one="a"} 25 - + # HELP summary_user help # TYPE summary_user summary # Summary for user 3 is now missing. @@ -730,36 +730,36 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { # TYPE counter counter # No change in counter counter 75 - + # HELP counter_labels help # TYPE counter_labels counter # No change in counter per label. counter_labels{label_one="a"} 75 - + # HELP counter_user help # TYPE counter_user counter # User 3 is now missing. counter_user{user="1"} 5 counter_user{user="2"} 10 counter_user{user="5"} 25 - + # HELP gauge help # TYPE gauge gauge # Drop in the gauge (value 3, counted 5 times) gauge 40 - + # HELP gauge_labels help # TYPE gauge_labels gauge # Drop in the gauge (value 3, counted 5 times) gauge_labels{label_one="a"} 40 - + # HELP gauge_user help # TYPE gauge_user gauge # User 3 is now missing. gauge_user{user="1"} 5 gauge_user{user="2"} 10 gauge_user{user="5"} 25 - + # HELP histogram help # TYPE histogram histogram # No change in the histogram @@ -769,7 +769,7 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { histogram_bucket{le="+Inf"} 25 histogram_sum 75 histogram_count 25 - + # HELP histogram_labels help # TYPE histogram_labels histogram # No change in the histogram per label @@ -779,19 +779,19 @@ func TestUserRegistries_RemoveUserRegistry_SoftRemoval(t *testing.T) { histogram_labels_bucket{label_one="a",le="+Inf"} 25 histogram_labels_sum{label_one="a"} 75 histogram_labels_count{label_one="a"} 25 - + # HELP summary help # TYPE summary summary # No change in the summary summary_sum 75 summary_count 25 - + # HELP summary_labels help # TYPE summary_labels summary # No change in the summary per label summary_labels_sum{label_one="a"} 75 summary_labels_count{label_one="a"} 25 - + # HELP summary_user help # TYPE summary_user summary # Summary for user 3 is now missing. @@ -817,12 +817,12 @@ func TestUserRegistries_RemoveUserRegistry_HardRemoval(t *testing.T) { # TYPE counter counter # Counter drop (reset!) counter 60 - + # HELP counter_labels help # TYPE counter_labels counter # Counter drop (reset!) counter_labels{label_one="a"} 60 - + # HELP counter_user help # TYPE counter_user counter # User 3 is now missing. @@ -830,17 +830,17 @@ func TestUserRegistries_RemoveUserRegistry_HardRemoval(t *testing.T) { counter_user{user="2"} 10 counter_user{user="4"} 20 counter_user{user="5"} 25 - + # HELP gauge help # TYPE gauge gauge # Drop in the gauge (value 3, counted 5 times) gauge 60 - + # HELP gauge_labels help # TYPE gauge_labels gauge # Drop in the gauge (value 3, counted 5 times) gauge_labels{label_one="a"} 60 - + # HELP gauge_user help # TYPE gauge_user gauge # User 3 is now missing. @@ -848,7 +848,7 @@ func TestUserRegistries_RemoveUserRegistry_HardRemoval(t *testing.T) { gauge_user{user="2"} 10 gauge_user{user="4"} 20 gauge_user{user="5"} 25 - + # HELP histogram help # TYPE histogram histogram # Histogram drop (reset for sum and count!) @@ -858,7 +858,7 @@ func TestUserRegistries_RemoveUserRegistry_HardRemoval(t *testing.T) { histogram_bucket{le="+Inf"} 20 histogram_sum 60 histogram_count 20 - + # HELP histogram_labels help # TYPE histogram_labels histogram # No change in the histogram per label @@ -868,19 +868,19 @@ func TestUserRegistries_RemoveUserRegistry_HardRemoval(t *testing.T) { histogram_labels_bucket{label_one="a",le="+Inf"} 20 histogram_labels_sum{label_one="a"} 60 histogram_labels_count{label_one="a"} 20 - + # HELP summary help # TYPE summary summary # Summary drop! summary_sum 60 summary_count 20 - + # HELP summary_labels help # TYPE summary_labels summary # Summary drop! summary_labels_sum{label_one="a"} 60 summary_labels_count{label_one="a"} 20 - + # HELP summary_user help # TYPE summary_user summary # Summary for user 3 is now missing. @@ -909,12 +909,12 @@ func TestUserRegistries_AddUserRegistry_ReplaceRegistry(t *testing.T) { # TYPE counter counter # No change in counter counter 75 - + # HELP counter_labels help # TYPE counter_labels counter # No change in counter per label counter_labels{label_one="a"} 75 - + # HELP counter_user help # TYPE counter_user counter # Per-user counter now missing. @@ -922,17 +922,17 @@ func TestUserRegistries_AddUserRegistry_ReplaceRegistry(t *testing.T) { counter_user{user="2"} 10 counter_user{user="3"} 15 counter_user{user="4"} 20 - + # HELP gauge help # TYPE gauge gauge # Gauge drops by 25 (value for user 5, times 5) gauge 50 - + # HELP gauge_labels help # TYPE gauge_labels gauge # Gauge drops by 25 (value for user 5, times 5) gauge_labels{label_one="a"} 50 - + # HELP gauge_user help # TYPE gauge_user gauge # Gauge for user 5 is missing @@ -940,7 +940,7 @@ func TestUserRegistries_AddUserRegistry_ReplaceRegistry(t *testing.T) { gauge_user{user="2"} 10 gauge_user{user="3"} 15 gauge_user{user="4"} 20 - + # HELP histogram help # TYPE histogram histogram # No change in histogram @@ -950,7 +950,7 @@ func TestUserRegistries_AddUserRegistry_ReplaceRegistry(t *testing.T) { histogram_bucket{le="+Inf"} 25 histogram_sum 75 histogram_count 25 - + # HELP histogram_labels help # TYPE histogram_labels histogram # No change in histogram per label. @@ -960,19 +960,19 @@ func TestUserRegistries_AddUserRegistry_ReplaceRegistry(t *testing.T) { histogram_labels_bucket{label_one="a",le="+Inf"} 25 histogram_labels_sum{label_one="a"} 75 histogram_labels_count{label_one="a"} 25 - + # HELP summary help # TYPE summary summary # No change in summary summary_sum 75 summary_count 25 - + # HELP summary_labels help # TYPE summary_labels summary # No change in summary per label summary_labels_sum{label_one="a"} 75 summary_labels_count{label_one="a"} 25 - + # HELP summary_user help # TYPE summary_user summary # Summary for user 5 now zero (reset) @@ -1258,3 +1258,51 @@ func verifyLabels(t *testing.T, m prometheus.Collector, filter map[string]string require.Equal(t, expectedLabels, result) } + +func TestSendSumOfNativeHistograms(t *testing.T) { + user1Metric := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "test_native_histogram", + NativeHistogramBucketFactor: 1.1, + }) + user2Metric := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "test_native_histogram", + NativeHistogramBucketFactor: 1.1, + }) + + user1Metric.Observe(1.0) + user1Metric.Observe(2.0) + user1Metric.Observe(5.0) + user2Metric.Observe(3.0) + user2Metric.Observe(4.0) + + user1Reg := prometheus.NewRegistry() + user2Reg := prometheus.NewRegistry() + user1Reg.MustRegister(user1Metric) + user2Reg.MustRegister(user2Metric) + + regs := NewUserRegistries() + regs.AddUserRegistry("user-1", user1Reg) + regs.AddUserRegistry("user-2", user2Reg) + mf := regs.BuildMetricFamiliesPerUser() + + desc := prometheus.NewDesc("test_native_histogram", "", nil, nil) + actual := collectMetrics(t, func(out chan prometheus.Metric) { + mf.SendSumOfHistograms(out, desc, "test_native_histogram") + }) + + require.Len(t, actual, 1) + h := actual[0].GetHistogram() + require.NotNil(t, h) + + require.Equal(t, uint64(5), h.GetSampleCount()) + require.Equal(t, 15.0, h.GetSampleSum()) + + require.NotNil(t, h.Schema) + require.True(t, + len(h.GetPositiveSpan()) > 0 || + len(h.GetNegativeSpan()) > 0 || + h.GetZeroCount() > 0 || + h.GetZeroThreshold() > 0, + "expected a native histogram output (spans/zero bucket present)", + ) +}