Skip to content

Commit 1807665

Browse files
committed
Add Info metric type.
Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent 682e5ec commit 1807665

File tree

7 files changed

+137
-10
lines changed

7 files changed

+137
-10
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Counters go up, and reset when the process restarts.
6767

6868
```python
6969
from prometheus_client import Counter
70-
c = Counter('my_failures_total', 'Description of counter')
70+
c = Counter('my_failures', 'Description of counter')
7171
c.inc() # Increment by 1
7272
c.inc(1.6) # Increment by given value
7373
```
@@ -169,6 +169,17 @@ with h.time():
169169
pass
170170
```
171171

172+
173+
### Info
174+
175+
Info tracks key-value information, usually about a whole target.
176+
177+
```python
178+
from prometheus_client import Info
179+
i = Info('my_build_version', 'Description of info')
180+
i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
181+
```
182+
172183
### Labels
173184

174185
All metrics can have labels, allowing grouping of related time series.
@@ -414,6 +425,7 @@ This comes with a number of limitations:
414425

415426
- Registries can not be used as normal, all instantiated metrics are exported
416427
- Custom collectors do not work (e.g. cpu and memory metrics)
428+
- Info metrics do not work
417429
- The pushgateway cannot be used
418430
- Gauges cannot use the `pid` label
419431

prometheus_client/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from . import process_collector
66
from . import platform_collector
77

8-
__all__ = ['Counter', 'Gauge', 'Summary', 'Histogram']
8+
__all__ = ['Counter', 'Gauge', 'Summary', 'Histogram', 'Info']
99

1010
CollectorRegistry = core.CollectorRegistry
1111
REGISTRY = core.REGISTRY
@@ -14,6 +14,7 @@
1414
Gauge = core.Gauge
1515
Summary = core.Summary
1616
Histogram = core.Histogram
17+
Info = core.Info
1718

1819
CONTENT_TYPE_LATEST = exposition.CONTENT_TYPE_LATEST
1920
generate_latest = exposition.generate_latest

prometheus_client/core.py

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,8 @@ def _get_names(self, collector):
8787
type_suffixes = {
8888
'counter': ['_total', '_created'],
8989
'summary': ['', '_sum', '_count', '_created'],
90-
'histogram': ['_bucket', '_sum', '_count', '_created']
90+
'histogram': ['_bucket', '_sum', '_count', '_created'],
91+
'info': ['_info'],
9192
}
9293
for metric in desc_func():
9394
for suffix in type_suffixes.get(metric.type, ['']):
@@ -150,7 +151,7 @@ def get_sample_value(self, name, labels=None):
150151
REGISTRY = CollectorRegistry(auto_describe=True)
151152
'''The default registry.'''
152153

153-
_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped')
154+
_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped', 'info')
154155

155156

156157
class Metric(object):
@@ -327,6 +328,32 @@ def add_metric(self, labels, buckets, sum_value):
327328
self.samples.append((self.name + '_sum', dict(zip(self._labelnames, labels)), sum_value))
328329

329330

331+
class InfoMetricFamily(Metric):
332+
'''A single info and its samples.
333+
334+
For use by custom collectors.
335+
'''
336+
def __init__(self, name, documentation, value=None, labels=None):
337+
Metric.__init__(self, name, documentation, 'info')
338+
if labels is not None and value is not None:
339+
raise ValueError('Can only specify at most one of value and labels.')
340+
if labels is None:
341+
labels = []
342+
self._labelnames = tuple(labels)
343+
if value is not None:
344+
self.add_metric([], value)
345+
346+
def add_metric(self, labels, value):
347+
'''Add a metric to the metric family.
348+
349+
Args:
350+
labels: A list of label values
351+
value: A dict of labels
352+
'''
353+
self.samples.append((self.name + '_info',
354+
dict(dict(zip(self._labelnames, labels)), **value), 1))
355+
356+
330357
class _MutexValue(object):
331358
'''A float protected by a mutex.'''
332359

@@ -899,7 +926,7 @@ def create_response(request):
899926
**NB** The Python client doesn't store or expose quantile information at this time.
900927
'''
901928
_type = 'histogram'
902-
_reserved_labelnames = ['histogram']
929+
_reserved_labelnames = ['le']
903930

904931
def __init__(self, name, labelnames, labelvalues, buckets=(.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, _INF)):
905932
self._sum = _ValueClass(self._type, name, name + '_sum', labelnames, labelvalues)
@@ -944,6 +971,46 @@ def _samples(self):
944971
return tuple(samples)
945972

946973

974+
@_MetricWrapper
975+
class Info(object):
976+
'''Info metric, key-value pairs.
977+
978+
Examples of Info include:
979+
- Build information
980+
- Version information
981+
- Potential target metadata
982+
983+
Example usage:
984+
from prometheus_client import Info
985+
986+
g = Info('my_build_info', 'Description of info')
987+
g.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
988+
989+
Info metrics do not work in multiprocess mode.
990+
'''
991+
_type = 'info'
992+
_reserved_labelnames = []
993+
_labelnames = []
994+
_value = {}
995+
996+
def __init__(self, name, labelnames, labelvalues):
997+
self._labelnames = set(labelnames)
998+
self._lock = Lock()
999+
1000+
def info(self, val):
1001+
'''Set info metric.'''
1002+
if self._labelnames.intersection(val.keys()):
1003+
raise ValueError('Overlapping labels for Info metric, metric: %s child: %s' % (
1004+
self._labelnames, val))
1005+
with self._lock:
1006+
self._value = dict(val)
1007+
1008+
1009+
def _samples(self):
1010+
with self._lock:
1011+
return (('_info', self._value, 1.0,), )
1012+
1013+
9471014
class _ExceptionCounter(object):
9481015
def __init__(self, counter, exception):
9491016
self._counter = counter

prometheus_client/exposition.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from contextlib import closing
1111
from wsgiref.simple_server import make_server, WSGIRequestHandler
1212

13-
from . import core
14-
import openmetrics.exposition
13+
from prometheus_client import core
14+
from prometheus_client import openmetrics
1515
try:
1616
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
1717
from SocketServer import ThreadingMixIn
@@ -70,11 +70,15 @@ def generate_latest(registry=core.REGISTRY):
7070
output = []
7171
for metric in registry.collect():
7272
mname = metric.name
73-
if metric.type == 'counter':
73+
mtype = metric.type
74+
if mtype == 'counter':
7475
mname = mname + '_total'
76+
elif mtype == 'info':
77+
mname = mname + '_info'
78+
mtype = 'gauge'
7579
output.append('# HELP {0} {1}'.format(
7680
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
77-
output.append('\n# TYPE {0} {1}\n'.format(mname, metric.type))
81+
output.append('\n# TYPE {0} {1}\n'.format(mname, mtype))
7882
for name, labels, value in metric.samples:
7983
if name == metric.name + '_created':
8084
continue # Ignore OpenMetrics specific sample.

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
packages=[
1616
'prometheus_client',
1717
'prometheus_client.bridge',
18+
'prometheus_client.openmetrics',
1819
'prometheus_client.twisted',
1920
],
2021
extras_require={

tests/test_core.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
GaugeMetricFamily,
1919
Histogram,
2020
HistogramMetricFamily,
21+
Info,
22+
InfoMetricFamily,
2123
Metric,
2224
Summary,
2325
SummaryMetricFamily,
@@ -287,6 +289,8 @@ def test_setting_buckets(self):
287289
self.assertRaises(ValueError, Histogram, 'h', 'help', registry=None, buckets=[3, 1])
288290

289291
def test_labels(self):
292+
self.assertRaises(ValueError, Histogram, 'h2', 'help', registry=None, labelnames=['le'])
293+
290294
self.labels.labels('a').observe(2)
291295
self.assertEqual(0, self.registry.get_sample_value('hl_bucket', {'le': '1.0', 'l': 'a'}))
292296
self.assertEqual(1, self.registry.get_sample_value('hl_bucket', {'le': '2.5', 'l': 'a'}))
@@ -295,6 +299,7 @@ def test_labels(self):
295299
self.assertEqual(1, self.registry.get_sample_value('hl_count', {'l': 'a'}))
296300
self.assertEqual(2, self.registry.get_sample_value('hl_sum', {'l': 'a'}))
297301

302+
298303
def test_function_decorator(self):
299304
self.assertEqual(0, self.registry.get_sample_value('h_count'))
300305
self.assertEqual(0, self.registry.get_sample_value('h_bucket', {'le': '+Inf'}))
@@ -339,6 +344,25 @@ def test_block_decorator(self):
339344
self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': '+Inf'}))
340345

341346

347+
class TestInfo(unittest.TestCase):
348+
def setUp(self):
349+
self.registry = CollectorRegistry()
350+
self.info = Info('i', 'help', registry=self.registry)
351+
self.labels = Info('il', 'help', ['l'], registry=self.registry)
352+
353+
def test_info(self):
354+
self.assertEqual(1, self.registry.get_sample_value('i_info', {}))
355+
self.info.info({'a': 'b', 'c': 'd'})
356+
self.assertEqual(None, self.registry.get_sample_value('i_info', {}))
357+
self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b', 'c': 'd'}))
358+
359+
def test_labels(self):
360+
self.assertRaises(ValueError, self.labels.labels('a').info, {'l': ''})
361+
362+
self.labels.labels('a').info({'foo': 'bar'})
363+
self.assertEqual(1, self.registry.get_sample_value('il_info', {'l': 'a', 'foo': 'bar'}))
364+
365+
342366
class TestMetricWrapper(unittest.TestCase):
343367
def setUp(self):
344368
self.registry = CollectorRegistry()
@@ -490,6 +514,16 @@ def test_histogram_labels(self):
490514
self.assertEqual(2, self.registry.get_sample_value('h_count', {'a': 'b'}))
491515
self.assertEqual(3, self.registry.get_sample_value('h_sum', {'a': 'b'}))
492516

517+
def test_info(self):
518+
self.custom_collector(InfoMetricFamily('i', 'help', value={'a': 'b'}))
519+
self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b'}))
520+
521+
def test_info_labels(self):
522+
cmf = InfoMetricFamily('i', 'help', labels=['a'])
523+
cmf.add_metric(['b'], {'c': 'd'})
524+
self.custom_collector(cmf)
525+
self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b', 'c': 'd'}))
526+
493527
def test_bad_constructors(self):
494528
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[])
495529
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])
@@ -513,6 +547,9 @@ def test_bad_constructors(self):
513547
self.assertRaises(ValueError, HistogramMetricFamily, 'h', 'help', buckets={}, sum_value=1, labels=['a'])
514548
self.assertRaises(KeyError, HistogramMetricFamily, 'h', 'help', buckets={}, sum_value=1)
515549

550+
self.assertRaises(ValueError, InfoMetricFamily, 'i', 'help', value={}, labels=[])
551+
self.assertRaises(ValueError, InfoMetricFamily, 'i', 'help', value={}, labels=['a'])
552+
516553
def test_labelnames(self):
517554
cmf = UntypedMetricFamily('u', 'help', labels=iter(['a']))
518555
self.assertEqual(('a',), cmf._labelnames)

tests/test_exposition.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
else:
1010
import unittest
1111

12-
from prometheus_client import Gauge, Counter, Summary, Histogram, Metric
12+
from prometheus_client import Gauge, Counter, Summary, Histogram, Info, Metric
1313
from prometheus_client import CollectorRegistry, generate_latest
1414
from prometheus_client import push_to_gateway, pushadd_to_gateway, delete_from_gateway
1515
from prometheus_client import CONTENT_TYPE_LATEST, instance_ip_grouping_key
@@ -72,6 +72,11 @@ def test_histogram(self):
7272
hh_sum 0.05
7373
''', generate_latest(self.registry))
7474

75+
def test_info(self):
76+
i = Info('ii', 'A info', ['a', 'b'], registry=self.registry)
77+
i.labels('c', 'd').info({'foo': 'bar'})
78+
self.assertEqual(b'# HELP ii_info A info\n# TYPE ii_info gauge\nii_info{a="c",b="d",foo="bar"} 1.0\n', generate_latest(self.registry))
79+
7580
def test_unicode(self):
7681
c = Counter('cc', '\u4500', ['l'], registry=self.registry)
7782
c.labels('\u4500').inc()

0 commit comments

Comments
 (0)