From 8426f967f35c73ac5e728872c0451f84eae692f6 Mon Sep 17 00:00:00 2001 From: Haegan McGarry Date: Mon, 9 Nov 2026 16:17:18 -0800 Subject: [PATCH] Omit _sum for negative-bucket histograms in multiprocess mode Histogram leaves out _sum when its lowest bucket is negative, since OpenMetrics 1.0 doesn't allow a sum there. MultiProcessCollector builds its own histogram samples and kept the _sum from the mmap files, so the same histogram exposed an extra series in multiprocess mode. Drop _sum on the accumulate path when the lowest bucket bound is negative. merge(accumulate=False) still returns the raw value. Signed-off-by: Haegan McGarry --- docs/content/instrumenting/histogram.md | 3 +++ prometheus_client/multiprocess.py | 3 +++ tests/test_multiprocess.py | 12 ++++++++++++ 3 files changed, 18 insertions(+) diff --git a/docs/content/instrumenting/histogram.md b/docs/content/instrumenting/histogram.md index fa0ffe1a..39edc0bc 100644 --- a/docs/content/instrumenting/histogram.md +++ b/docs/content/instrumenting/histogram.md @@ -18,6 +18,9 @@ A Histogram exposes three time series per metric: - `_sum` — sum of all observed values - `_count` — total number of observations +If the lowest bucket bound is negative, `_sum` is left out, because OpenMetrics +1.0 does not allow a sum for histograms with negative buckets. + ## Constructor ```python diff --git a/prometheus_client/multiprocess.py b/prometheus_client/multiprocess.py index 046bb2c2..b6258b74 100644 --- a/prometheus_client/multiprocess.py +++ b/prometheus_client/multiprocess.py @@ -158,6 +158,9 @@ def _accumulate_metrics(metrics, accumulate): samples[labels][sample_key] = value if accumulate: samples[labels][(metric.name + '_count', labels)] = acc + # Like Histogram, don't include sum if there are negative buckets. + if min(values) < 0: + samples[labels].pop((metric.name + '_sum', labels), None) # Convert to correct sample format. metric.samples = [] diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py index c0acfadd..869a3a45 100644 --- a/tests/test_multiprocess.py +++ b/tests/test_multiprocess.py @@ -123,6 +123,18 @@ def test_histogram_nan(self): self.assertEqual(2, self.registry.get_sample_value('h_count')) self.assertTrue(math.isnan(self.registry.get_sample_value('h_sum'))) + def test_histogram_negative_buckets(self): + h1 = Histogram('h', 'help', labelnames=['l'], buckets=[-1, 0, 1], registry=None) + values.ValueClass = MultiProcessValue(lambda: 456) + h2 = Histogram('h', 'help', labelnames=['l'], buckets=[-1, 0, 1], registry=None) + h1.labels('x').observe(-0.5) + h2.labels('x').observe(2) + + self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'l': 'x', 'le': '0.0'})) + self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'l': 'x', 'le': '+Inf'})) + self.assertEqual(2, self.registry.get_sample_value('h_count', {'l': 'x'})) + self.assertIsNone(self.registry.get_sample_value('h_sum', {'l': 'x'})) + def test_gauge_all(self): g1 = Gauge('g', 'help', registry=None) values.ValueClass = MultiProcessValue(lambda: 456)