Skip to content

Commit 1aa8405

Browse files
committed
Add support for Enum/StateSet
Signed-off-by: Brian Brazil <brian.brazil@robustperception.io>
1 parent 1807665 commit 1aa8405

File tree

6 files changed

+141
-10
lines changed

6 files changed

+141
-10
lines changed

README.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,6 @@ with h.time():
169169
pass
170170
```
171171

172-
173172
### Info
174173

175174
Info tracks key-value information, usually about a whole target.
@@ -180,6 +179,17 @@ i = Info('my_build_version', 'Description of info')
180179
i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
181180
```
182181

182+
### Enum
183+
184+
Enum tracks which of a set of states something is currently in.
185+
186+
```python
187+
from prometheus_client import Enum
188+
e = Enum('my_task_state', 'Description of enum',
189+
states=['starting', 'running', 'stopped'])
190+
e.state('running')
191+
```
192+
183193
### Labels
184194

185195
All metrics can have labels, allowing grouping of related time series.
@@ -425,7 +435,7 @@ This comes with a number of limitations:
425435

426436
- Registries can not be used as normal, all instantiated metrics are exported
427437
- Custom collectors do not work (e.g. cpu and memory metrics)
428-
- Info metrics do not work
438+
- Info and Enum metrics do not work
429439
- The pushgateway cannot be used
430440
- Gauges cannot use the `pid` label
431441

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', 'Info']
8+
__all__ = ['Counter', 'Gauge', 'Summary', 'Histogram', 'Info', 'Enum']
99

1010
CollectorRegistry = core.CollectorRegistry
1111
REGISTRY = core.REGISTRY
@@ -15,6 +15,7 @@
1515
Summary = core.Summary
1616
Histogram = core.Histogram
1717
Info = core.Info
18+
Enum = core.Enum
1819

1920
CONTENT_TYPE_LATEST = exposition.CONTENT_TYPE_LATEST
2021
generate_latest = exposition.generate_latest

prometheus_client/core.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,8 @@ def get_sample_value(self, name, labels=None):
151151
REGISTRY = CollectorRegistry(auto_describe=True)
152152
'''The default registry.'''
153153

154-
_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped', 'info')
154+
_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram',
155+
'untyped', 'info', 'stateset')
155156

156157

157158
class Metric(object):
@@ -354,6 +355,35 @@ def add_metric(self, labels, value):
354355
dict(dict(zip(self._labelnames, labels)), **value), 1))
355356

356357

358+
class StateSetMetricFamily(Metric):
359+
'''A single stateset and its samples.
360+
361+
For use by custom collectors.
362+
'''
363+
def __init__(self, name, documentation, value=None, labels=None):
364+
Metric.__init__(self, name, documentation, 'stateset')
365+
if labels is not None and value is not None:
366+
raise ValueError('Can only specify at most one of value and labels.')
367+
if labels is None:
368+
labels = []
369+
self._labelnames = tuple(labels)
370+
if value is not None:
371+
self.add_metric([], value)
372+
373+
def add_metric(self, labels, value):
374+
'''Add a metric to the metric family.
375+
376+
Args:
377+
labels: A list of label values
378+
value: A dict of string state names to booleans
379+
'''
380+
labels = tuple(labels)
381+
for state, enabled in value.items():
382+
v = (1 if enabled else 0)
383+
self.samples.append((self.name,
384+
dict(zip(self._labelnames + (self.name,), labels + (state,))), v))
385+
386+
357387
class _MutexValue(object):
358388
'''A float protected by a mutex.'''
359389

