From 91f8ae0244248e7e5bb34beb7ffff29063303030 Mon Sep 17 00:00:00 2001 From: Andreas Zeidler Date: Wed, 1 Dec 2021 11:29:59 +0100 Subject: [PATCH 1/4] Allow to add labels inside a context manager This way labels that depend on the result of the measured operation can be added more conveniently, e.g. the status code of an http request: from prometheus_client import Histogram from requests import get teapot = Histogram('teapot', 'A teapot', ['status']) with teapot.time() as metric: response = get('https://httpbin.org/status/418') metric.labels(status=response.status_code) Signed-off-by: Andreas Zeidler --- prometheus_client/context_managers.py | 7 +++++++ tests/test_core.py | 9 +++++++++ 2 files changed, 16 insertions(+) diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py index 0704d1e5..bb2ae847 100644 --- a/prometheus_client/context_managers.py +++ b/prometheus_client/context_managers.py @@ -50,12 +50,19 @@ def _new_timer(self): def __enter__(self): self._start = default_timer() + return self def __exit__(self, typ, value, traceback): # Time can go backwards. duration = max(default_timer() - self._start, 0) self._callback(duration) + def labels(self, *args, **kw): + instance = self._callback.__self__ + self._callback = getattr( + instance.labels(*args, **kw), + self._callback.__name__) + def __call__(self, f): def wrapped(func, *args, **kwargs): # Obtaining new instance of timer every time diff --git a/tests/test_core.py b/tests/test_core.py index 349e4753..63b536b6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -435,6 +435,15 @@ 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'})) + def test_block_decorator_with_label(self): + value = self.registry.get_sample_value + self.assertEqual(None, value('hl_count', {'l': 'a'})) + self.assertEqual(None, value('hl_bucket', {'le': '+Inf', 'l': 'a'})) + with self.labels.time() as metric: + metric.labels('a') + self.assertEqual(1, value('hl_count', {'l': 'a'})) + self.assertEqual(1, value('hl_bucket', {'le': '+Inf', 'l': 'a'})) + def test_exemplar_invalid_label_name(self): self.assertRaises(ValueError, self.histogram.observe, 3.0, exemplar={':o)': 'smile'}) self.assertRaises(ValueError, self.histogram.observe, 3.0, exemplar={'1': 'number'}) From 2b57f24c5f6879090b49443152675fd807951230 Mon Sep 17 00:00:00 2001 From: Andreas Zeidler Date: Wed, 1 Dec 2021 11:34:36 +0100 Subject: [PATCH 2/4] Also allow to add deferred labels for 'gauge' and 'summary' metrics For this to work the 'observability' check needs to be deferred as well, in case a label is added inside the context manager thereby making the metric observable. Signed-off-by: Andreas Zeidler --- prometheus_client/context_managers.py | 2 ++ prometheus_client/metrics.py | 2 -- tests/test_core.py | 27 +++++++++++++++++++++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py index bb2ae847..eba60bcb 100644 --- a/prometheus_client/context_managers.py +++ b/prometheus_client/context_managers.py @@ -55,6 +55,8 @@ def __enter__(self): def __exit__(self, typ, value, traceback): # Time can go backwards. duration = max(default_timer() - self._start, 0) + instance = self._callback.__self__ + instance._raise_if_not_observable() self._callback(duration) def labels(self, *args, **kw): diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index ea797e9d..252b859e 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -396,7 +396,6 @@ def time(self): Can be used as a function decorator or context manager. """ - self._raise_if_not_observable() return Timer(self.set) def set_function(self, f): @@ -475,7 +474,6 @@ def time(self): Can be used as a function decorator or context manager. """ - self._raise_if_not_observable() return Timer(self.observe) def _child_samples(self): diff --git a/tests/test_core.py b/tests/test_core.py index 63b536b6..38bc20c4 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -222,13 +222,25 @@ def test_time_block_decorator(self): time.sleep(.001) self.assertNotEqual(0, self.registry.get_sample_value('g')) + def test_time_block_decorator_with_label(self): + value = self.registry.get_sample_value + self.assertEqual(None, value('g2', {'label1': 'foo'})) + with self.gauge_with_label.time() as metric: + metric.labels('foo') + self.assertLess(0, value('g2', {'label1': 'foo'})) + def test_track_in_progress_not_observable(self): g = Gauge('test', 'help', labelnames=('label',), registry=self.registry) assert_not_observable(g.track_inprogress) def test_timer_not_observable(self): g = Gauge('test', 'help', labelnames=('label',), registry=self.registry) - assert_not_observable(g.time) + + def manager(): + with g.time(): + pass + + assert_not_observable(manager) class TestSummary(unittest.TestCase): @@ -318,10 +330,21 @@ def test_block_decorator(self): pass self.assertEqual(1, self.registry.get_sample_value('s_count')) + def test_block_decorator_with_label(self): + value = self.registry.get_sample_value + self.assertEqual(None, value('s_with_labels_count', {'label1': 'foo'})) + with self.summary_with_labels.time() as metric: + metric.labels('foo') + self.assertEqual(1, value('s_with_labels_count', {'label1': 'foo'})) + def test_timer_not_observable(self): s = Summary('test', 'help', labelnames=('label',), registry=self.registry) - assert_not_observable(s.time) + def manager(): + with s.time(): + pass + + assert_not_observable(manager) class TestHistogram(unittest.TestCase): From 4f400d0e6becbead187f7206f580ee88bf367fca Mon Sep 17 00:00:00 2001 From: Andreas Zeidler Date: Sat, 11 Dec 2021 13:40:20 +0100 Subject: [PATCH 3/4] Pass metric instance and callback name to `Timer` This should make the code slightly more readable. Signed-off-by: Andreas Zeidler --- prometheus_client/context_managers.py | 18 ++++++++---------- prometheus_client/metrics.py | 6 +++--- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py index eba60bcb..c18a6e34 100644 --- a/prometheus_client/context_managers.py +++ b/prometheus_client/context_managers.py @@ -42,11 +42,12 @@ def wrapped(func, *args, **kwargs): class Timer: - def __init__(self, callback): - self._callback = callback + def __init__(self, metric, callback_name): + self._metric = metric + self._callback_name = callback_name def _new_timer(self): - return self.__class__(self._callback) + return self.__class__(self._metric, self._callback_name) def __enter__(self): self._start = default_timer() @@ -55,15 +56,12 @@ def __enter__(self): def __exit__(self, typ, value, traceback): # Time can go backwards. duration = max(default_timer() - self._start, 0) - instance = self._callback.__self__ - instance._raise_if_not_observable() - self._callback(duration) + self._metric._raise_if_not_observable() + callback = getattr(self._metric, self._callback_name) + callback(duration) def labels(self, *args, **kw): - instance = self._callback.__self__ - self._callback = getattr( - instance.labels(*args, **kw), - self._callback.__name__) + self._metric = self._metric.labels(*args, **kw) def __call__(self, f): def wrapped(func, *args, **kwargs): diff --git a/prometheus_client/metrics.py b/prometheus_client/metrics.py index 252b859e..802adeb7 100644 --- a/prometheus_client/metrics.py +++ b/prometheus_client/metrics.py @@ -396,7 +396,7 @@ def time(self): Can be used as a function decorator or context manager. """ - return Timer(self.set) + return Timer(self, 'set') def set_function(self, f): """Call the provided function to return the Gauge value. @@ -474,7 +474,7 @@ def time(self): Can be used as a function decorator or context manager. """ - return Timer(self.observe) + return Timer(self, 'observe') def _child_samples(self): return ( @@ -598,7 +598,7 @@ def time(self): Can be used as a function decorator or context manager. """ - return Timer(self.observe) + return Timer(self, 'observe') def _child_samples(self): samples = [] From fdf5e10caf1eccbb3bad6ad4e8358b6c8082409b Mon Sep 17 00:00:00 2001 From: Andreas Zeidler Date: Wed, 15 Dec 2021 17:16:40 +0100 Subject: [PATCH 4/4] Remove redundant check for observability The callbacks are already taking care of this anyway. Signed-off-by: Andreas Zeidler --- prometheus_client/context_managers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/prometheus_client/context_managers.py b/prometheus_client/context_managers.py index c18a6e34..6a9fa801 100644 --- a/prometheus_client/context_managers.py +++ b/prometheus_client/context_managers.py @@ -56,7 +56,6 @@ def __enter__(self): def __exit__(self, typ, value, traceback): # Time can go backwards. duration = max(default_timer() - self._start, 0) - self._metric._raise_if_not_observable() callback = getattr(self._metric, self._callback_name) callback(duration)