8000 Add gsum/gcount to GaugeHistogram. · ethervoid/client_python@8a4a65c · GitHub
[go: up one dir, main page]

Skip to content

Commit 8a4a65c

Browse files
committed
Add gsum/gcount to GaugeHistogram.
Allow gsum, gcount, and created to be sanely returned in Prometheus format. Extend openmetrics parser unittests to cover Info and StateSet. Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent 18017c6 commit 8a4a65c

File tree

8 files changed

+137
-35
lines changed

8 files changed

+137
-35
lines changed

prometheus_client/core.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ def _get_names(self, collector):
122122
'counter': ['_total', '_created'],
123123
'summary': ['', '_sum', '_count', '_created'],
124124
'histogram': ['_bucket', '_sum', '_count', '_created'],
125+
'gaugehistogram': ['_bucket', '_gsum', '_gcount'],
125126
'info': ['_info'],
126127
}
127128
for metric in desc_func():
@@ -391,29 +392,33 @@ class GaugeHistogramMetricFamily(Metric):
391392
392393
For use by custom collectors.
393394
'''
394-
def __init__(self, name, documentation, buckets=None, labels=None, unit=''):
395+
def __init__(self, name, documentation, buckets=None, gsum_value=None, labels=None, unit=''):
395396
Metric.__init__(self, name, documentation, 'gaugehistogram', unit)
396397
if labels is not None and buckets is not None:
397398
raise ValueError('Can only specify at most one of buckets and labels.')
398399
if labels is None:
399400
labels = []
400401
self._labelnames = tuple(labels)
401402
if buckets is not None:
402-
self.add_metric([], buckets)
403+
self.add_metric([], buckets, gsum_value)
403404

404-
def add_metric(self, labels, buckets, timestamp=None):
405+
def add_metric(self, labels, buckets, gsum_value, timestamp=None):
405406
'''Add a metric to the metric family.
406407
407408
Args:
408409
labels: A list of label values
409410
buckets: A list of pairs of bucket names and values.
410411
The buckets must be sorted, and +Inf present.
412+
gsum_value: The sum value of the metric.
411413
'''
412414
for bucket, value in buckets:
413415
self.samples.append(Sample(
414416
self.name + '_bucket',
415417
dict(list(zip(self._labelnames, labels)) + [('le', bucket)]),
416418
value, timestamp))
419+
# +Inf is last and provides the count value.
420+
self.samples.append(Sample(self.name + '_gcount', dict(zip(self._labelnames, labels)), buckets[-1][1], timestamp))
421+
self.samples.append(Sample(self.name + '_gsum', dict(zip(self._labelnames, labels)), gsum_value, timestamp))
417422

418423

419424
class InfoMetricFamily(Metric):
@@ -465,7 +470,7 @@ def add_metric(self, labels, value, timestamp=None):
465470
value: A dict of string state names to booleans
466471
'''
467472
labels = tuple(labels)
468-
for state, enabled in value.items():
473+
for state, enabled in sorted(value.items()):
469474
v = (1 if enabled else 0)
470475
self.samples.append(Sample(self.name,
471476
dict(zip(self._labelnames + (self.name,), labels + (state,))), v, timestamp))

prometheus_client/exposition.py

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,22 @@ def start_wsgi_server(port, addr='', registry=core.REGISTRY):
6767

