diff --git a/synapse/metrics/metric.py b/synapse/metrics/metric.py
index 4df5ebfda6..7175881941 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 b7facb8587..b25520821d 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):
|