Skip to content

Commit

Permalink
Make counter metric name not have _total internally.
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
brian-brazil committed Sep 7, 2018
1 parent da8e6c6 commit a4dd93b
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 73 deletions.
30 changes: 21 additions & 9 deletions prometheus_client/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ def _get_names(self, collector):

result = []
type_suffixes = {
'summary': ['', '_sum', '_count'],
'histogram': ['_bucket', '_sum', '_count']
'counter': ['_total', '_created'],
'summary': ['', '_sum', '_count', '_created'],
'histogram': ['_bucket', '_sum', '_count', '_created']
}
for metric in desc_func():
for suffix in type_suffixes.get(metric.type, ['']):
Expand Down Expand Up @@ -213,24 +214,30 @@ class CounterMetricFamily(Metric):
For use by custom collectors.
'''
def __init__(self, name, documentation, value=None, labels=None):
def __init__(self, name, documentation, value=None, labels=None, created=None):
# Glue code for pre-OpenMetrics metrics.
if name.endswith('_total'):
name = name[:-6]
Metric.__init__(self, name, documentation, 'counter')
if labels is not None and value is not None:
raise ValueError('Can only specify at most one of value and labels.')
if labels is None:
labels = []
self._labelnames = tuple(labels)
if value is not None:
self.add_metric([], value)
self.add_metric([], value, created)

def add_metric(self, labels, value):
def add_metric(self, labels, value, created=None):
'''Add a metric to the metric family.
Args:
labels: A list of label values
value: The value of the metric.
value: The value of the metric
created: Optional unix timestamp the child was created at.
'''
self.samples.append((self.name, dict(zip(self._labelnames, labels)), value))
self.samples.append((self.name + '_total', dict(zip(self._labelnames, labels)), value))
if created is not None:
self.samples.append((self.name + '_created', dict(zip(self._labelnames, labels)), created))


class GaugeMetricFamily(Metric):
Expand Down Expand Up @@ -592,6 +599,9 @@ def init(name, documentation, labelnames=(), namespace='', subsystem='', registr
full_name += subsystem + '_'
full_name += name

if cls._type == 'counter' and full_name.endswith('_total'):
full_name = full_name[:-6] # Munge to OpenMetrics.

if labelnames:
labelnames = tuple(labelnames)
for l in labelnames:
Expand Down Expand Up @@ -664,7 +674,9 @@ def f():
_reserved_labelnames = []

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

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

def _samples(self):
return (('', {}, self._value.get()), )
return (('_total', {}, self._value.get()), )


@_MetricWrapper
Expand Down
9 changes: 7 additions & 2 deletions prometheus_client/exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,15 @@ def generate_latest(registry=core.REGISTRY):
'''Returns the metrics from the registry in latest text format as a string.'''
output = []
for metric in registry.collect():
mname = metric.name
if metric.type == 'counter':
mname = mname + '_total'
output.append('# HELP {0} {1}'.format(
metric.name, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
output.append('\n# TYPE {0} {1}\n'.format(metric.name, metric.type))
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
output.append('\n# TYPE {0} {1}\n'.format(mname, metric.type))
for name, labels, value in metric.samples:
if name == metric.name + '_created':
continue # Ignore OpenMetrics specific sample.
if labels:
labelstr = '{{{0}}}'.format(','.join(
['{0}="{1}"'.format(
Expand Down
10 changes: 10 additions & 0 deletions prometheus_client/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ def text_fd_to_metric_families(fd):
allowed_names = []

def build_metric(name, documentation, typ, samples):
# Munge counters into OpenMetrics representation
# used internally.
if typ == 'counter':
if name.endswith('_total'):
name = name[:-6]
else:
new_samples = []
for s in samples:
new_samples.append(tuple((s[0] + '_total', ) + s[1:]))
samples = new_samples
metric = core.Metric(name, documentation, typ)
metric.samples = samples
return metric
Expand Down
105 changes: 57 additions & 48 deletions tests/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@
class TestCounter(unittest.TestCase):
def setUp(self):
self.registry = CollectorRegistry()
self.counter = Counter('c', 'help', registry=self.registry)
self.counter = Counter('c_total', 'help', registry=self.registry)

def test_increment(self):
self.assertEqual(0, self.registry.get_sample_value('c'))
self.assertEqual(0, self.registry.get_sample_value('c_total'))
self.counter.inc()
self.assertEqual(1, self.registry.get_sample_value('c'))
self.assertEqual(1, self.registry.get_sample_value('c_total'))
self.counter.inc(7)
self.assertEqual(8, self.registry.get_sample_value('c'))
self.assertEqual(8, self.registry.get_sample_value('c_total'))

def test_negative_increment_raises(self):
self.assertRaises(ValueError, self.counter.inc, -1)
Expand All @@ -54,18 +54,18 @@ def f(r):
f(False)
except TypeError:
pass
self.assertEqual(0, self.registry.get_sample_value('c'))
self.assertEqual(0, self.registry.get_sample_value('c_total'))

try:
f(True)
except ValueError:
pass
self.assertEqual(1, self.registry.get_sample_value('c'))
self.assertEqual(1, self.registry.get_sample_value('c_total'))

def test_block_decorator(self):
with self.counter.count_exceptions():
pass
self.assertEqual(0, self.registry.get_sample_value('c'))
self.assertEqual(0, self.registry.get_sample_value('c_total'))

raised = False
try:
Expand All @@ -74,7 +74,7 @@ def test_block_decorator(self):
except:
raised = True
self.assertTrue(raised)
self.assertEqual(1, self.registry.get_sample_value('c'))
self.assertEqual(1, self.registry.get_sample_value('c_total'))


class TestGauge(unittest.TestCase):
Expand Down Expand Up @@ -342,23 +342,23 @@ def test_block_decorator(self):
class TestMetricWrapper(unittest.TestCase):
def setUp(self):
self.registry = CollectorRegistry()
self.counter = Counter('c', 'help', labelnames=['l'], registry=self.registry)
self.counter = Counter('c_total', 'help', labelnames=['l'], registry=self.registry)
self.two_labels = Counter('two', 'help', labelnames=['a', 'b'], registry=self.registry)

def test_child(self):
self.counter.labels('x').inc()
self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
self.two_labels.labels('x', 'y').inc(2)
self.assertEqual(2, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
self.assertEqual(2, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))

def test_remove(self):
self.counter.labels('x').inc()
self.counter.labels('y').inc(2)
self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'y'}))
self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'}))
self.counter.remove('x')
self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'x'}))
self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'y'}))
self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'x'}))
self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'y'}))

def test_incorrect_label_count_raises(self):
self.assertRaises(ValueError, self.counter.labels)
Expand All @@ -369,10 +369,10 @@ def test_incorrect_label_count_raises(self):
def test_labels_coerced_to_string(self):
self.counter.labels(None).inc()
self.counter.labels(l=None).inc()
self.assertEqual(2, self.registry.get_sample_value('c', {'l': 'None'}))
self.assertEqual(2, self.registry.get_sample_value('c_total', {'l': 'None'}))

self.counter.remove(None)
self.assertEqual(None, self.registry.get_sample_value('c', {'l': 'None'}))
self.assertEqual(None, self.registry.get_sample_value('c_total', {'l': 'None'}))

def test_non_string_labels_raises(self):
class Test(object):
Expand All @@ -381,18 +381,18 @@ class Test(object):
self.assertRaises(TypeError, self.counter.labels, l=Test())

def test_namespace_subsystem_concatenated(self):
c = Counter('c', 'help', namespace='a', subsystem='b', registry=self.registry)
c = Counter('c_total', 'help', namespace='a', subsystem='b', registry=self.registry)
c.inc()
self.assertEqual(1, self.registry.get_sample_value('a_b_c'))
self.assertEqual(1, self.registry.get_sample_value('a_b_c_total'))

def test_labels_by_kwarg(self):
self.counter.labels(l='x').inc()
self.assertEqual(1, self.registry.get_sample_value('c', {'l': 'x'}))
self.assertEqual(1, self.registry.get_sample_value('c_total', {'l': 'x'}))
self.assertRaises(ValueError, self.counter.labels, l='x', m='y')
self.assertRaises(ValueError, self.counter.labels, m='y')
self.assertRaises(ValueError, self.counter.labels)
self.two_labels.labels(a='x', b='y').inc()
self.assertEqual(1, self.registry.get_sample_value('two', {'a': 'x', 'b': 'y'}))
self.assertEqual(1, self.registry.get_sample_value('two_total', {'a': 'x', 'b': 'y'}))
self.assertRaises(ValueError, self.two_labels.labels, a='x', b='y', c='z')
self.assertRaises(ValueError, self.two_labels.labels, a='x', c='z')
self.assertRaises(ValueError, self.two_labels.labels, b='y', c='z')
Expand All @@ -405,10 +405,10 @@ def test_invalid_names_raise(self):
self.assertRaises(ValueError, Counter, '^', 'help')
self.assertRaises(ValueError, Counter, '', 'help', namespace='&')
self.assertRaises(ValueError, Counter, '', 'help', subsystem='(')
self.assertRaises(ValueError, Counter, 'c', '', labelnames=['^'])
self.assertRaises(ValueError, Counter, 'c', '', labelnames=['a:b'])
self.assertRaises(ValueError, Counter, 'c', '', labelnames=['__reserved'])
self.assertRaises(ValueError, Summary, 'c', '', labelnames=['quantile'])
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['^'])
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['a:b'])
self.assertRaises(ValueError, Counter, 'c_total', '', labelnames=['__reserved'])
self.assertRaises(ValueError, Summary, 'c_total', '', labelnames=['quantile'])

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

def test_counter(self):
self.custom_collector(CounterMetricFamily('c', 'help', value=1))
self.assertEqual(1, self.registry.get_sample_value('c', {}))
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
self.assertEqual(1, self.registry.get_sample_value('c_total', {}))

def test_counter_total(self):
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1))
self.assertEqual(1, self.registry.get_sample_value('c_total', {}))

def test_counter_labels(self):
cmf = CounterMetricFamily('c', 'help', labels=['a', 'c'])
cmf = CounterMetricFamily('c_total', 'help', labels=['a', 'c_total'])
cmf.add_metric(['b', 'd'], 2)
self.custom_collector(cmf)
self.assertEqual(2, self.registry.get_sample_value('c', {'a': 'b', 'c': 'd'}))
self.assertEqual(2, self.registry.get_sample_value('c_total', {'a': 'b', 'c_total': 'd'}))

def test_gauge(self):
self.custom_collector(GaugeMetricFamily('g', 'help', value=1))
Expand Down Expand Up @@ -490,8 +494,8 @@ def test_bad_constructors(self):
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[])
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])

self.assertRaises(ValueError, CounterMetricFamily, 'c', 'help', value=1, labels=[])
self.assertRaises(ValueError, CounterMetricFamily, 'c', 'help', value=1, labels=['a'])
self.assertRaises(ValueError, CounterMetricFamily, 'c_total', 'help', value=1, labels=[])
self.assertRaises(ValueError, CounterMetricFamily, 'c_total', 'help', value=1, labels=['a'])

self.assertRaises(ValueError, GaugeMetricFamily, 'g', 'help', value=1, labels=[])
self.assertRaises(ValueError, GaugeMetricFamily, 'g', 'help', value=1, labels=['a'])
Expand All @@ -512,7 +516,7 @@ def test_bad_constructors(self):
def test_labelnames(self):
cmf = UntypedMetricFamily('u', 'help', labels=iter(['a']))
self.assertEqual(('a',), cmf._labelnames)
cmf = CounterMetricFamily('c', 'help', labels=iter(['a']))
cmf = CounterMetricFamily('c_total', 'help', labels=iter(['a']))
self.assertEqual(('a',), cmf._labelnames)
gmf = GaugeMetricFamily('g', 'help', labels=iter(['a']))
self.assertEqual(('a',), gmf._labelnames)
Expand All @@ -525,16 +529,20 @@ def test_labelnames(self):
class TestCollectorRegistry(unittest.TestCase):
def test_duplicate_metrics_raises(self):
registry = CollectorRegistry()
Counter('c', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 'c', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'c', 'help', registry=registry)
Counter('c_total', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 'c_total', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'c_total', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'c_created', 'help', registry=registry)

Gauge('g', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'g', 'help', registry=registry)
Gauge('g_created', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'g_created', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 'g', 'help', registry=registry)

Summary('s', 'help', registry=registry)
self.assertRaises(ValueError, Summary, 's', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 's_created', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 's_sum', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
# We don't currently expose quantiles, but let's prevent future
# clashes anyway.
self.assertRaises(ValueError, Gauge, 's', 'help', registry=registry)
Expand All @@ -543,18 +551,19 @@ def test_duplicate_metrics_raises(self):
self.assertRaises(ValueError, Histogram, 'h', 'help', registry=registry)
# Clashes aggaint various suffixes.
self.assertRaises(ValueError, Summary, 'h', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 'h_count', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 'h_sum', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 'h_bucket', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'h_count', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'h_sum', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'h_bucket', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 'h_created', 'help', registry=registry)
# The name of the histogram itself isn't taken.
Counter('h', 'help', registry=registry)
Gauge('h', 'help', registry=registry)

def test_unregister_works(self):
registry = CollectorRegistry()
s = Summary('s', 'help', registry=registry)
self.assertRaises(ValueError, Counter, 's_count', 'help', registry=registry)
self.assertRaises(ValueError, Gauge, 's_count', 'help', registry=registry)
registry.unregister(s)
Counter('s_count', 'help', registry=registry)
Gauge('s_count', 'help', registry=registry)

def custom_collector(self, metric_family, registry):
class CustomCollector(object):
Expand All @@ -564,16 +573,16 @@ def collect(self):

def test_autodescribe_disabled_by_default(self):
registry = CollectorRegistry()
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)

registry = CollectorRegistry(auto_describe=True)
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c', 'help', value=1), registry)
self.custom_collector(CounterMetricFamily('c_total', 'help', value=1), registry)
self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c_total', 'help', value=1), registry)

def test_restricted_registry(self):
registry = CollectorRegistry()
Counter('c', 'help', registry=registry)
Counter('c_total', 'help', registry=registry)
Summary('s', 'help', registry=registry).observe(7)

m = Metric('s', 'help', 'summary')
Expand Down
10 changes: 7 additions & 3 deletions tests/test_exposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ def setUp(self):
def test_counter(self):
c = Counter('cc', 'A counter', registry=self.registry)
c.inc()
self.assertEqual(b'# HELP cc A counter\n# TYPE cc counter\ncc 1.0\n', generate_latest(self.registry))
self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))

def test_counter_total(self):
c = Counter('cc_total', 'A counter', registry=self.registry)
c.inc()
self.assertEqual(b'# HELP cc_total A counter\n# TYPE cc_total counter\ncc_total 1.0\n', generate_latest(self.registry))
def test_gauge(self):
g = Gauge('gg', 'A gauge', registry=self.registry)
g.set(17)
Expand Down Expand Up @@ -71,12 +75,12 @@ def test_histogram(self):
def test_unicode(self):
c = Counter('cc', '\u4500', ['l'], registry=self.registry)
c.labels('\u4500').inc()
self.assertEqual(b'# HELP cc \xe4\x94\x80\n# TYPE cc counter\ncc{l="\xe4\x94\x80"} 1.0\n', generate_latest(self.registry))
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))

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

def test_nonnumber(self):

Expand Down
Loading

0 comments on commit a4dd93b

Please sign in to comment.