8000 Merge pull request #110 from prometheus/desc · sonlinux/client_python@1020618 · GitHub
[go: up one dir, main page]

Skip to content

Commit 1020618

Browse files
authored
Merge pull request prometheus#110 from prometheus/desc
Add describe and limiting of http expostion
2 parents facada3 + 29e83ad commit 1020618

File tree

4 files changed

+158
-9
lines changed

4 files changed

+158
-9
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: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,29 +38,94 @@ 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
6399

100+
def restricted_registry(self, names):
101+
'''Returns object that only collects some metrics.
102+
103+
Returns an object which upon collect() will return
104+
only samples with the given names.
105+
106+
Intended usage is:
107+
generate_latest(REGISTRY.restricted_registry(['a_timeseries']))
108+
109+
Experimental.'''
110+
names = set(names)
111+
collectors = set()
112+
with self._lock:
113+
for name in names:
114+
if name in self._names_to_collectors:
115+
collectors.add(self._names_to_collectors[name])
116+
metrics = []
117+
for collector in collectors:
118+
for metric in collector.collect():
119+
samples = [s for s in metric.samples if s[0] in names]
120+
if samples:
121+
m = Metric(metric.name, metric.documentation, metric.type)
122+
m.samples = samples
123+
metrics.append(m)
124+
class RestrictedRegistry(object):
125+
def collect(self):
126+
return metrics
127+
return RestrictedRegistry()
128+
64129
def get_sample_value(self, name, labels=None):
65130
'''Returns the sample value, or None if not found.
66131
@@ -75,7 +140,7 @@ def get_sample_value(self, name, labels=None):
75140
return None
76141

77142

78-
REGISTRY = CollectorRegistry()
143+
REGISTRY = CollectorRegistry(auto_describe=True)
79144
'''The default registry.'''
80145

81146
_METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped')
@@ -476,6 +541,10 @@ def init(name, documentation, labelnames=(), namespace='', subsystem='', registr
476541
if not _METRIC_NAME_RE.match(full_name):
477542
raise ValueError('Invalid metric name: ' + full_name)
478543

544+
def describe():
545+
return [Metric(full_name, documentation, cls._type)]
546+
collector.describe = describe
547+
479548
def collect():
480549
metric = Metric(full_name, documentation, cls._type)
481550
for suffix, labels, value in collector._samples():

prometheus_client/exposition.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
from BaseHTTPServer import HTTPServer
1616
from urllib2 import build_opener, Request, HTTPHandler
1717
from urllib import quote_plus
18+
from urlparse import parse_qs, urlparse
1819
except ImportError:
1920
# Python 3
2021
unicode = str
2122
from http.server import BaseHTTPRequestHandler
2223
from http.server import HTTPServer
2324
from urllib.request import build_opener, Request, HTTPHandler
24-
from urllib.parse import quote_plus
25+
from urllib.parse import quote_plus, parse_qs, urlparse
2526

2627

2728
CONTENT_TYPE_LATEST = str('text/plain; version=0.0.4; charset=utf-8')
@@ -34,7 +35,11 @@ def prometheus_app(environ, start_response):
3435
status = str('200 OK')
3536
headers = [(str('Content-type'), CONTENT_TYPE_LATEST)]
3637
start_response(status, headers)
37-
return [generate_latest(registry)]
38+
params = parse_qs(environ['QUERY_STRING'])
39+
r = registry
40+
if 'name[]' in params:
41+
r = r.restricted_registry(params['name[]'])
42+
return [generate_latest(r)]
3843
return prometheus_app
3944

4045

@@ -73,7 +78,11 @@ def do_GET(self):
7378
self.send_response(200)
7479
self.send_header('Content-Type', CONTENT_TYPE_LATEST)
7580
self.end_headers()
76-
self.wfile.write(generate_latest(core.REGISTRY))
81+
registry = core.REGISTRY
82+
params = parse_qs(urlparse(self.path).query)
83+
if 'name[]' in params:
84+
registry = registry.restricted_registry(params['name[]'])
85+
self.wfile.write(generate_latest(registry))
7786

7887
def log_message(self, format, *args):
7988
return

tests/test_core.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,64 @@ 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+
434+
def test_restricted_registry(self):
435+
registry = CollectorRegistry()
436+
Counter('c', 'help', registry=registry)
437+
Summary('s', 'help', registry=registry).observe(7)
438+
439+
m = Metric('s', 'help', 'summary')
440+
m.samples = [('s_sum', {}, 7)]
441+
self.assertEquals([m], registry.restricted_registry(['s_sum']).collect())
442+
385443

386444
if __name__ == '__main__':
387445
unittest.main()

0 commit comments

Comments
 (0)
0