@@ -983,19 +1013,18 @@ class Info(object):
9831013
Example usage:
9841014
from prometheus_client import Info
9851015
986-
g = Info('my_build_info', 'Description of info')
987-
g.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
1016+
i = Info('my_build', 'Description of info')
1017+
i.info({'version': '1.2.3', 'buildhost': 'foo@bar'})
9881018
9891019
Info metrics do not work in multiprocess mode.
9901020
'''
9911021
_type = 'info'
9921022
_reserved_labelnames = []
993-
_labelnames = []
994-
_value = {}
9951023

9961024
def __init__(self, name, labelnames, labelvalues):
9971025
self._labelnames = set(labelnames)
9981026
self._lock = Lock()
1027+
self._value = {}
9991028

10001029
def info(self, val):
10011030
'''Set info metric.'''
@@ -1011,6 +1040,44 @@ def _samples(self):
10111040
return (('_info', self._value, 1.0,), )
10121041

10131042

1043+
@_MetricWrapper
1044+
class Enum(object):
1045+
'''Enum metric, which of a set of states is true.
1046+
1047+
Example usage:
1048+
from prometheus_client import Enum
1049+
1050+
e = Enum('task_state', 'Description of enum',
1051+
states=['starting', 'running', 'stopped'])
1052+
e.state('running')
1053+
1054+
The first listed state will be the default.
1055+
Enum metrics do not work in multiprocess mode.
1056+
'''
1057+
_type = 'stateset'
1058+
_reserved_labelnames = []
1059+
1060+
def __init__(self, name, labelnames, labelvalues, states=None):
1061+
if name in labelnames:
1062+
raise ValueError('Overlapping labels for Enum metric: %s' % (name,))
1063+
if not states:
1064+
raise ValueError('No states provided for Enum metric: %s' % (name,))
1065+
self._name = name
1066+
self._states = states
1067+
self._value = 0
1068+
self._lock = Lock()
1069+
1070+
def state(self, state):
1071+
'''Set enum metric state.'''
1072+
with self._lock:
1073+
self._value = self._states.index(state)
1074+
1075+
def _samples(self):
1076+
with self._lock:
1077+
return [('', {self._name: s}, 1 if i == self._value else 0,)
1078+
for i, s in enumerate(self._states)]
1079+
1080+
10141081
class _ExceptionCounter(object):
10151082
def __init__(self, counter, exception):
10161083
self._counter = counter

prometheus_client/exposition.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ def generate_latest(registry=core.REGISTRY):
7676
elif mtype == 'info':
7777
mname = mname + '_info'
7878
mtype = 'gauge'
79+
elif mtype == 'stateset':
80+
mtype = 'gauge'
7981
output.append('# HELP {0} {1}'.format(
8082
mname, metric.documentation.replace('\\', r'\\').replace('\n', r'\n')))
8183
output.append('\n# TYPE {0} {1}\n'.format(mname, mtype))

tests/test_core.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
HistogramMetricFamily,
2121
Info,
2222
InfoMetricFamily,
23+
Enum,
24+
StateSetMetricFamily,
2325
Metric,
2426
Summary,
2527
SummaryMetricFamily,
@@ -289,7 +291,7 @@ def test_setting_buckets(self):
289291
self.assertRaises(ValueError, Histogram, 'h', 'help', registry=None, buckets=[3, 1])
290292

291293
def test_labels(self):
292-
self.assertRaises(ValueError, Histogram, 'h2', 'help', registry=None, labelnames=['le'])
294+
self.assertRaises(ValueError, Histogram, 'h', 'help', registry=None, labelnames=['le'])
293295

294296
self.labels.labels('a').observe(2)
295297
self.assertEqual(0, self.registry.get_sample_value('hl_bucket', {'le': '1.0', 'l': 'a'}))
@@ -363,6 +365,35 @@ def test_labels(self):
363365
self.assertEqual(1, self.registry.get_sample_value('il_info', {'l': 'a', 'foo': 'bar'}))
364366

365367

368+
class TestEnum(unittest.TestCase):
369+
def setUp(self):
370+
self.registry = CollectorRegistry()
371+
self.enum = Enum('e', 'help', states=['a', 'b', 'c'], registry=self.registry)
372+
self.labels = Enum('el', 'help', ['l'], states=['a', 'b', 'c'], registry=self.registry)
373+
374+
def test_enum(self):
375+
self.assertEqual(1, self.registry.get_sample_value('e', {'e': 'a'}))
376+
self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'b'}))
377+
self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'c'}))
378+
379+
self.enum.state('b')
380+
self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'a'}))
381+
self.assertEqual(1, self.registry.get_sample_value('e', {'e': 'b'}))
382+
self.assertEqual(0, self.registry.get_sample_value('e', {'e': 'c'}))
383+
384+
self.assertRaises(ValueError, self.enum.state, 'd')
385+
self.assertRaises(ValueError, Enum, 'e', 'help', registry=None)
386+
387+
def test_labels(self):
388+
self.labels.labels('a').state('c')
389+
self.assertEqual(0, self.registry.get_sample_value('el', {'l': 'a', 'el': 'a'}))
390+
self.assertEqual(0, self.registry.get_sample_value('el', {'l': 'a', 'el': 'b'}))
391+
self.assertEqual(1, self.registry.get_sample_value('el', {'l': 'a', 'el': 'c'}))
392+
393+
e = Enum('e', 'help', registry=None, labelnames=['e'])
394+
self.assertRaises(ValueError, e.labels, '')
395+
396+
366397
class TestMetricWrapper(unittest.TestCase):
367398
def setUp(self):
368399
self.registry = CollectorRegistry()
@@ -524,6 +555,18 @@ def test_info_labels(self):
524555
self.custom_collector(cmf)
525556
self.assertEqual(1, self.registry.get_sample_value('i_info', {'a': 'b', 'c': 'd'}))
526557

558+
def test_stateset(self):
559+
self.custom_collector(StateSetMetricFamily('s', 'help', value={'a': True, 'b': True,}))
560+
self.assertEqual(1, self.registry.get_sample_value('s', {'s': 'a'}))
561+
self.assertEqual(1, self.registry.get_sample_value('s', {'s': 'b'}))
562+
563+
def test_stateset_labels(self):
564+
cmf = StateSetMetricFamily('s', 'help', labels=['foo'])
565+
cmf.add_metric(['bar'], {'a': False, 'b': False,})
566+
self.custom_collector(cmf)
567+
self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'a'}))
568+
self.assertEqual(0, self.registry.get_sample_value('s', {'foo': 'bar', 's': 'b'}))
569+
527570
def test_bad_constructors(self):
528571
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=[])
529572
self.assertRaises(ValueError, UntypedMetricFamily, 'u', 'help', value=1, labels=['a'])
@@ -550,6 +593,9 @@ def test_bad_constructors(self):
550593
self.assertRaises(ValueError, InfoMetricFamily, 'i', 'help', value={}, labels=[])
551594
self.assertRaises(ValueError, InfoMetricFamily, 'i', 'help', value={}, labels=['a'])
552595

596+
self.assertRaises(ValueError, StateSetMetricFamily, 's', 'help', value={'a': True}, labels=[])
597+
self.assertRaises(ValueError, StateSetMetricFamily, 's', 'help', value={'a': True}, labels=['a'])
598+
553599
def test_labelnames(self):
554600
cmf = UntypedMetricFamily('u', 'help', labels=iter(['a']))
555601
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, Info, Metric
12+
from prometheus_client import Gauge, Counter, Summary, Histogram, Info, Enum, 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
@@ -77,6 +77,11 @@ def test_info(self):
7777
i.labels('c', 'd').info({'foo': 'bar'})
7878
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))
7979

80+
def test_enum(self):
81+
i = Enum('ee', 'An enum', ['a', 'b'], registry=self.registry, states=['foo', 'bar'])
82+
i.labels('c', 'd').state('bar')
83+
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))
84+
8085
def test_unicode(self):
8186
c = Counter('cc', '\u4500', ['l'], registry=self.registry)
8287
c.labels('\u4500').inc()

0 commit comments

Comments
 (0)