Skip to content

Commit a4dd93b

Browse files
committed
Make counter metric name not have _total internally.
With OpenMetrics the _total is a suffix on a sample for a counter, so the convention that Counters should end in total is now enforced. If an existing counter is missing the _total, it'll now appear on the /metrics. Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent da8e6c6 commit a4dd93b

File tree

7 files changed

+113
-73
lines changed

7 files changed

+113
-73
lines changed

prometheus_client/core.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,9 @@ def _get_names(self, collector):
8585

8686
result = []
8787
type_suffixes = {
88-
'summary': ['', '_sum', '_count'],
89-
'histogram': ['_bucket', '_sum', '_count']
88+
'counter': ['_total', '_created'],
89+
'summary': ['', '_sum', '_count', '_created'],
90+
'histogram': ['_bucket', '_sum', '_count', '_created']
9091
}
9192
for metric in desc_func():
9293
for suffix in type_suffixes.get(metric.type, ['']):
@@ -213,24 +214,30 @@ class CounterMetricFamily(Metric):
213214
214215
For use by custom collectors.
215216
'''
216-
def __init__(self, name, documentation, value=None, labels=None):
217+
def __init__(self, name, documentation, value=None, labels=None, created=None):
218+
# Glue code for pre-OpenMetrics metrics.
219+
if name.endswith('_total'):
220+
name = name[:-6]
217221
Metric.__init__(self, name, documentation, 'counter')
218222
if labels is not None and value is not None:
219223
raise ValueError('Can only specify at most one of value and labels.')
220224
if labels is None:
221225
labels = []
222226
self._labelnames = tuple(labels)
223227
if value is not None:
224-
self.add_metric([], value)
228+
self.add_metric([], value, created)
225229

226-
def add_metric(self, labels, value):
230+
def add_metric(self, labels, value, created=None):
227231
'''Add a metric to the metric family.
228232
229233
Args:
230234
labels: A list of label values
231-
value: The value of the metric.
235+
value: The value of the metric
236+
created: Optional unix timestamp the child was created at.
232237
'''
233-
self.samples.append((self.name, dict(zip(self._labelnames, labels)), value))
238+
self.samples.append((self.name + '_total', dict(zip(self._labelnames, labels)), value))
239+
if created is not None:
240+
self.samples.append((self.name + '_created', dict(zip(self._labelnames, labels)), created))
234241

235242

236243
class GaugeMetricFamily(Metric):
@@ -592,6 +599,9 @@ def init(name, documentation, labelnames=(), namespace='', subsystem='', registr
592599
full_name += subsystem + '_'
593600
full_name += name
594601

602+
if cls._type == 'counter' and full_name.endswith('_total'):
603+
full_name = full_name[:-6] # Munge to OpenMetrics.
604+
595605
if labelnames:
596606
labelnames = tuple(labelnames)
597607
for l in labelnames:
@@ -664,7 +674,9 @@ def f():
664674
_reserved_labelnames = []
665675

666676
def __init__(self, name, labelnames, labelvalues):
667-
self._value = _ValueClass(self._type, name, name, labelnames, labelvalues)
677+
if name.endswith('_total'):
678+
name = name[:-6]
679+
self._value = _ValueClass(self._type, name, name + '_total', labelnames, labelvalues)
668680

669681
def inc(self, amount=1):
670682
'''Increment counter by the given amount.'''
@@ -682,7 +694,7 @@ def count_exceptions(self, exception=Exception):
682694
return _ExceptionCounter(self, exception)
683695

684696
def _samples(self):
685-
return (('', {}, self._value.get()), )
697+
return (('_total', {}, self._value.get()), )
686698

687699

688700
@_MetricWrapper

prometheus_client/exposition.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,15 @@ def generate_latest(registry=core.REGISTRY):
6767
'''Returns the metrics from the registry in latest text format as a string.'''
6868
output = []
6969
for metric in registry.collect():
70+
mname = metric.name
71+
if metric.type == 'counter':
72+
mname = mname + '_total'
7073
output.append('# HELP {0} {1}'.format(
71-
metric.name, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
72-
output.append('\n# TYPE {0} {1}\n'.format(metric.name, metric.type))
74+
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
75+
output.append('\n# TYPE {0} {1}\n'.format(mname, metric.type))
7376
for name, labels, value in metric.samples:
77+
if name == metric.name + '_created':
78+
continue # Ignore OpenMetrics specific sample.
7479
if labels:
7580
labelstr = '{{{0}}}'.format(','.join(
7681
['{0}="{1}"'.format(

prometheus_client/parser.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,16 @@ def text_fd_to_metric_families(fd):
156156
allowed_names = []
157157

158158
def build_metric(name, documentation, typ, samples):
159+
# Munge counters into OpenMetrics representation
160+
# used internally.
161+
if typ == 'counter':
162+
if name.endswith('_total'):
163+
name = name[:-6]
164+
else:
165+
new_samples = []
166+
for s in samples:
167+
new_samples.append(tuple((s[0] + '_total', ) + s[1:]))
168+
samples = new_samples
159169
metric = core.Metric(name, documentation, typ)
160170
metric.samples = samples
161171
return metric

tests/test_core.py

Lines changed: 57 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@
2828
class TestCounter(unittest.TestCase):
2929
def setUp(self):
3030
self.registry = CollectorRegistry()
31-
self.counter = Counter('c', 'help', registry=self.registry)
31+
self.counter = Counter('c_total', 'help', registry=self.registry)
3232

3333
def test_increment(self):
34-
self.assertEqual(0, self.registry.get_sample_value('c'))
34+
self.assertEqual(0, self.registry.get_sample_value('c_total'))
3535
self.counter.inc()
36-
self.assertEqual(1, self.registry.get_sample_value('c'))
36+
self.assertEqual(1, self.registry.get_sample_value('c_total'))
3737
self.counter.inc(7)
38-
self.assertEqual(8, self.registry.get_sample_value('c'))
38+
self.assertEqual(8, self.registry.get_sample_value('c_total'))
3939

4040
def test_negative_increment_raises(self):
4141
self.assertRaises(ValueError, self.counter.inc, -1)
@@ -54,18 +54,18 @@ def f(r):
5454
f(False)
5555
except TypeError:
5656
pass
57-
self.assertEqual(0, self.registry.get_sample_value('c'))
57+
self.assertEqual(0, self.registry.get_sample_value('c_total'))
5858

5959
try:
6060
f(True)
6161
except ValueError:
6262
pass
63-
self.assertEqual(1, self.registry.get_sample_value('c'))
63+
self.assertEqual(1, self.registry.get_sample_value('c_total'))
6464

6565
def test_block_decorator(self):
6666
with self.counter.count_exceptions():
6767
pass
68-
self.assertEqual(0, self.registry.get_sample_value('c'))
68+
self.assertEqual(0, self.registry.get_sample_value('c_total'))
6969

7070
raised = False
7171
try:
@@ -74,7 +74,7 @@ def test_block_decorator(self):
7474
except:
7575
raised = True
7676
self.assertTrue(raised)
77-
self.assertEqual(1, self.registry.get_sample_value('c'))
77+
self.assertEqual(1, self.registry.get_sample_value('c_total'))
7878

7979

8080
class TestGauge(unittest.TestCase):
@@ -342,23 +342,23 @@ def test_block_decorator(self):
342342
class TestMetricWrapper(unittest.TestCase):
343343
def setUp(self):
344344
self.registry = CollectorRegistry()
345-
self.counter = Counter('c', 'help', labelnames=['l'], registry=self.registry)
345+
self.counter = Counter('c_total', 'help', labelnames=['l'], registry=self.registry)
346346
self.two_labels = Counter('two', 'help', labelnames=['a', 'b'], registry=self.registry)
347347

348348
def test_child(self):
349349
self.counter.labels('x').inc()
350-
self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
350+
self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
351351
self.two_labels.labels('x', 'y').inc(2)
352-
self.assertEqual(2, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
352+
self.assertEqual(2, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))
353353

354354
def test_remove(self):
355355
self.counter.labels('x').inc()
356356
self.counter.labels('y').inc(2)
357-
self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
358-
self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'y'}))
357+
self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
358+
self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'}))
359359
self.counter.remove('x')
360-
self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'x'}))
361-
self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'y'}))
360+
self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'}))
361+
self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'}))
362362

363363
def test_incorrect_label_count_raises(self):
364364
self.assertRaises(ValueError, self.counter.labels)
@@ -369,10 +369,10 @@ def test_incorrect_label_count_raises(self):
369369
def test_labels_coerced_to_string(self):
370370
self.counter.labels(None).inc()
371371
self.counter.labels(l=None).inc()
372-
self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'None'}))
372+
self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'None'}))
373373

374374
self.counter.remove(None)
375-
self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'None'}))
375+
self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'}))
376376

377377
def test_non_string_labels_raises(self):
378378
class Test(object):
@@ -381,18 +381,18 @@ class Test(object):
381381
self.assertRaises(TypeError, self.counter.labels, l=Test())
382382

383383
def test_namespace_subsystem_concatenated(self):
384-
c = Counter('c', 'help', namespace='a', subsystem='b', registry=self.registry)
384+
c = Counter('c_total', 'help', namespace='a', subsystem='b', registry=self.registry)
385385
c.inc()
386-
self.assertEqual(1, self.registry.get_sample_value('a_b_c'))
386+
self.assertEqual(1, self.registry.get_sample_value('a_b_c_total'))
387387

388388
def test_labels_by_kwarg(self):
389389
self.counter.labels(l='x').inc()
390-
self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
390+
self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
391391
self.assertRaises(ValueError, self.counter.labels, l='x', m='y')
392392
self.assertRaises(ValueError, self.counter.labels, m='y')
393393
self.assertRaises(ValueError, self.counter.labels)
394394
self.two_labels.labels(a='x', b='y').inc()
395-
self.assertEqual(1, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
395+
self.assertEqual(1, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))
396396
self.assertRaises(ValueError, self.two_labels.labels, a='x', b='y', c='z')
397397
self.assertRaises(ValueError, self.two_labels.labels, a='x', c='z')
398398
self.assertRaises(ValueError, self.two_labels.labels, b='y', c='z')
@@ -405,10 +405,10 @@ def test_invalid_names_raise(self):
405405
self.assertRaises(ValueError, Counter, '^', 'help')
406406
self.assertRaises(ValueError, Counter, '', 'help', namespace='&')
407407
self.assertRaises(ValueError, Counter, '', 'help', subsystem='(')
408-
self.assertRaises(ValueError, Counter, 'c', '', labelnames=['^'])
409-
self.assertRaises(ValueError, Counter, 'c', '', labelnames=['a:b'])
410-
self.assertRaises(ValueError, Counter, 'c', '', labelnames=['__reserved'])
411-
self.assertRaises(ValueError, Summary, 'c', '', labelnames=['quantile'])
408+
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['^'])
409+
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['a:b'])
410+
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved'])
411+
self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile'])
412412

413413
def test_empty_labels_list(self):
414414
Histogram('h', 'help', [], registry=self.registry)
@@ -439,14 +439,18 @@ def test_untyped_labels(self):
439439
self.assertEqual(2, self.registry.get_sample_value('u', {'a': 'b', 'c': 'd'}))
440440

441441
def test_counter(self):
442-
self.custom_collector(CounterMetricFamily('c', 'help', value=1))
443-
self.assertEqual(1, self.registry.get_sample_value('c', {}))
442+
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
443+
self.assertEqual(1, self.registry.get_sample_value('c_total', {}))
444+
445+
def test_counter_total(self):
446+
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
447+
self.assertEqual(1, self.registry.get_sample_value('c_total', {}))
444448

445449
def test_counter_labels(self):
446-
cmf = CounterMetricFamily('c', 'help', labels=['a', 'c'])
450+
cmf = CounterMetricFamily('c_total', 'help', labels=['a', 'c_total'])
447451
cmf.add_metric(['b', 'd'], 2)
448452
self.custom_collector(cmf)
449-
self.assertEqual(2, self.registry.get_sample_value('c', {'a': 'b', 'c': 'd'}))
453+
self.assertEqual(2, self.registry.get_sample_value('c_total', {'a': 'b', 'c_total': 'd'}))
450454

451455
def test_gauge(self):
452456
self.custom_collector(GaugeMetricFamily('g', 'help', value=1))
@@ -490,8 +494,8 @@ def test_bad_constructors(self):
490494
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[])
491495
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])
492496

493-
self.assertRaises(ValueError, CounterMetricFamily, 'c', 'help', value=1, labels=[])
494-
self.assertRaises(ValueError, CounterMetricFamily, 'c', 'help', value=1, labels=['a'])
497+
self.assertRaises(ValueError, CounterMetricFamily, 'c_total', 'help', value=1, labels=[])
498+
self.assertRaises(ValueError, CounterMetricFamily, 'c_total', 'help', value=1, labels=['a'])
495499

496500
self.assertRaises(ValueError, GaugeMetricFamily, 'g', 'help', value=1, labels=[])
497501
self.assertRaises(ValueError, GaugeMetricFamily, 'g', 'help', value=1, labels=['a'])
@@ -512,7 +516,7 @@ def test_bad_constructors(self):
512516
def test_labelnames(self):
513517
cmf = UntypedMetricFamily('u', 'help', labels=iter(['a']))
514518
self.assertEqual(('a',), cmf._labelnames)
515-
cmf = CounterMetricFamily('c', 'help', labels=iter(['a']))
519+
cmf = CounterMetricFamily('c_total', 'help', labels=iter(['a']))
516520
self.assertEqual(('a',), cmf._labelnames)
517521
gmf = GaugeMetricFamily('g', 'help', labels=iter(['a']))
518522
self.assertEqual(('a',), gmf._labelnames)
@@ -525,16 +529,20 @@ def test_labelnames(self):
525529
class TestCollectorRegistry(unittest.TestCase):
526530
def test_duplicate_metrics_raises(self):
527531
registry = CollectorRegistry()
528-
Counter('c', 'help', registry=registry)
529-
self.assertRaises(ValueError, Counter, 'c', 'help', registry=registry)
530-
self.assertRaises(ValueError, Gauge, 'c', 'help', registry=registry)
532+
Counter('c_total', 'help', registry=registry)
533+
self.assertRaises(ValueError, Counter, 'c_total', 'help', registry=registry)
534+
self.assertRaises(ValueError, Gauge, 'c_total', 'help', registry=registry)
535+
self.assertRaises(ValueError, Gauge, 'c_created', 'help', registry=registry)
531536

532-
Gauge('g', 'help', registry=registry)
533-
self.assertRaises(ValueError, Gauge, 'g', 'help', registry=registry)
537+
Gauge('g_created', 'help', registry=registry)
538+
self.assertRaises(ValueError, Gauge, 'g_created', 'help', registry=registry)
534539
self.assertRaises(ValueError, Counter, 'g', 'help', registry=registry)
535540

536541
Summary('s', 'help', registry=registry)
537542
self.assertRaises(ValueError, Summary, 's', 'help', registry=registry)
543+
self.assertRaises(ValueError, Gauge, 's_created', 'help', registry=registry)
544+
self.assertRaises(ValueError, Gauge, 's_sum', 'help', registry=registry)
545+
self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
538546
# We don't currently expose quantiles, but let's prevent future
539547
# clashes anyway.
540548
self.assertRaises(ValueError, Gauge, 's', 'help', registry=registry)
@@ -543,18 +551,19 @@ def test_duplicate_metrics_raises(self):
543551
self.assertRaises(ValueError, Histogram, 'h', 'help', registry=registry)
544552
# Clashes aggaint various suffixes.
545553
self.assertRaises(ValueError, Summary, 'h', 'help', registry=registry)
546-
self.assertRaises(ValueError, Counter, 'h_count', 'help', registry=registry)
547-
self.assertRaises(ValueError, Counter, 'h_sum', 'help', registry=registry)
548-
self.assertRaises(ValueError, Counter, 'h_bucket', 'help', registry=registry)
554+
self.assertRaises(ValueError, Gauge, 'h_count', 'help', registry=registry)
555+
self.assertRaises(ValueError, Gauge, 'h_sum', 'help', registry=registry)
556+
self.assertRaises(ValueError, Gauge, 'h_bucket', 'help', registry=registry)
557+
self.assertRaises(ValueError, Gauge, 'h_created', 'help', registry=registry)
549558
# The name of the histogram itself isn't taken.
550-
Counter('h', 'help', registry=registry)
559+
Gauge('h', 'help', registry=registry)
551560

552561
def test_unregister_works(self):
553562
registry = CollectorRegistry()
554563
s = Summary('s', 'help', registry=registry)
555-
self.assertRaises(ValueError, Counter, 's_count', 'help', registry=registry)
564+
self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
556565
registry.unregister(s)
557-
Counter('s_count', 'help', registry=registry)
566+
Gauge('s_count', 'help', registry=registry)
558567

559568
def custom_collector(self, metric_family, registry):
560569
class CustomCollector(object):
@@ -564,16 +573,16 @@ def collect(self):
564573

565574
def test_autodescribe_disabled_by_default(self):
566575
registry = CollectorRegistry()
567-
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
568-
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
576+
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
577+
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
569578

570579
registry = CollectorRegistry(auto_describe=True)
571-
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
572-
self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c', 'help', value=1), registry)
580+
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
581+
self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c_total', 'help', value=1), registry)
573582

574583
def test_restricted_registry(self):
575584
registry = CollectorRegistry()
576-
Counter('c', 'help', registry=registry)
585+
Counter('c_total', 'help', registry=registry)
577586
Summary('s', 'help', registry=registry).observe(7)
578587

579588
m = Metric('s', 'help', 'summary')

tests/test_exposition.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,12 @@ def setUp(self):
3131
def test_counter(self):
3232
c = Counter('cc', 'A counter', registry=self.registry)
3333
c.inc()
34-
self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc 1.0\n', generate_latest(self.registry))
34+
self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
3535

36+
def test_counter_total(self):
37+
c = Counter('cc_total', 'A counter', registry=self.registry)
38+
c.inc()
39+
self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
3640
def test_gauge(self):
3741
g = Gauge('gg', 'A gauge', registry=self.registry)
3842
g.set(17)
@@ -71,12 +75,12 @@ def test_histogram(self):
7175
def test_unicode(self):
7276
c = Counter('cc', '\u4500', ['l'], registry=self.registry)
7377
c.labels('\u4500').inc()
74-
self.assertEqual(b'# HELP cc \xe4\x94\x80\n# TYPE cc counter\ncc{l="\xe4\x94\x80"} 1.0\n', generate_latest(self.registry))
78+
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))
7579

7680
def test_escaping(self):
7781
c = Counter('cc', 'A\ncount\\er', ['a'], registry=self.registry)
7882
c.labels('\\x\n"').inc(1)
79-
self.assertEqual(b'# HELP cc A\\ncount\\\\er\n# TYPE cc counter\ncc{a="\\\\x\\n\\""} 1.0\n', generate_latest(self.registry))
83+
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))
8084

8185
def test_nonnumber(self):
8286

0 commit comments

Comments
 (0)