From 14add3f4119d72ac59741d79994e3d59254b04d3 Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Wed, 14 Oct 2026 00:54:40 +0100 Subject: [PATCH] Coerce label values to strings in MetricFamily add_metric The custom collector MetricFamily classes stored label values verbatim, so non-string values such as ints crashed exposition with AttributeError: 'int' object has no attribute 'replace'. Coerce them to str on entry, matching what Metric.labels() already does for the standard metric types. Signed-off-by: Christopher Pruijsen --- prometheus_client/metrics_core.py | 9 +++++- tests/test_core.py | 50 +++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/prometheus_client/metrics_core.py b/prometheus_client/metrics_core.py index 27d1712d..d531206d 100644 --- a/prometheus_client/metrics_core.py +++ b/prometheus_client/metrics_core.py @@ -92,6 +92,7 @@ def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Un labels: A list of label values value: The value of the metric. """ + labels = tuple(str(l) for l in labels) self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp)) @@ -140,6 +141,7 @@ def add_metric(self, value: The value of the metric created: Optional unix timestamp the child was created at. """ + labels = tuple(str(l) for l in labels) self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp, exemplar)) if created is not None: self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp)) @@ -174,6 +176,7 @@ def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Un labels: A list of label values value: A float """ + labels = tuple(str(l) for l in labels) self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp)) @@ -217,6 +220,7 @@ def add_metric(self, count_value: The count value of the metric. sum_value: The sum value of the metric. """ + labels = tuple(str(l) for l in labels) self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value, timestamp)) self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp)) @@ -261,6 +265,7 @@ def add_metric(self, The buckets must be sorted, and +Inf present. sum_value: The sum value of the metric. """ + labels = tuple(str(l) for l in labels) for b in buckets: bucket, value = b[:2] exemplar = None @@ -319,6 +324,7 @@ def add_metric(self, The buckets must be sorted, and +Inf present. gsum_value: The sum value of the metric. """ + labels = tuple(str(l) for l in labels) for bucket, value in buckets: self.samples.append(Sample( self.name + '_bucket', @@ -364,6 +370,7 @@ def add_metric(self, labels: A list of label values value: A dict of labels """ + labels = tuple(str(l) for l in labels) self.samples.append(Sample( self.name + '_info', dict(dict(zip(self._labelnames, labels)), **value), @@ -404,7 +411,7 @@ def add_metric(self, labels: A list of label values value: A dict of string state names to booleans """ - labels = tuple(labels) + labels = tuple(str(l) for l in labels) for state, enabled in sorted(value.items()): v = (1 if enabled else 0) self.samples.append(Sample( diff --git a/tests/test_core.py b/tests/test_core.py index 3aa19c24..9a8c3757 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -13,6 +13,7 @@ StateSetMetricFamily, Summary, SummaryMetricFamily, UntypedMetricFamily, ) from prometheus_client.decorator import getargspec +from prometheus_client.exposition import generate_latest from prometheus_client.metrics import _get_use_created from prometheus_client.validation import ( disable_legacy_validation, enable_legacy_validation, @@ -889,6 +890,55 @@ def test_stateset_labels(self): self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'a'})) self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'b'})) + def test_labels_coerced_to_string(self): + umf = UntypedMetricFamily('u', 'help', labels=['a']) + umf.add_metric([1], 2) + self.custom_collector(umf) + self.assertEqual(2, self.registry.get_sample_value('u', {'a': '1'})) + + cmf = CounterMetricFamily('c', 'help', labels=['a']) + cmf.add_metric([1], 2, created=3) + self.custom_collector(cmf) + self.assertEqual(2, self.registry.get_sample_value('c_total', {'a': '1'})) + self.assertEqual(3, self.registry.get_sample_value('c_created', {'a': '1'})) + + gmf = GaugeMetricFamily('g', 'help', labels=['a']) + gmf.add_metric([1], 2) + self.custom_collector(gmf) + self.assertEqual(2, self.registry.get_sample_value('g', {'a': '1'})) + + smf = SummaryMetricFamily('s', 'help', labels=['a']) + smf.add_metric([1], count_value=1, sum_value=2) + self.custom_collector(smf) + self.assertEqual(1, self.registry.get_sample_value('s_count', {'a': '1'})) + self.assertEqual(2, self.registry.get_sample_value('s_sum', {'a': '1'})) + + hmf = HistogramMetricFamily('h', 'help', labels=['a']) + hmf.add_metric([1], buckets=[('0', 1), ('+Inf', 2)], sum_value=3) + self.custom_collector(hmf) + self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'a': '1', 'le': '0'})) + self.assertEqual(2, self.registry.get_sample_value('h_count', {'a': '1'})) + self.assertEqual(3, self.registry.get_sample_value('h_sum', {'a': '1'})) + + ghmf = GaugeHistogramMetricFamily('gh', 'help', labels=['a']) + ghmf.add_metric([1], buckets=[('0', 1), ('+Inf', 2)], gsum_value=3) + self.custom_collector(ghmf) + self.assertEqual(1, self.registry.get_sample_value('gh_bucket', {'a': '1', 'le': '0'})) + self.assertEqual(2, self.registry.get_sample_value('gh_gcount', {'a': '1'})) + self.assertEqual(3, self.registry.get_sample_value('gh_gsum', {'a': '1'})) + + imf = InfoMetricFamily('i', 'help', labels=['a']) + imf.add_metric([1], {'c': 'd'}) + self.custom_collector(imf) + self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': '1', 'c': 'd'})) + + ssf = StateSetMetricFamily('ss', 'help', labels=['a']) + ssf.add_metric([1], {'x': True}) + self.custom_collector(ssf) + self.assertEqual(1, self.registry.get_sample_value('ss', {'a': '1', 'ss': 'x'})) + + self.assertIn(b'g{a="1"} 2.0', generate_latest(self.registry)) + def test_bad_constructors(self): self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[]) self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])