Skip to content

Commit fa7efa1

Browse files
authored
Coerce label values to strings in MetricFamily add_metric (#1221)
* 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 <christopher.pruijsen@gmail.com> * Coerce Info and StateSet dict values to strings and widen hints InfoMetricFamily label values passed in the value dict and StateSetMetricFamily state names were still stored verbatim, so ints there produced samples with non-string labels. Coerce them to str too. Since add_metric now accepts any stringable label value, type the label values as Sequence[object], the info dict as Mapping[str, object] and the stateset dict as Mapping[Any, bool]. Signed-off-by: Christopher Pruijsen <christopher.pruijsen@gmail.com> --------- Signed-off-by: Christopher Pruijsen <christopher.pruijsen@gmail.com>
1 parent 5a47ba6 commit fa7efa1

2 files changed

Lines changed: 86 additions & 29 deletions

File tree

‎prometheus_client/metrics_core.py‎

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Optional, Sequence, Tuple, Union
1+
from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple, Union
22

33
from .samples import Exemplar, NativeHistogram, Sample, Timestamp
44
from .validation import _validate_metric_name
@@ -86,13 +86,14 @@ def __init__(self,
8686
if value is not None:
8787
self.add_metric([], value)
8888

89-
def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None:
89+
def add_metric(self, labels: Sequence[object], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None:
9090
"""Add a metric to the metric family.
9191
Args:
9292
labels: A list of label values
9393
value: The value of the metric.
9494
"""
95-
self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
95+
label_values = tuple(str(l) for l in labels)
96+
self.samples.append(Sample(self.name, dict(zip(self._labelnames, label_values)), value, timestamp))
9697

9798

9899
# For backward compatibility.
@@ -127,7 +128,7 @@ def __init__(self,
127128
self.add_metric([], value, created, exemplar=exemplar)
128129

129130
def add_metric(self,
130-
labels: Sequence[str],
131+
labels: Sequence[object],
131132
value: float,
132133
created: Optional[float] = None,
133134
timestamp: Optional[Union[Timestamp, float]] = None,
@@ -140,9 +141,10 @@ def add_metric(self,
140141
value: The value of the metric
141142
created: Optional unix timestamp the child was created at.
142143
"""
143-
self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, labels)), value, timestamp, exemplar))
144+
label_values = tuple(str(l) for l in labels)
145+
self.samples.append(Sample(self.name + '_total', dict(zip(self._labelnames, label_values)), value, timestamp, exemplar))
144146
if created is not None:
145-
self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, labels)), created, timestamp))
147+
self.samples.append(Sample(self.name + '_created', dict(zip(self._labelnames, label_values)), created, timestamp))
146148

147149

148150
class GaugeMetricFamily(Metric):
@@ -167,14 +169,15 @@ def __init__(self,
167169
if value is not None:
168170
self.add_metric([], value)
169171

170-
def add_metric(self, labels: Sequence[str], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None:
172+
def add_metric(self, labels: Sequence[object], value: float, timestamp: Optional[Union[Timestamp, float]] = None) -> None:
171173
"""Add a metric to the metric family.
172174
173175
Args:
174176
labels: A list of label values
175177
value: A float
176178
"""
177-
self.samples.append(Sample(self.name, dict(zip(self._labelnames, labels)), value, timestamp))
179+
label_values = tuple(str(l) for l in labels)
180+
self.samples.append(Sample(self.name, dict(zip(self._labelnames, label_values)), value, timestamp))
178181

179182

180183
class SummaryMetricFamily(Metric):
@@ -204,7 +207,7 @@ def __init__(self,
204207
self.add_metric([], count_value, sum_value)
205208

206209
def add_metric(self,
207-
labels: Sequence[str],
210+
labels: Sequence[object],
208211
count_value: int,
209212
sum_value: float,
210213
timestamp:
@@ -217,8 +220,9 @@ def add_metric(self,
217220
count_value: The count value of the metric.
218221
sum_value: The sum value of the metric.
219222
"""
220-
self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, labels)), count_value, timestamp))
221-
self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
223+
label_values = tuple(str(l) for l in labels)
224+
self.samples.append(Sample(self.name + '_count', dict(zip(self._labelnames, label_values)), count_value, timestamp))
225+
self.samples.append(Sample(self.name + '_sum', dict(zip(self._labelnames, label_values)), sum_value, timestamp))
222226

223227

224228
class HistogramMetricFamily(Metric):
@@ -247,7 +251,7 @@ def __init__(self,
247251
self.add_metric([], buckets, sum_value)
248252

249253
def add_metric(self,
250-
labels: Sequence[str],
254+
labels: Sequence[object],
251255
buckets: Sequence[Union[Tuple[str, float], Tuple[str, float, Exemplar]]],
252256
sum_value: Optional[float],
253257
timestamp: Optional[Union[Timestamp, float]] = None) -> None:
@@ -261,14 +265,15 @@ def add_metric(self,
261265
The buckets must be sorted, and +Inf present.
262266
sum_value: The sum value of the metric.
263267
"""
268+
label_values = tuple(str(l) for l in labels)
264269
for b in buckets:
265270
bucket, value = b[:2]
266271
exemplar = None
267272
if len(b) == 3:
268273
exemplar = b[2] # type: ignore
269274
self.samples.append(Sample(
270275
self.name + '_bucket',
271-
dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
276+
dict(list(zip(self._labelnames, label_values)) + [('le', bucket)]),
272277
value,
273278
timestamp,
274279
exemplar,
@@ -277,9 +282,9 @@ def add_metric(self,
277282
if float(buckets[0][0]) >= 0 and sum_value is not None:
278283
# +Inf is last and provides the count value.
279284
self.samples.append(
280-
Sample(self.name + '_count', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp))
285+
Sample(self.name + '_count', dict(zip(self._labelnames, label_values)), buckets[-1][1], timestamp))
281286
self.samples.append(
282-
Sample(self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value, timestamp))
287+
Sample(self.name + '_sum', dict(zip(self._labelnames, label_values)), sum_value, timestamp))
283288

284289

285290
class GaugeHistogramMetricFamily(Metric):
@@ -306,7 +311,7 @@ def __init__(self,
306311
self.add_metric([], buckets, gsum_value)
307312

308313
def add_metric(self,
309-
labels: Sequence[str],
314+
labels: Sequence[object],
310315
buckets: Sequence[Tuple[str, float]],
311316
gsum_value: Optional[float],
312317
timestamp: Optional[Union[float, Timestamp]] = None,
@@ -319,16 +324,17 @@ def add_metric(self,
319324
The buckets must be sorted, and +Inf present.
320325
gsum_value: The sum value of the metric.
321326
"""
327+
label_values = tuple(str(l) for l in labels)
322328
for bucket, value in buckets:
323329
self.samples.append(Sample(
324330
self.name + '_bucket',
325-
dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
331+
dict(list(zip(self._labelnames, label_values)) + [('le', bucket)]),
326332
value, timestamp))
327333
# +Inf is last and provides the count value.
328334
self.samples.extend([
329-
Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp),
335+
Sample(self.name + '_gcount', dict(zip(self._labelnames, label_values)), buckets[-1][1], timestamp),
330336
# TODO: Handle None gsum_value correctly. Currently a None will fail exposition but is allowed here.
331-
Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp), # type: ignore
337+
Sample(self.name + '_gsum', dict(zip(self._labelnames, label_values)), gsum_value, timestamp), # type: ignore
332338
])
333339

334340

@@ -341,7 +347,7 @@ class InfoMetricFamily(Metric):
341347
def __init__(self,
342348
name: str,
343349
documentation: str,
344-
value: Optional[Dict[str, str]] = None,
350+
value: Optional[Mapping[str, object]] = None,
345351
labels: Optional[Sequence[str]] = None,
346352
):
347353
Metric.__init__(self, name, documentation, 'info')
@@ -354,8 +360,8 @@ def __init__(self,
354360
self.add_metric([], value)
355361

356362
def add_metric(self,
357-
labels: Sequence[str],
358-
value: Dict[str, str],
363+
labels: Sequence[object],
364+
value: Mapping[str, object],
359365
timestamp: Optional[Union[Timestamp, float]] = None,
360366
) -> None:
361367
"""Add a metric to the metric family.
@@ -364,9 +370,10 @@ def add_metric(self,
364370
labels: A list of label values
365371
value: A dict of labels
366372
"""
373+
label_values = tuple(str(l) for l in labels)
367374
self.samples.append(Sample(
368375
self.name + '_info',
369-
dict(dict(zip(self._labelnames, labels)), **value),
376+
dict(dict(zip(self._labelnames, label_values)), **{k: str(v) for k, v in value.items()}),
370377
1,
371378
timestamp,
372379
))
@@ -381,7 +388,7 @@ class StateSetMetricFamily(Metric):
381388
def __init__(self,
382389
name: str,
383390
documentation: str,
384-
value: Optional[Dict[str, bool]] = None,
391+
value: Optional[Mapping[Any, bool]] = None,
385392
labels: Optional[Sequence[str]] = None,
386393
):
387394
Metric.__init__(self, name, documentation, 'stateset')
@@ -394,8 +401,8 @@ def __init__(self,
394401
self.add_metric([], value)
395402

396403
def add_metric(self,
397-
labels: Sequence[str],
398-
value: Dict[str, bool],
404+
labels: Sequence[object],
405+
value: Mapping[Any, bool],
399406
timestamp: Optional[Union[Timestamp, float]] = None,
400407
) -> None:
401408
"""Add a metric to the metric family.
@@ -404,12 +411,12 @@ def add_metric(self,
404411
labels: A list of label values
405412
value: A dict of string state names to booleans
406413
"""
407-
labels = tuple(labels)
408-
for state, enabled in sorted(value.items()):
414+
label_values = tuple(str(l) for l in labels)
415+
for state, enabled in sorted((str(s), e) for s, e in value.items()):
409416
v = (1 if enabled else 0)
410417
self.samples.append(Sample(
411418
self.name,
412-
dict(zip(self._labelnames + (self.name,), labels + (state,))),
419+
dict(zip(self._labelnames + (self.name,), label_values + (state,))),
413420
v,
414421
timestamp,
415422
))

‎tests/test_core.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
StateSetMetricFamily, Summary, SummaryMetricFamily, UntypedMetricFamily,
1515
)
1616
from prometheus_client.decorator import getargspec
17+
from prometheus_client.exposition import generate_latest
1718
from prometheus_client.metrics import _get_use_created
1819
from prometheus_client.validation import (
1920
disable_legacy_validation, enable_legacy_validation,
@@ -984,6 +985,55 @@ def test_stateset_labels(self):
984985
self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'a'}))
985986
self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'b'}))
986987

