8000 Added metrics to MetricFamily exceptions to fix #362 (#364) · rayandas/client_python@4884cb3 · GitHub
[go: up one dir, main page]

Skip to content

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 4884cb3

Browse files
wolphbrian-brazil
authored andcommitted
Added metrics to MetricFamily exceptions to fix prometheus#362 (prometheus#364)
Signed-off-by: Rick van Hattem <Wolph@wol.ph>
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

Lines changed: 35 additions & 30 deletions
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

Lines changed: 48 additions & 43 deletions
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

Lines changed: 82 additions & 1 deletion
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)
0