Skip to content

Commit 4884cb3

Browse files
wolphbrian-brazil
authored andcommitted
Added metrics to MetricFamily exceptions to fix #362 (#364)
Signed-off-by: Rick van Hattem <[email protected]>
1 parent f5e818a commit 4884cb3

File tree

3 files changed

+165
-74
lines changed

3 files changed

+165
-74
lines changed

prometheus_client/exposition.py

+35-30
Original file line numberDiff line numberDiff line change
@@ -87,36 +87,41 @@ def sample_line(s):
8787

8888
output = []
8989
for metric in registry.collect():
90-
mname = metric.name
91-
mtype = metric.type
92-
# Munging from OpenMetrics into Prometheus format.
93-
if mtype == 'counter':
94-
mname = mname + '_total'
95-
elif mtype == 'info':
96-
mname = mname + '_info'
97-
mtype = 'gauge'
98-
elif mtype == 'stateset':
99-
mtype = 'gauge'
100-
elif mtype == 'gaugehistogram':
101-
# A gauge histogram is really a gauge,
102-
# but this captures the strucutre better.
103-
mtype = 'histogram'
104-
elif mtype == 'unknown':
105-
mtype = 'untyped'
106-
107-
output.append('# HELP {0} {1}\n'.format(
108-
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
109-
output.append('# TYPE {0} {1}\n'.format(mname, mtype))
110-
111-
om_samples = {}
112-
for s in metric.samples:
113-
for suffix in ['_created', '_gsum', '_gcount']:
114-
if s.name == metric.name + suffix:
115-
# OpenMetrics specific sample, put in a gauge at the end.
116-
om_samples.setdefault(suffix, []).append(sample_line(s))
117-
break
118-
else:
119-
output.append(sample_line(s))
90+
try:
91+
mname = metric.name
92+
mtype = metric.type
93+
# Munging from OpenMetrics into Prometheus format.
94+
if mtype == 'counter':
95+
mname = mname + '_total'
96+
elif mtype == 'info':
97+
mname = mname + '_info'
98+
mtype = 'gauge'
99+
elif mtype == 'stateset':
100+
mtype = 'gauge'
101+
elif mtype == 'gaugehistogram':
102+
# A gauge histogram is really a gauge,
103+
# but this captures the strucutre better.
104+
mtype = 'histogram'
105+
elif mtype == 'unknown':
106+
mtype = 'untyped'
107+
108+
output.append('# HELP {0} {1}\n'.format(
109+
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
110+
output.append('# TYPE {0} {1}\n'.format(mname, mtype))
111+
112+
om_samples = {}
113+
for s in metric.samples:
114+
for suffix in ['_created', '_gsum', '_gcount']:
115+
if s.name == metric.name + suffix:
116+
# OpenMetrics specific sample, put in a gauge at the end.
117+
om_samples.setdefault(suffix, []).append(sample_line(s))
118+
break
119+
else:
120+
output.append(sample_line(s))
121+
except Exception as exception:
122+
exception.args = (exception.args or ('',)) + (metric,)
123+
raise
124+
120125
for suffix, lines in sorted(om_samples.items()):
121126
output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix))
122127
output.extend(lines)

prometheus_client/openmetrics/exposition.py

+48-43
Original file line numberDiff line numberDiff line change
@@ -12,49 +12,54 @@ def generate_latest(registry):
1212
'''Returns the metrics from the registry in latest text format as a string.'''
1313
output = []
1414
for metric in registry.collect():
15-
mname = metric.name
16-
output.append('# HELP {0} {1}\n'.format(
17-
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')))
18-
output.append('# TYPE {0} {1}\n'.format(mname, metric.type))
19-
if metric.unit:
20-
output.append('# UNIT {0} {1}\n'.format(mname, metric.unit))
21-
for s in metric.samples:
22-
if s.labels:
23-
labelstr = '{{{0}}}'.format(','.join(
24-
['{0}="{1}"'.format(
25-
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
26-
for k, v in sorted(s.labels.items())]))
27-
else:
28-
labelstr = ''
29-
if s.exemplar:
30-
if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'):
31-
raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name))
32-
labels = '{{{0}}}'.format(','.join(
33-
['{0}="{1}"'.format(
34-
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
35-
for k, v in sorted(s.exemplar.labels.items())]))
36-
if s.exemplar.timestamp is not None:
37-
exemplarstr = ' # {0} {1} {2}'.format(
38-
labels,
39-
floatToGoString(s.exemplar.value),
40-
s.exemplar.timestamp,
41-
)
15+
try:
16+
mname = metric.name
17+
output.append('# HELP {0} {1}\n'.format(
18+
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"')))
19+
output.append('# TYPE {0} {1}\n'.format(mname, metric.type))
20+
if metric.unit:
21+
output.append('# UNIT {0} {1}\n'.format(mname, metric.unit))
22+
for s in metric.samples:
23+
if s.labels:
24+
labelstr = '{{{0}}}'.format(','.join(
25+
['{0}="{1}"'.format(
26+
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
27+
for k, v in sorted(s.labels.items())]))
4228
else:
43-
exemplarstr = ' # {0} {1}'.format(
44-
labels,
45-
floatToGoString(s.exemplar.value),
46-
)
47-
else:
48-
exemplarstr = ''
49-
timestamp = ''
50-
if s.timestamp is not None:
51-
timestamp = ' {0}'.format(s.timestamp)
52-
output.append('{0}{1} {2}{3}{4}\n'.format(
53-
s.name,
54-
labelstr,
55-
floatToGoString(s.value),
56-
timestamp,
57-
exemplarstr,
58-
))
29+
labelstr = ''
30+
if s.exemplar:
31+
if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'):
32+
raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name))
33+
labels = '{{{0}}}'.format(','.join(
34+
['{0}="{1}"'.format(
35+
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
36+
for k, v in sorted(s.exemplar.labels.items())]))
37+
if s.exemplar.timestamp is not None:
38+
exemplarstr = ' # {0} {1} {2}'.format(
39+
labels,
40+
floatToGoString(s.exemplar.value),
41+
s.exemplar.timestamp,
42+
)
43+
else:
44+
exemplarstr = ' # {0} {1}'.format(
45+
labels,
46+
floatToGoString(s.exemplar.value),
47+
)
48+
else:
49+
exemplarstr = ''
50+
timestamp = ''
51+
if s.timestamp is not None:
52+
timestamp = ' {0}'.format(s.timestamp)
53+
output.append('{0}{1} {2}{3}{4}\n'.format(
54+
s.name,
55+
labelstr,
56+
floatToGoString(s.value),
57+
timestamp,
58+
exemplarstr,
59+
))
60+
except Exception as exception:
61+
exception.args = (exception.args or ('',)) + (metric,)
62+
raise
63+
5964
output.append('# EOF\n')
6065
return ''.join(output).encode('utf-8')

tests/test_exposition.py

+82-1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import sys
44
import threading
55
import time
6+
import pytest
67

78
from prometheus_client import (
89
CollectorRegistry, CONTENT_TYPE_LATEST, Counter, delete_from_gateway, Enum,
@@ -11,8 +12,9 @@
1112
)
1213
from prometheus_client.core import GaugeHistogramMetricFamily, Timestamp
1314
from prometheus_client.exposition import (
14-
basic_auth_handler, default_handler, MetricsHandler,
15+
basic_auth_handler, default_handler, MetricsHandler, generate_latest,
1516
)
17+
from prometheus_client import core
1618

1719
if sys.version_info < (2, 7):
1820
# We need the skip decorators from unittest2 on Python 2.6.
@@ -303,5 +305,84 @@ def test_metrics_handler_subclassing(self):
303305
self.assertTrue(issubclass(handler, (MetricsHandler, subclass)))
304306

305307

308+
@pytest.fixture
309+
def registry():
310+
return core.CollectorRegistry()
311+
312+
313+
class Collector:
314+
def __init__(self, metric_family, *values):
315+
self.metric_family = metric_family
316+
self.values = values
317+
318+
def collect(self):
319+
self.metric_family.add_metric([], *self.values)
320+
return [self.metric_family]
321+
322+
323+
def _expect_metric_exception(registry, expected_error):
324+
try:
325+
generate_latest(registry)
326+
except expected_error as exception:
327+
assert isinstance(exception.args[-1], core.Metric)
328+
# Got a valid error as expected, return quietly
329+
return
330+
331+
raise RuntimeError('Expected exception not raised')
332+
333+
334+
@pytest.mark.parametrize('MetricFamily', [
335+
core.CounterMetricFamily,
336+
core.GaugeMetricFamily,
337+
])
338+
@pytest.mark.parametrize('value,error', [
339+
(None, TypeError),
340+
('', ValueError),
341+
('x', ValueError),
342+
([], TypeError),
343+
({}, TypeError),
344+
])
345+
def test_basic_metric_families(registry, MetricFamily, value, error):
346+
metric_family = MetricFamily(MetricFamily.__name__, 'help')
347+
registry.register(Collector(metric_family, value))
348+
_expect_metric_exception(registry, error)
349+
350+
351+
@pytest.mark.parametrize('count_value,sum_value,error', [
352+
(None, 0, TypeError),
353+
(0, None, TypeError),
354+
('', 0, ValueError),
355+
(0, '', ValueError),
356+
([], 0, TypeError),
357+
(0, [], TypeError),
358+
({}, 0, TypeError),
359+
(0, {}, TypeError),
360+
])
361+
def test_summary_metric_family(registry, count_value, sum_value, error):
362+
metric_family = core.SummaryMetricFamily('summary', 'help')
363+
registry.register(Collector(metric_family, count_value, sum_value))
364+
_expect_metric_exception(registry, error)
365+
366+
367+
@pytest.mark.parametrize('MetricFamily', [
368+
core.HistogramMetricFamily,
369+
core.GaugeHistogramMetricFamily,
370+
])
371+
@pytest.mark.parametrize('buckets,sum_value,error', [
372+
([('spam', 0), ('eggs', 0)], None, TypeError),
373+
([('spam', 0), ('eggs', None)], 0, TypeError),
374+
([('spam', 0), (None, 0)], 0, AttributeError),
375+
([('spam', None), ('eggs', 0)], 0, TypeError),
376+
([(None, 0), ('eggs', 0)], 0, AttributeError),
377+
([('spam', 0), ('eggs', 0)], '', ValueError),
378+
([('spam', 0), ('eggs', '')], 0, ValueError),
379+
([('spam', ''), ('eggs', 0)], 0, ValueError),
380+
])
381+
def test_histogram_metric_families(MetricFamily, registry, buckets, sum_value, error):
382+
metric_family = MetricFamily(MetricFamily.__name__, 'help')
383+
registry.register(Collector(metric_family, buckets, sum_value))
384+
_expect_metric_exception(registry, error)
385+
386+
306387
if __name__ == '__main__':
307388
unittest.main()

0 commit comments

Comments
 (0)