6868
def generate_latest(registry=core.REGISTRY):
6969
'''Returns the metrics from the registry in latest text format as a string.'''
70+
71+
def sample_line(s):
72+
if s.labels:
73+
labelstr = '{{{0}}}'.format(','.join(
74+
['{0}="{1}"'.format(
75+
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
76+
for k, v in sorted(s.labels.items())]))
77+
else:
78+
labelstr = ''
79+
timestamp = ''
80+
if s.timestamp is not None:
81+
# Convert to milliseconds.
82+
timestamp = ' {0:d}'.format(int(float(s.timestamp) * 1000))
83+
return '{0}{1} {2}{3}\n'.format(
84+
s.name, labelstr, core._floatToGoString(s.value), timestamp)
85+
7086
output = []
7187
for metric in registry.collect():
7288
mname = metric.name
@@ -86,25 +102,22 @@ def generate_latest(registry=core.REGISTRY):
86102
elif mtype == 'unknown':
87103
mtype = 'untyped'
88104

89-
output.append('# HELP {0} {1}'.format(
105+
output.append('# HELP {0} {1}\n'.format(
90106
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
91-
output.append('\n# TYPE {0} {1}\n'.format(mname, mtype))
107+
output.append('# TYPE {0} {1}\n'.format(mname, mtype))
108+
109+
om_samples = {}
92110
for s in metric.samples:
93-
if s.name == metric.name + '_created':
94-
continue # Ignore OpenMetrics specific sample. TODO: Make these into a gauge.
95-
if s.labels:
96-
labelstr = '{{{0}}}'.format(','.join(
97-
['{0}="{1}"'.format(
98-
k, v.replace('\\', r'\\').replace('\n', r'\n').replace('"', r'\"'))
99-
for k, v in sorted(s.labels.items())]))
111+
for suffix in ['_created', '_gsum', '_gcount']:
112+
if s.name == metric.name + suffix:
113+
# OpenMetrics specific sample, put in a gauge at the end.
114+
om_samples.setdefault(suffix, []).append(sample_line(s))
115+
break
100116
else:
101-
labelstr = ''
102-
timestamp = ''
103-
if s.timestamp is not None:
104-
# Convert to milliseconds.
105-
timestamp = ' {0:d}'.format(int(float(s.timestamp) * 1000))
106-
output.append('{0}{1} {2}{3}\n'.format(
107-
s.name, labelstr, core._floatToGoString(s.value), timestamp))
117+
output.append(sample_line(s))
118+
for suffix, lines in sorted(om_samples.items()):
119+
output.append('# TYPE {0}{1} gauge\n'.format(metric.name, suffix))
120+
output.extend(lines)
108121
return ''.join(output).encode('utf-8')
109122

110123

prometheus_client/openmetrics/exposition.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ def generate_latest(registry):
2626
else:
2727
labelstr = ''
2828
if s.exemplar:
29-
if metric.type != 'histogram' or not s.name.endswith('_bucket'):
29+
if metric.type not in ('histogram', 'gaugehistogram') or not s.name.endswith('_bucket'):
3030
raise ValueError("Metric {0} has exemplars, but is not a histogram bucket".format(metric.name))
3131
labels = '{{{0}}}'.format(','.join(
3232
['{0}="{1}"'.format(
@@ -42,7 +42,6 @@ def generate_latest(registry):
4242
exemplarstr = ''
4343
timestamp = ''
4444
if s.timestamp is not None:
45-
# Convert to milliseconds.
4645
timestamp = ' {0}'.format(s.timestamp)
4746
output.append('{0}{1} {2}{3}{4}\n'.format(s.name, labelstr,
4847
core._floatToGoString(s.value), timestamp, exemplarstr))

prometheus_client/openmetrics/parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,8 @@ def build_metric(name, documentation, typ, unit, samples):
302302
'counter': ['_total', '_created'],
303303
'summary': ['_count', '_sum', '', '_created'],
304304
'histogram': ['_count', '_sum', '_bucket', 'created'],
305-
'gaugehistogram': ['_bucket'],
305+
'gaugehistogram': ['_gcount', '_gsum', '_bucket'],
306+
'info': ['_info'],
306307
}.get(typ, [''])
307308
allowed_names = [name + n for n in allowed_names]
308309
elif parts[1] == 'UNIT':

tests/openmetrics/test_exposition.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,11 +134,13 @@ def collect(self):
134134
generate_latest(self.registry)
135135

136136
def test_gaugehistogram(self):
137-
self.custom_collector(GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))]))
137+
self.custom_collector(GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))], gsum_value=7))
138138
self.assertEqual(b'''# HELP gh help
139139
# TYPE gh gaugehistogram
140140
gh_bucket{le="1.0"} 4.0
141141
gh_bucket{le="+Inf"} 5.0
142+
gh_gcount 5.0
143+
gh_gsum 7.0
142144
# EOF
143145
''', generate_latest(self.registry))
144146

tests/openmetrics/test_parser.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
CollectorRegistry,
1414
CounterMetricFamily,
1515
Exemplar,
16+
GaugeHistogramMetricFamily,
1617
GaugeMetricFamily,
1718
HistogramMetricFamily,
19+
InfoMetricFamily,
1820
Metric,
1921
Sample,
22+
StateSetMetricFamily,
2023
SummaryMetricFamily,
2124
Timestamp,
2225
)
@@ -120,6 +123,48 @@ def test_histogram_exemplars(self):
120123
hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, None, Exemplar({"a": "d"}, 4, Timestamp(123, 0)))
121124
self.assertEqual([hfm], list(families))
122125