988+
def test_labels_coerced_to_string(self):
989+
umf = UntypedMetricFamily('u', 'help', labels=['a'])
990+
umf.add_metric([1], 2)
991+
self.custom_collector(umf)
992+
self.assertEqual(2, self.registry.get_sample_value('u', {'a': '1'}))
993+
994+
cmf = CounterMetricFamily('c', 'help', labels=['a'])
995+
cmf.add_metric([1], 2, created=3)
996+
self.custom_collector(cmf)
997+
self.assertEqual(2, self.registry.get_sample_value('c_total', {'a': '1'}))
998+
self.assertEqual(3, self.registry.get_sample_value('c_created', {'a': '1'}))
999+
1000+
gmf = GaugeMetricFamily('g', 'help', labels=['a'])
1001+
gmf.add_metric([1], 2)
1002+
self.custom_collector(gmf)
1003+
self.assertEqual(2, self.registry.get_sample_value('g', {'a': '1'}))
1004+
1005+
smf = SummaryMetricFamily('s', 'help', labels=['a'])
1006+
smf.add_metric([1], count_value=1, sum_value=2)
1007+
self.custom_collector(smf)
1008+
self.assertEqual(1, self.registry.get_sample_value('s_count', {'a': '1'}))
1009+
self.assertEqual(2, self.registry.get_sample_value('s_sum', {'a': '1'}))
1010+
1011+
hmf = HistogramMetricFamily('h', 'help', labels=['a'])
1012+
hmf.add_metric([1], buckets=[('0', 1), ('+Inf', 2)], sum_value=3)
1013+
self.custom_collector(hmf)
1014+
self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'a': '1', 'le': '0'}))
1015+
self.assertEqual(2, self.registry.get_sample_value('h_count', {'a': '1'}))
1016+
self.assertEqual(3, self.registry.get_sample_value('h_sum', {'a': '1'}))
1017+
1018+
ghmf = GaugeHistogramMetricFamily('gh', 'help', labels=['a'])
1019+
ghmf.add_metric([1], buckets=[('0', 1), ('+Inf', 2)], gsum_value=3)
1020+
self.custom_collector(ghmf)
1021+
self.assertEqual(1, self.registry.get_sample_value('gh_bucket', {'a': '1', 'le': '0'}))
1022+
self.assertEqual(2, self.registry.get_sample_value('gh_gcount', {'a': '1'}))
1023+
self.assertEqual(3, self.registry.get_sample_value('gh_gsum', {'a': '1'}))
1024+
1025+
imf = InfoMetricFamily('i', 'help', labels=['a'])
1026+
imf.add_metric([1], {'c': 2})
1027+
self.custom_collector(imf)
1028+
self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': '1', 'c': '2'}))
1029+
1030+
ssf = StateSetMetricFamily('ss', 'help', labels=['a'])
1031+
ssf.add_metric([1], {3: True})
1032+
self.custom_collector(ssf)
1033+
self.assertEqual(1, self.registry.get_sample_value('ss', {'a': '1', 'ss': '3'}))
1034+
1035+
self.assertIn(b'g{a="1"} 2.0', generate_latest(self.registry))
1036+
9871037
def test_bad_constructors(self):
9881038
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[])
9891039
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])

0 commit comments

Comments
 (0)