diff --git a/swh/core/statsd.py b/swh/core/statsd.py --- a/swh/core/statsd.py +++ b/swh/core/statsd.py @@ -73,12 +73,14 @@ Attributes: elapsed (float): the elapsed time at the point of completion """ - def __init__(self, statsd, metric=None, tags=None, sample_rate=1): + def __init__(self, statsd, metric=None, error_metric=None, + tags=None, sample_rate=1): self.statsd = statsd self.metric = metric + self.error_metric = error_metric self.tags = tags self.sample_rate = sample_rate - self.elapsed = None + self.elapsed = None # this is for testing purpose def __call__(self, func): """ @@ -96,9 +98,11 @@ start = monotonic() try: result = await func(*args, **kwargs) - return result - finally: - self._send(start) + except: # noqa + self._send_error() + raise + self._send(start) + return result return wrapped_co # Others @@ -106,9 +110,12 @@ def wrapped(*args, **kwargs): start = monotonic() try: - return func(*args, **kwargs) - finally: - self._send(start) + result = func(*args, **kwargs) + except: # noqa + self._send_error() + raise + self._send(start) + return result return wrapped def __enter__(self): @@ -118,14 +125,23 @@ return self def __exit__(self, type, value, traceback): - # Report the elapsed time of the context manager. - self._send(self._start) + # Report the elapsed time of the context manager if no error. + if type is None: + self._send(self._start) + else: + self._send_error() def _send(self, start): elapsed = (monotonic() - start) * 1000 - self.statsd.timing(self.metric, elapsed, self.tags, self.sample_rate) + self.statsd.timing(self.metric, elapsed, + tags=self.tags, sample_rate=self.sample_rate) self.elapsed = elapsed + def _send_error(self): + if self.error_metric is None: + self.error_metric = self.metric + '_error_count' + self.statsd.increment(self.error_metric, tags=self.tags) + def start(self): """Start the timer""" self.__enter__() @@ -192,7 +208,6 @@ UserWarning, ) continue - print(tag) k, v = tag.split(':', 1) self.constant_tags[k] = v @@ -262,7 +277,7 @@ """ self._report(metric, 'ms', value, tags, sample_rate) - def timed(self, metric=None, tags=None, sample_rate=1): + def timed(self, metric=None, error_metric=None, tags=None, sample_rate=1): """ A decorator or context manager that will measure the distribution of a function's/context's run time. Optionally specify a list of tags or a @@ -288,7 +303,10 @@ finally: statsd.timing('user.query.time', time.monotonic() - start) """ - return TimedContextManagerDecorator(self, metric, tags, sample_rate) + return TimedContextManagerDecorator( + statsd=self, metric=metric, + error_metric=error_metric, + tags=tags, sample_rate=sample_rate) def set(self, metric, value, tags=None, sample_rate=1): """ @@ -369,7 +387,6 @@ for (k, v) in sorted(tags.items()) )) if tags else "", ) - # Send it self._send(payload) diff --git a/swh/core/tests/test_statsd.py b/swh/core/tests/test_statsd.py --- a/swh/core/tests/test_statsd.py +++ b/swh/core/tests/test_statsd.py @@ -246,6 +246,31 @@ self.assertEqual('timed.test', name) self.assert_almost_equal(500, float(value), 100) + def test_timed_exception(self): + """ + Exception bubble out of the decorator and is reported + to statsd as a dedicated counter. + """ + @self.statsd.timed('timed.test') + def func(a, b, c=1, d=1): + """docstring""" + time.sleep(0.5) + return (a / b, c, d) + + self.assertEqual('func', func.__name__) + self.assertEqual('docstring', func.__doc__) + + with self.assertRaises(ZeroDivisionError): + func(1, 0) + + packet = self.recv() + name_value, type_ = packet.split('|') + name, value = name_value.split(':') + + self.assertEqual('c', type_) + self.assertEqual('timed.test_error_count', name) + self.assertEqual(int(value), 1) + def test_timed_no_metric(self, ): """ Test using a decorator without providing a metric. @@ -320,13 +345,14 @@ def test_timed_context_exception(self): """ - Exception bubbles out of the `timed` context manager. + Exception bubbles out of the `timed` context manager and is + reported to statsd as a dedicated counter. """ class ContextException(Exception): pass def func(self): - with self.statsd.timed('timed_context.test.exception'): + with self.statsd.timed('timed_context.test'): time.sleep(0.5) raise ContextException() @@ -338,9 +364,9 @@ name_value, type_ = packet.split('|') name, value = name_value.split(':') - self.assertEqual('ms', type_) - self.assertEqual('timed_context.test.exception', name) - self.assert_almost_equal(500, float(value), 100) + self.assertEqual('c', type_) + self.assertEqual('timed_context.test_error_count', name) + self.assertEqual(int(value), 1) def test_timed_context_no_metric_name_exception(self): """Test that an exception occurs if using a context manager without a