126+
def test_simple_gaugehistogram(self):
127+
families = text_string_to_metric_families("""# TYPE a gaugehistogram
128+
# HELP a help
129+
a_bucket{le="1"} 0
130+
a_bucket{le="+Inf"} 3
131+
a_gcount 3
132+
a_gsum 2
133+
# EOF
134+
""")
135+
self.assertEqual([GaugeHistogramMetricFamily("a", "help", gsum_value=2, buckets=[("1", 0.0), ("+Inf", 3.0)])], list(families))
136+
137+
def test_histogram_exemplars(self):
138+
families = text_string_to_metric_families("""# TYPE a gaugehistogram
139+
# HELP a help
140+
a_bucket{le="1"} 0 # {a="b"} 0.5
141+
a_bucket{le="2"} 2 123 # {a="c"} 0.5
142+
a_bucket{le="+Inf"} 3 # {a="d"} 4 123
143+
# EOF
144+
""")
145+
hfm = GaugeHistogramMetricFamily("a", "help")
146+
hfm.add_sample("a_bucket", {"le": "1"}, 0.0, None, Exemplar({"a": "b"}, 0.5))
147+
hfm.add_sample("a_bucket", {"le": "2"}, 2.0, Timestamp(123, 0), Exemplar({"a": "c"}, 0.5)),
148+
hfm.add_sample("a_bucket", {"le": "+Inf"}, 3.0, None, Exemplar({"a": "d"}, 4, Timestamp(123, 0)))
149+
self.assertEqual([hfm], list(families))
150+
151+
def test_simple_info(self):
152+
families = text_string_to_metric_families("""# TYPE a info
153+
# HELP a help
154+
a_info{foo="bar"} 1
155+
# EOF
156+
""")
157+
self.assertEqual([InfoMetricFamily("a", "help", {'foo': 'bar'})], list(families))
158+
159+
def test_simple_stateset(self):
160+
families = text_string_to_metric_families("""# TYPE a stateset
161+
# HELP a help
162+
a{a="bar"} 0
163+
a{a="foo"} 1
164+
# EOF
165+
""")
166+
self.assertEqual([StateSetMetricFamily("a", "help", {'foo': True, 'bar': False})], list(families))
167+
123168
def test_no_metadata(self):
124169
families = text_string_to_metric_families("""a 1
125170
# EOF

tests/test_core.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,10 +569,12 @@ def test_gaugehistogram(self):
569569

570570
def test_gaugehistogram_labels(self):
571571
cmf = GaugeHistogramMetricFamily('h', 'help', labels=['a'])
572-
cmf.add_metric(['b'], buckets=[('0', 1), ('+Inf', 2)])
572+
cmf.add_metric(['b'], buckets=[('0', 1), ('+Inf', 2)], gsum_value=3)
573573
self.custom_collector(cmf)
574574
self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'a': 'b', 'le': '0'}))
575575
self.assertEqual(2, self.registry.get_sample_value('h_bucket', {'a': 'b', 'le': '+Inf'}))
576+
self.assertEqual(2, self.registry.get_sample_value('h_gcount', {'a': 'b'}))
577+
self.assertEqual(3, self.registry.get_sample_value('h_gsum', {'a': 'b'}))
576578

577579
def test_info(self):
578580
self.custom_collector(InfoMetricFamily('i', 'help', value={'a': 'b'}))

tests/test_exposition.py

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import sys
44
import threading
5+
import time
56

67
if sys.version_info < (2, 7):
78
# We need the skip decorators from unittest2 on Python 2.6.
@@ -29,6 +30,13 @@ class TestGenerateText(unittest.TestCase):
2930
def setUp(self):
3031
self.registry = CollectorRegistry()
3132

33+
# Mock time so _created values are fixed.
34+
self.old_time = time.time
35+
time.time = lambda: 123.456
36+
37+
def tearDown(self):
38+
time.time = self.old_time
39+
3240
def custom_collector(self, metric_family):
3341
class CustomCollector(object):
3442
def collect(self):
@@ -38,12 +46,23 @@ def collect(self):
3846
def test_counter(self):
3947
c = Counter('cc', 'A counter', registry=self.registry)
4048
c.inc()
41-
self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
49+
self.assertEqual(b'''# HELP cc_total A counter
50+
# TYPE cc_total counter
51+
cc_total 1.0
52+
# TYPE cc_created gauge
53+
cc_created 123.456
54+
''', generate_latest(self.registry))
4255

4356
def test_counter_total(self):
4457
c = Counter('cc_total', 'A counter', registry=self.registry)
4558
c.inc()
46-
self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
59+
self.assertEqual(b'''# HELP cc_total A counter
60+
# TYPE cc_total counter
61+
cc_total 1.0
62+
# TYPE cc_created gauge
63+
cc_created 123.456
64+
''', generate_latest(self.registry))
65+
4766
def test_gauge(self):
4867
g = Gauge('gg', 'A gauge', registry=self.registry)
4968
g.set(17)
@@ -52,7 +71,13 @@ def test_gauge(self):
5271
def test_summary(self):
5372
s = Summary('ss', 'A summary', ['a', 'b'], registry=self.registry)
5473
s.labels('c', 'd').observe(17)
55-
self.assertEqual(b'# HELP ss A summary\n# TYPE ss summary\nss_count{a="c",b="d"} 1.0\nss_sum{a="c",b="d"} 17.0\n', generate_latest(self.registry))
74+
self.assertEqual(b'''# HELP ss A summary
75+
# TYPE ss summary
76+
ss_count{a="c",b="d"} 1.0
77+
ss_sum{a="c",b="d"} 17.0
78+
# TYPE ss_created gauge
79+
ss_created{a="c",b="d"} 123.456
80+
''', generate_latest(self.registry))
5681

5782
@unittest.skipIf(sys.version_info < (2, 7), "Test requires Python 2.7+.")
5883
def test_histogram(self):
@@ -77,11 +102,21 @@ def test_histogram(self):
77102
hh_bucket{le="+Inf"} 1.0
78103
hh_count 1.0
79104
hh_sum 0.05
105+
# TYPE hh_created gauge
106+
hh_created 123.456
80107
''', generate_latest(self.registry))
81108

82109
def test_gaugehistogram(self):
83-
self.custom_collector(GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', (5))]))
84-
self.assertEqual(b'''# HELP gh help\n# TYPE gh histogram\ngh_bucket{le="1.0"} 4.0\ngh_bucket{le="+Inf"} 5.0\n''', generate_latest(self.registry))
110+
self.custom_collector(GaugeHistogramMetricFamily('gh', 'help', buckets=[('1.0', 4), ('+Inf', 5)], gsum_value=7))
111+
self.assertEqual(b'''# HELP gh help
112+
# TYPE gh histogram
113+
gh_bucket{le="1.0"} 4.0
114+
gh_bucket{le="+Inf"} 5.0
115+
# TYPE gh_gcount gauge
116+
gh_gcount 5.0
117+
# TYPE gh_gsum gauge
118+
gh_gsum 7.0
119+
''', generate_latest(self.registry))
85120

86121
def test_info(self):
87122
i = Info('ii', 'A info', ['a', 'b'], registry=self.registry)
@@ -94,14 +129,14 @@ def test_enum(self):
94129
self.assertEqual(b'# HELP ee An enum\n# TYPE ee gauge\nee{a="c",b="d",ee="foo"} 0.0\nee{a="c",b="d",ee="bar"} 1.0\n', generate_latest(self.registry))
95130

96131
def test_unicode(self):
97-
c = Counter('cc', '\u4500', ['l'], registry=self.registry)
132+
c = Gauge('cc', '\u4500', ['l'], registry=self.registry)
98133
c.labels('\u4500').inc()
99-
self.assertEqual(b'# HELP cc_total \xe4\x94\x80\n# TYPE cc_total counter\ncc_total{l="\xe4\x94\x80"} 1.0\n', generate_latest(self.registry))
134+
self.assertEqual(b'# HELP cc \xe4\x94\x80\n# TYPE cc gauge\ncc{l="\xe4\x94\x80"} 1.0\n', generate_latest(self.registry))
100135

101136
def test_escaping(self):
102-
c = Counter('cc', 'A\ncount\\er', ['a'], registry=self.registry)
103-
c.labels('\\x\n"').inc(1)
104-
self.assertEqual(b'# HELP cc_total A\\ncount\\\\er\n# TYPE cc_total counter\ncc_total{a="\\\\x\\n\\""} 1.0\n', generate_latest(self.registry))
137+
g = Gauge('cc', 'A\ngaug\\e', ['a'], registry=self.registry)
138+
g.labels('\\x\n"').inc(1)
139+
self.assertEqual(b'# HELP cc A\\ngaug\\\\e\n# TYPE cc gauge\ncc{a="\\\\x\\n\\""} 1.0\n', generate_latest(self.registry))
105140

106141
def test_nonnumber(self):
107142

0 commit comments

Comments
 (0)
0