8000 Add describe and autodescribe to detect dupes. · techscientist/client_python@65e3c8b · GitHub
[go: up one dir, main page]

Skip to content
< 8000 /react-partial>

Commit 65e3c8b

Browse files
committed
Add describe and autodescribe to detect dupes.
This works by adding an optional describe method to collectors, which returns data in the same format as collect (though hopefully without the samples). If present it is called at registration time. If describe is not present and auto_describe=True is set on the registry, then collect is called instead. This is enabled by default on the default registry, but disabled elsewhere. There's not much point doing extra checks for a transient registry used to push to a pushgateway, particularly if collection is expensive.
1 parent 5e9b0b1 commit 65e3c8b

File tree

3 files changed

+108
-6
lines changed

3 files changed

+108
-6
lines changed

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,19 @@ REGISTRY.register(CustomCollector())
365365

366366
`SummaryMetricFamily` and `HistogramMetricFamily` work similarly.
367367

368+
A collector may implement a `describe` method which returns metrics in the same
369+
format as `collect` (though you don't have to include the samples). This is
370+
used to predetermine the names of time series a `CollectorRegistry` exposes and
371+
thus to detect collisions and duplicate registrations.
372+
373+
Usually custom collectors do not have to implement `describe`. If `describe` is
374+
not implemented and the CollectorRegistry was created with `auto_desribe=True`
375+
(which is the case for the default registry) then `collect` will be called at
376+
registration time instead of `describe`. If this could cause problems, either
377+
implement a proper `describe`, or if that's not practical have `describe`
378+
return an empty list.
379+
380+
368381
## Multiprocess Mode (Gunicorn)
369382

370383
**Experimental: This feature is new and has rough edges.**

prometheus_client/core.py

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,25 +38,61 @@ class CollectorRegistry(object):
3838
Metric objects. The returned metrics should be consistent with the Prometheus
3939
exposition formats.
4040
'''
41-
def __init__(self):
42-
self._collectors = set()
41+
def __init__(self, auto_describe=False):
42+
self._collector_to_names = {}
43+
self._names_to_collectors = {}
44+
self._auto_describe = auto_describe
4345
self._lock = Lock()
4446

4547
def register(self, collector):
4648
'''Add a collector to the registry.'''
4749
with self._lock:
48-
self._collectors.add(collector)
50+
names = self._get_names(collector)
51+
for name in names:
52+
if name in self._names_to_collectors:
53+
raise ValueError('Timeseries already present '
54+
'in CollectorRegistry: ' + name)
55+
for name in names:
56+
self._names_to_collectors[name] = collector
57+
self._collector_to_names[collector] = names
4958

5059
def unregister(self, collector):
5160
'''Remove a collector from the registry.'''
5261
with self._lock:
53-
self._collectors.remove(collector)
62+
for name in self._collector_to_names[collector]:
63+
del self._names_to_collectors[name]
64+
del self._collector_to_names[collector]
65+
66+
def _get_names(self, collector):
67+
'''Get names of timeseries the collector produces.'''
68+
desc_func = None
69+
# If there's a describe function, use it.
70+
try:
71+
desc_func = collector.describe
72+
except AttributeError:
73+
pass
74+
# Otherwise, if auto describe is enabled use the collect function.
75+
if not desc_func and self._auto_describe:
76+
desc_func = collector.collect
77+
78+
if not desc_func:
79+
return []
80+
81+
result = []
82+
type_suffixes = {
83+
'summary': ['', '_sum', '_count'],
84+
'histogram': ['_bucket', '_sum', '_count']
85+
}
86+
for metric in desc_func():
87+
for suffix in type_suffixes.get(metric.type, ['']):
88+
result.append(metric.name + suffix)
89+
return result
5490

5591
def collect(self):
5692
'''Yields metrics from the collectors in the registry.'''
5793
collectors = None
5894
with self._lock:
59-
collectors = copy.copy(self._collectors)
95+
collectors = copy.copy(self._collector_to_names)
6096
for collector in collectors:
6197
for metric in collector.collect():
6298
yield metric
@@ -75,7 +111,7 @@ def get_sample_value(self, name, labels=None):
75111
return None
76112

77113

78-
REGISTRY = CollectorRegistry()
114+
REGISTRY = CollectorRegistry(auto_describe=True)
79115
'''The default registry.'''
80116

81117
_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped')
@@ -476,6 +512,10 @@ def init(name, documentation, labelnames=(), namespace='', subsystem='', registr
476512
if not _METRIC_NAME_RE.match(full_name):
477513
raise ValueError('Invalid metric name: ' + full_name)
478514

515+
def describe():
516+
return [Metric(full_name, documentation, cls._type)]
517+
collector.describe = describe
518+
479519
def collect():
480520
metric = Metric(full_name, documentation, cls._type)
481521
for suffix, labels, value in collector._samples():

tests/test_core.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,55 @@ def test_bad_constructors(self):
382382
self.assertRaises(ValueError, HistogramMetricFamily, 'h', 'help', buckets={}, sum_value=1, labels=['a'])
383383
self.assertRaises(KeyError, HistogramMetricFamily, 'h', 'help', buckets={}, sum_value=1)
384384

385+
class TestCollectorRegistry(unittest.TestCase):
386+
def test_duplicate_metrics_raises(self):
387+
registry = CollectorRegistry()
388+
Counter('c', 'help', registry=registry)
389+
self.assertRaises(ValueError, Counter, 'c', 'help', registry=registry)
390+
self.assertRaises(ValueError, Gauge, 'c', 'help', registry=registry)
391+
392+
Gauge('g', 'help', registry=registry)
393+
self.assertRaises(ValueError, Gauge, 'g', 'help', registry=registry)
394+
self.assertRaises(ValueError, Counter, 'g', 'help', registry=registry)
395+
396+
Summary('s', 'help', registry=registry)
397+
self.assertRaises(ValueError, Summary, 's', 'help', registry=registry)
398+
# We don't currently expose quantiles, but let's prevent future
399+
# clashes anyway.
400+
self.assertRaises(ValueError, Gauge, 's', 'help', registry=registry)
401+
402+
Histogram('h', 'help', registry=registry)
403+
self.assertRaises(ValueError, Histogram, 'h', 'help', registry=registry)
404+
# Clashes aggaint various suffixes.
405+
self.assertRaises(ValueError, Summary, 'h', 'help', registry=registry)
406+
self.assertRaises(ValueError, Counter, 'h_count', 'help', registry=registry)
407+
self.assertRaises(ValueError, Counter, 'h_sum', 'help', registry=registry)
408+
self.assertRaises(ValueError, Counter, 'h_bucket', 'help', registry=registry)
409+
# The name of the histogram itself isn't taken.
410+
Counter('h', 'help', registry=registry)
411+
412+
def test_unregister_works(self):
413+
registry = CollectorRegistry()
414+
s = Summary('s', 'help', registry=registry)
415+
self.assertRaises(ValueError, Counter, 's_count', 'help', registry=registry)
416+
registry.unregister(s)
417+
Counter('s_count', 'help', registry=registry)
418+
419+
def custom_collector(self, metric_family, registry):
420+
class CustomCollector(object):
421+
def collect(self):
422+
return [metric_family]
423+
registry.register(CustomCollector())
424+
425+
def test_autodescribe_disabled_by_default(self):
426+
registry = CollectorRegistry()
427+
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
428+
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
429+
430+
registry = CollectorRegistry(auto_describe=True)
431+
self.custom_collector(CounterMetricFamily('c', 'help', value=1), registry)
432+
self.assertRaises(ValueError, self.custom_collector, CounterMetricFamily('c', 'help', value=1), registry)
433+
385434

386435
if __name__ == '__main__':
387436
unittest.main()

0 commit comments

Comments
 (0)
0