summary refs log tree commit diff
path: root/synapse/metrics/metric.py
blob: 17cd673891736128fae739b66ab1dfbf22b1bbcb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# -*- coding: utf-8 -*-
# Copyright 2015 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# 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, labels=[]):
        self.name = name
        self.labels = labels # OK not to clone as we never write it

    def dimension(self):
        return len(self.labels)

    def is_scalar(self):
        return not len(self.labels)

    def _render_key(self, values):
        if self.is_scalar():
            return ""
        # TODO: some kind of value escape
        return "{%s}" % (
            ",".join(["%s=%s" % kv for kv in zip(self.labels, values)])
        )

    def render(self):
        return map_concat(self.render_item, sorted(self.counts.keys()))


class CounterMetric(BaseMetric):
    """The simplest kind of metric; one that stores a monotonically-increasing
    integer that counts events."""

    def __init__(self, *args, **kwargs):
        super(CounterMetric, self).__init__(*args, **kwargs)

        self.counts = {}

        # Scalar metrics are never empty
        if self.is_scalar():
            self.counts[()] = 0

    def inc(self, *values):
        if len(values) != self.dimension():
            raise ValueError("Expected as many values to inc() as labels (%d)" %
                (self.dimension())
            )

        # TODO: should assert that the tag values are all strings

        if values not in self.counts:
            self.counts[values] = 1
        else:
            self.counts[values] += 1

    def fetch(self):
        return dict(self.counts)

    def render_item(self, k):
        return ["%s%s %d" % (self.name, self._render_key(k), self.counts[k])]


class CallbackMetric(BaseMetric):
    """A metric that returns the numeric value returned by a callback whenever
    it is rendered. Typically this is used to implement gauges that yield the
    size or other state of some in-memory object by actively querying it."""

    def __init__(self, name, callback, labels=[]):
        super(CallbackMetric, self).__init__(name, labels=labels)

        self.callback = callback

    def render(self):
        value = self.callback()

        if self.is_scalar():
            return ["%s %d" % (self.name, value)]

        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_item(self, k):
        keystr = self._render_key(k)

        return ["%s:count%s %d" % (self.name, keystr, self.counts[k]),
                "%s:msec%s %d" % (self.name, keystr, self.times[k])]


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.

    This metric generates standard metric name pairs, so that monitoring rules
    can easily be applied to measure hit ratio."""

    def __init__(self, name, size_callback, labels=[]):
        self.name = name

        self.hits   = CounterMetric(name + ":hits",   labels=labels)
        self.misses = CounterMetric(name + ":misses", labels=labels)

        self.size = CallbackMetric(name + ":size",
            callback=size_callback,
            labels=labels,
        )

    def inc_hits(self, *values):
        self.hits.inc(*values)

    def inc_misses(self, *values):
        self.misses.inc(*values)

    def render(self):
        return self.hits.render() + self.misses.render() + self.size.render()