From 775ef5493af82b23a8aa636919f809d2d08ee555 Mon Sep 17 00:00:00 2001 From: Arnab Nandy Date: Fri, 3 Jul 2026 18:14:43 +0530 Subject: [PATCH] Fix native histogram bucket index merge on scale-down Signed-off-by: Arnab Nandy --- .../metrics/core/metrics/Histogram.java | 2 +- .../metrics/core/metrics/HistogramTest.java | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java index 9a7f9b7c9..c4bb1f5fe 100644 --- a/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java +++ b/prometheus-metrics-core/src/main/java/io/prometheus/metrics/core/metrics/Histogram.java @@ -614,7 +614,7 @@ private void doubleBucketWidth(Map buckets) { } buckets.clear(); for (i = 0; i < keys.length; i++) { - int index = (keys[i] + 1) / 2; + int index = (keys[i] > 0 ? keys[i] + 1 : keys[i]) / 2; LongAdder count = buckets.computeIfAbsent(index, k -> new LongAdder()); count.add(values[i]); } diff --git a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java index 69518205d..cbfd5fade 100644 --- a/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java +++ b/prometheus-metrics-core/src/test/java/io/prometheus/metrics/core/metrics/HistogramTest.java @@ -28,7 +28,9 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Random; import java.util.concurrent.CompletionService; import java.util.concurrent.CountDownLatch; @@ -39,6 +41,7 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.LongAdder; import java.util.stream.Collectors; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -836,6 +839,36 @@ void testNativeBucketIndexToUpperBound() } } + @Test + void testDoubleBucketWidthMergesNegativeIndexesCorrectly() + throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { + Histogram histogram = Histogram.builder().name("test").nativeOnly().build(); + Histogram.DataPoint dataPoint = histogram.newDataPoint(); + + Method doubleBucketWidth = + Histogram.DataPoint.class.getDeclaredMethod("doubleBucketWidth", Map.class); + doubleBucketWidth.setAccessible(true); + + int[] keys = {-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5}; + int[] expectedMergedIndexes = {-2, -2, -1, -1, 0, 0, 1, 1, 2, 2, 3}; + + for (int i = 0; i < keys.length; i++) { + Map buckets = new HashMap<>(); + LongAdder adder = new LongAdder(); + adder.add(7); + buckets.put(keys[i], adder); + + doubleBucketWidth.invoke(dataPoint, buckets); + + assertThat(buckets.keySet()) + .as("merged index for original key " + keys[i]) + .containsExactly(expectedMergedIndexes[i]); + assertThat(buckets.get(expectedMergedIndexes[i]).sum()) + .as("count preserved for original key " + keys[i]) + .isEqualTo(7); + } + } + /** * Test if lowerBound < value <= upperBound is true for the bucket index returned by * findBucketIndex()