8000 Expand unit support. · danarwix/client_python@5cc3ca8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 5cc3ca8

Browse files
committed
Expand unit support.
If unit is missing add it rather than erroring. Add unit support to core types. Don't allow unit on info/stateset/enum, as that doesn't make sense. Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent d619202 commit 5cc3ca8

File tree

4 files changed

+36
-10
lines changed

4 files changed

+36
-10
lines changed

prometheus_client/core.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -198,12 +198,12 @@ class Metric(object):
198198
and SummaryMetricFamily instead.
199199
'''
200200
def __init__(self, name, documentation, typ, unit=''):
201+
if unit and not name.endswith("_" + unit):
202+
name += "_" + unit
201203
if not _METRIC_NAME_RE.match(name):
202204
raise ValueError('Invalid metric name: ' + name)
203205
self.name = name
204206
self.documentation = documentation
205-
if unit and not name.endswith("_" + unit):
206-
raise ValueError("Metric name not suffixed by unit: " + name)
207207
self.unit = unit
208208
if typ == 'untyped':
209209
typ = 'unknown'
@@ -239,8 +239,8 @@ class UnknownMetricFamily(Metric):
239239
'''A single unknwon metric and its samples.
240240
For use by custom collectors.
241241
'''
242-
def __init__(self, name, documentation, value=None, labels=None):
243-
Metric.__init__(self, name, documentation, 'unknown')
242+
def __init__(self, name, documentation, value=None, labels=None, unit=''):
243+
Metric.__init__(self, name, documentation, 'unknown', unit)
244244
if labels is not None and value is not None:
245245
raise ValueError('Can only specify at most one of value and labels.')
246246
if labels is None:
@@ -265,11 +265,11 @@ class CounterMetricFamily(Metric):
265265
266266
For use by custom collectors.
267267
'''
268-
def __init__(self, name, documentation, value=None, labels=None, created=None):
268+
def __init__(self, name, documentation, value=None, labels=None, created=None, unit=''):
269269
# Glue code for pre-OpenMetrics metrics.
270270
if name.endswith('_total'):
271271
name = name[:-6]
272-
Metric.__init__(self, name, documentation, 'counter')
272+
Metric.__init__(self, name, documentation, 'counter', unit)
273273
if labels is not None and value is not None:
274274
raise ValueError('Can only specify at most one of value and labels.')
275275
if labels is None:
@@ -735,14 +735,19 @@ def _samples(self):
735735

736736
def _MetricWrapper(cls):
737737
'''Provides common functionality for metrics.'''
738-
def init(name, documentation, labelnames=(), namespace='', subsystem='', registry=REGISTRY, **kwargs):
738+
def init(name, documentation, labelnames=(), namespace='', subsystem='', unit='', registry=REGISTRY, **kwargs):
739739
full_name = ''
740740
if namespace:
741741
full_name += namespace + '_'
742742
if subsystem:
743743
full_name += subsystem + '_'
744744
full_name += name
745745

746+
if unit and not full_name.endswith("_" + unit):
747+
full_name += "_" + unit
748+
if unit and cls._type in ('info', 'stateset'):
749+
raise ValueError('Metric name is of a type that cannot have a unit: ' + full_name)
750+
746751
if cls._type == 'counter' and full_name.endswith('_total'):
747752
full_name = full_name[:-6] # Munge to OpenMetrics.
748753

@@ -767,7 +772,7 @@ def describe():
767772
collector.describe = describe
768773

769774
def collect():
770-
metric = Metric(full_name, documentation, cls._type)
775+
metric = Metric(full_name, documentation, cls._type, unit)
771776
for suffix, labels, value in collector._samples():
772777
metric.add_sample(full_name + suffix, labels, value)
773778
return [metric]

prometheus_client/openmetrics/parser.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,11 +244,15 @@ def build_metric(name, documentation, typ, unit, samples):
244244
if name in seen_metrics:
245245
raise ValueError("Duplicate metric: " + name)
246246
seen_metrics.add(name)
247+
if unit and not name.endswith("_" + unit):
248+
raise ValueError("Unit does not match metric name: " + name)
249+
if unit and typ in ['info', 'stateset']:
250+
raise ValueError("Units not allowed for this metric type: " + name)
247251
metric = core.Metric(name, documentation, typ, unit)
248252
# TODO: check labelvalues are valid utf8
249253
# TODO: check only histogram buckets have exemplars.
250-
# TODO: Info and stateset can't have units
251254
# TODO: check samples are appropriately grouped and ordered
255+
# TODO: check info/stateset values are 1/0
252256
# TODO: check for metadata in middle of samples
253257
# TODO: Check histogram bucket rules being followed
254258
metric.samples = samples

tests/openmetrics/test_parser.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,8 @@ def test_invalid_input(self):
384384
('# UNIT a\t\n# EOF\n'),
385385
('# UNIT a seconds\n# EOF\n'),
386386
('# UNIT a_seconds seconds \n# EOF\n'),
387+
('# TYPE x_u info\n# UNIT x_u u\n# EOF\n'),
388+
('# TYPE x_u stateset\n# UNIT x_u u\n# EOF\n'),
387389
# Bad metric names.
388390
('0a 1\n# EOF\n'),
389391
('a.b 1\n# EOF\n'),

tests/test_core.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,11 +466,22 @@ def test_invalid_names_raise(self):
466466
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['a:b'])
467467
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved'])
468468
self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile'])
469-
470469
def test_empty_labels_list(self):
471470
Histogram('h', 'help', [], registry=self.registry)
472471
self.assertEqual(0, self.registry.get_sample_value('h_sum'))
473472

473+
def test_unit_appended(self):
474+
Histogram('h', 'help', [], registry=self.registry, unit="seconds")
475+
self.assertEqual(0, self.registry.get_sample_value('h_seconds_sum'))
476+
477+
def test_unit_notappended(self):
478+
Histogram('h_seconds', 'help', [], registry=self.registry, unit="seconds")
479+
self.assertEqual(0, self.registry.get_sample_value('h_seconds_sum'))
480+
481+
def test_no_units_for_info_enum(self):
482+
self.assertRaises(ValueError, Info, 'foo', 'help', unit="x")
483+
self.assertRaises(ValueError, Enum, 'foo', 'help', unit="x")
484+
474485
def test_wrapped_original_class(self):
475486
self.assertEqual(Counter.__wrapped__, Counter('foo', 'bar').__class__)
476487

@@ -495,6 +506,10 @@ def test_untyped_labels(self):
495506
self.custom_collector(cmf)
496507
self.assertEqual(2, self.registry.get_sample_value('u', {'a': 'b', 'c': 'd'}))
497508

509+
def test_untyped_unit(self):
510+
self.custom_collector(UntypedMetricFamily('u', 'help', value=1, unit='unit'))
511+
self.assertEqual(1, self.registry.get_sample_value('u_unit', {}))
512+
498513
def test_counter(self):
499514
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
500515
self.assertEqual(1, self.registry.get_sample_value('c_total', {}))

0 commit comments

Comments
 (0)
0