From 72625f2f4d633e9fe59e61bb371a118927e5c66c Mon Sep 17 00:00:00 2001 From: "Paul \"LeoNerd\" Evans" Date: Wed, 4 Mar 2015 19:22:14 +0000 Subject: [PATCH] Initial hack at a TimerMetric; for storing counts + duration accumulators --- synapse/metrics/metric.py | 48 ++++++++++++++++++++++++++++++++++++ tests/metrics/test_metric.py | 36 ++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/synapse/metrics/metric.py b/synapse/metrics/metric.py index 4df5ebfda..717588194 100644 --- a/synapse/metrics/metric.py +++ b/synapse/metrics/metric.py @@ -14,6 +14,15 @@ # limitations under the License. +from itertools import chain + + +# TODO(paul): I can't believe Python doesn't have one of these +def map_concat(func, items): + # flatten a list-of-lists + return list(chain.from_iterable(map(func, items))) + + class BaseMetric(object): def __init__(self, name, keys=[]): @@ -87,6 +96,45 @@ class CallbackMetric(BaseMetric): return ["%s{%s} %d" % (self.name, self._render_key(k), value[k]) for k in sorted(value.keys())] + +class TimerMetric(CounterMetric): + """A combination of an event counter and a time accumulator, which counts + both the number of events and how long each one takes. + + TODO(paul): Try to export some heatmap-style stats? + """ + + def __init__(self, *args, **kwargs): + super(TimerMetric, self).__init__(*args, **kwargs) + + self.times = {} + + # Scalar metrics are never empty + if self.is_scalar(): + self.times[()] = 0 + + def inc_time(self, msec, *values): + self.inc(*values) + + if values not in self.times: + self.times[values] = msec + else: + self.times[values] += msec + + def render(self): + if self.is_scalar(): + return ["%s:count %d" % (self.name, self.counts[()]), + "%s:msec %d" % (self.name, self.times[()])] + + def render_item(k): + keystr = self._render_key(k) + + return ["%s{%s}:count %d" % (self.name, keystr, self.counts[k]), + "%s{%s}:msec %d" % (self.name, keystr, self.times[k])] + + return map_concat(render_item, sorted(self.counts.keys())) + + class CacheMetric(object): """A combination of two CounterMetrics, one to count cache hits and one to count misses, and a callback metric to yield the current size. diff --git a/tests/metrics/test_metric.py b/tests/metrics/test_metric.py index b7facb858..b25520821 100644 --- a/tests/metrics/test_metric.py +++ b/tests/metrics/test_metric.py @@ -16,7 +16,7 @@ from tests import unittest from synapse.metrics.metric import ( - CounterMetric, CallbackMetric, CacheMetric + CounterMetric, CallbackMetric, TimerMetric, CacheMetric ) @@ -97,6 +97,40 @@ class CallbackMetricTestCase(unittest.TestCase): ]) +class TimerMetricTestCase(unittest.TestCase): + + def test_scalar(self): + metric = TimerMetric("thing") + + self.assertEquals(metric.render(), [ + "thing:count 0", + "thing:msec 0", + ]) + + metric.inc_time(500) + + self.assertEquals(metric.render(), [ + "thing:count 1", + "thing:msec 500", + ]) + + def test_vector(self): + metric = TimerMetric("queries", keys=["verb"]) + + self.assertEquals(metric.render(), []) + + metric.inc_time(300, "SELECT") + metric.inc_time(200, "SELECT") + metric.inc_time(800, "INSERT") + + self.assertEquals(metric.render(), [ + "queries{verb=INSERT}:count 1", + "queries{verb=INSERT}:msec 800", + "queries{verb=SELECT}:count 2", + "queries{verb=SELECT}:msec 500", + ]) + + class CacheMetricTestCase(unittest.TestCase): def test_cache(self):