diff --git a/prometheus_client/__init__.py b/prometheus_client/__init__.py index 416c25c7..835e5e08 100644 --- a/prometheus_client/__init__.py +++ b/prometheus_client/__init__.py @@ -7,7 +7,6 @@ import os import time import threading -from contextlib import contextmanager try: from BaseHTTPServer import BaseHTTPRequestHandler except ImportError: @@ -25,10 +24,9 @@ _MINUS_INF = float("-inf") - class CollectorRegistry(object): '''Metric collector registry. - + Collectors must have a no-argument method 'collect' that returns a list of Metric objects. The returned metrics should be consistent with the Prometheus exposition formats. @@ -58,7 +56,7 @@ def collect(self): def get_sample_value(self, name, labels=None): '''Returns the sample value, or None if not found. - + This is inefficient, and intended only for use in unittests. ''' if labels is None: @@ -75,6 +73,7 @@ def get_sample_value(self, name, labels=None): _METRIC_TYPES = ('counter', 'gauge', 'summary', 'histogram', 'untyped') + class Metric(object): '''A single metric and it's samples.''' def __init__(self, name, documentation, typ): @@ -169,10 +168,12 @@ def collect(): return init + @_MetricWrapper class Counter(object): _type = 'counter' _reserved_labelnames = [] + def __init__(self): self._value = 0.0 self._lock = Lock() @@ -194,10 +195,14 @@ def count_exceptions(self, exception=Exception): class ExceptionCounter(object): def __init__(self, counter): self._counter = counter - def __enter__(self): pass + + def __enter__(self): + pass + def __exit__(self, typ, value, traceback): if isinstance(value, exception): self._counter.inc() + def __call__(self, f): @wraps(f) def wrapped(*args, **kwargs): @@ -210,10 +215,12 @@ def _samples(self): with self._lock: return (('', {}, self._value), ) + @_MetricWrapper class Gauge(object): _type = 'gauge' _reserved_labelnames = [] + def __init__(self): self._value = 0.0 self._lock = Lock() @@ -247,10 +254,13 @@ def track_inprogress(self): class InprogressTracker(object): def __init__(self, gauge): self._gauge = gauge + def __enter__(self): self._gauge.inc() + def __exit__(self, typ, value, traceback): self._gauge.dec() + def __call__(self, f): @wraps(f) def wrapped(*args, **kwargs): @@ -263,10 +273,12 @@ def _samples(self): with self._lock: return (('', {}, self._value), ) + @_MetricWrapper class Summary(object): _type = 'summary' _reserved_labelnames = ['quantile'] + def __init__(self): self._count = 0.0 self._sum = 0.0 @@ -286,11 +298,14 @@ def time(self): class Timer(object): def __init__(self, summary): self._summary = summary + def __enter__(self): self._start = time.time() + def __exit__(self, typ, value, traceback): # Time can go backwards. self._summary.observe(max(time.time() - self._start, 0)) + def __call__(self, f): @wraps(f) def wrapped(*args, **kwargs): @@ -305,6 +320,7 @@ def _samples(self): ('_count', {}, self._count), ('_sum', {}, self._sum)) + def _floatToGoString(d): if d == _INF: return '+Inf' @@ -313,14 +329,16 @@ def _floatToGoString(d): else: return repr(d) + @_MetricWrapper class Histogram(object): _type = 'histogram' _reserved_labelnames = ['histogram'] + def __init__(self, buckets=(.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, _INF)): self._sum = 0.0 self._lock = Lock() - buckets = [float (b) for b in buckets] + buckets = [float(b) for b in buckets] if buckets != sorted(buckets): # This is probably an error on the part of the user, # so raise rather than sorting for them. @@ -349,11 +367,14 @@ def time(self): class Timer(object): def __init__(self, histogram): self._histogram = histogram + def __enter__(self): self._start = time.time() + def __exit__(self, typ, value, traceback): # Time can go backwards. self._histogram.observe(max(time.time() - self._start, 0)) + def __call__(self, f): @wraps(f) def wrapped(*args, **kwargs): @@ -373,10 +394,11 @@ def _samples(self): samples.append(('_sum', {}, self._sum)) return tuple(samples) - + CONTENT_TYPE_LATEST = 'text/plain; version=0.0.4; charset=utf-8' '''Content type of the latest text format''' + def generate_latest(registry=REGISTRY): '''Returns the metrics from the registry in latest text format as a string.''' output = [] @@ -403,6 +425,7 @@ def do_GET(self): self.end_headers() self.wfile.write(generate_latest(REGISTRY)) + def write_to_textfile(path, registry): '''Write metrics to the given path. @@ -418,13 +441,13 @@ def write_to_textfile(path, registry): if __name__ == '__main__': c = Counter('cc', 'A counter') c.inc() - + g = Gauge('gg', 'A gauge') g.set(17) - + s = Summary('ss', 'A summary', ['a', 'b']) s.labels('c', 'd').observe(17) - + h = Histogram('hh', 'A histogram') h.observe(.6) diff --git a/tests/test_client.py b/tests/test_client.py index 535f2e86..7f3dfd49 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,6 +4,7 @@ from prometheus_client import Gauge, Counter, Summary, Histogram from prometheus_client import CollectorRegistry, generate_latest + class TestCounter(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -15,7 +16,7 @@ def test_increment(self): self.assertEqual(1, self.registry.get_sample_value('c')) self.counter.inc(7) self.assertEqual(8, self.registry.get_sample_value('c')) - + def test_negative_increment_raises(self): self.assertRaises(ValueError, self.counter.inc, -1) @@ -50,6 +51,7 @@ def test_block_decorator(self): self.assertTrue(raised) self.assertEqual(1, self.registry.get_sample_value('c')) + class TestGauge(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -66,6 +68,7 @@ def test_gauge(self): def test_function_decorator(self): self.assertEqual(0, self.registry.get_sample_value('g')) + @self.gauge.track_inprogress() def f(): self.assertEqual(1, self.registry.get_sample_value('g')) @@ -78,6 +81,7 @@ def test_block_decorator(self): self.assertEqual(1, self.registry.get_sample_value('g')) self.assertEqual(0, self.registry.get_sample_value('g')) + class TestSummary(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -92,6 +96,7 @@ def test_summary(self): def test_function_decorator(self): self.assertEqual(0, self.registry.get_sample_value('s_count')) + @self.summary.time() def f(): pass @@ -104,6 +109,7 @@ def test_block_decorator(self): pass self.assertEqual(1, self.registry.get_sample_value('s_count')) + class TestHistogram(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -165,6 +171,7 @@ def test_labels(self): def test_function_decorator(self): self.assertEqual(0, self.registry.get_sample_value('h_count')) self.assertEqual(0, self.registry.get_sample_value('h_bucket', {'le': '+Inf'})) + @self.histogram.time() def f(): pass @@ -180,6 +187,7 @@ def test_block_decorator(self): self.assertEqual(1, self.registry.get_sample_value('h_count')) self.assertEqual(1, self.registry.get_sample_value('h_bucket', {'le': '+Inf'})) + class TestMetricWrapper(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry() @@ -221,6 +229,7 @@ def test_invalid_names_raise(self): self.assertRaises(ValueError, Counter, 'c', '', labelnames=['__reserved']) self.assertRaises(ValueError, Summary, 'c', '', labelnames=['quantile']) + class TestGenerateText(unittest.TestCase): def setUp(self): self.registry = CollectorRegistry()