From a560cb6bce36dfed39bb00b6a5e35b0bf1cfdb48 Mon Sep 17 00:00:00 2001 From: Zhe Chen Date: Fri, 16 Oct 2026 15:21:40 +0800 Subject: [PATCH] Count NaN histogram observations in the overflow bucket Signed-off-by: Zhe Chen --- prometheus_client/metrics.py | 4 ++++ tests/test_core.py | 13 +++++++++++++ tests/test_multiprocess.py | 13 +++++++++++++ 3 files changed, 30 insertions(+) diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index dfa158b9..3b58e2ba 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -680,6 +680,8 @@ def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> N observations. See https://prometheus.io/docs/practices/histograms/#count-and-sum-of-observations for details. + + NaN observations are counted in the +Inf bucket and make the sum NaN. """ self._raise_if_not_observable() self._sum.inc(amount) @@ -690,6 +692,8 @@ def observe(self, amount: float, exemplar: Optional[Dict[str, str]] = None) -> N _validate_exemplar(exemplar) self._buckets[i].set_exemplar(Exemplar(exemplar, amount, time.time())) break + else: + self._buckets[-1].inc(1) def time(self) -> Timer: """Time a block of code or function, and observe the duration in seconds. diff --git a/tests/test_core.py b/tests/test_core.py index 2c29f79a..614e6873 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,5 @@ from concurrent.futures import ThreadPoolExecutor +import math import os from threading import Barrier, Event import time @@ -445,6 +446,18 @@ def test_histogram(self): self.assertEqual(3, self.registry.get_sample_value('h_count')) self.assertEqual(float("inf"), self.registry.get_sample_value('h_sum')) + def test_histogram_nan(self): + self.histogram.observe(2) + self.histogram.observe(float('nan')) + self.histogram.observe(3) + + self.assertEqual(0, self.registry.get_sample_value('h_bucket', {'le': '1.0'})) + self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': '2.5'})) + self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'le': '5.0'})) + self.assertEqual(3, self.registry.get_sample_value('h_bucket', {'le': '+Inf'})) + self.assertEqual(3, self.registry.get_sample_value('h_count')) + self.assertTrue(math.isnan(self.registry.get_sample_value('h_sum'))) + def test_histogram_not_observable(self): """.observe() must fail if the Summary is not observable.""" assert_not_observable(self.labels.observe, 1) diff --git a/tests/test_multiprocess.py b/tests/test_multiprocess.py index d7cfe3f2..b673712f 100644 --- a/tests/test_multiprocess.py +++ b/tests/test_multiprocess.py @@ -1,4 +1,5 @@ import glob +import math import os import shutil import tempfile @@ -109,6 +110,18 @@ def test_histogram_adds(self): self.assertEqual(3, self.registry.get_sample_value('h_sum')) self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'le': '5.0'})) + def test_histogram_nan(self): + h1 = Histogram('h', 'help', registry=None) + values.ValueClass = MultiProcessValue(lambda: 456) + h2 = Histogram('h', 'help', registry=None) + h1.observe(float('nan')) + h2.observe(2) + + self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': '5.0'})) + self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'le': '+Inf'})) + self.assertEqual(2, self.registry.get_sample_value('h_count')) + self.assertTrue(math.isnan(self.registry.get_sample_value('h_sum'))) + def test_gauge_all(self): g1 = Gauge('g', 'help', registry=None) values.ValueClass = MultiProcessValue(lambda: 456)