1
0

metric.py 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. # -*- coding: utf-8 -*-
  2. # Copyright 2015 OpenMarket Ltd
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License");
  5. # you may not use this file except in compliance with the License.
  6. # You may obtain a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS,
  12. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. # See the License for the specific language governing permissions and
  14. # limitations under the License.
  15. from itertools import chain
  16. # TODO(paul): I can't believe Python doesn't have one of these
  17. def map_concat(func, items):
  18. # flatten a list-of-lists
  19. return list(chain.from_iterable(map(func, items)))
  20. class BaseMetric(object):
  21. def __init__(self, name, labels=[]):
  22. self.name = name
  23. self.labels = labels # OK not to clone as we never write it
  24. def dimension(self):
  25. return len(self.labels)
  26. def is_scalar(self):
  27. return not len(self.labels)
  28. def _render_labelvalue(self, value):
  29. # TODO: some kind of value escape
  30. return '"%s"' % (value)
  31. def _render_key(self, values):
  32. if self.is_scalar():
  33. return ""
  34. return "{%s}" % (
  35. ",".join(["%s=%s" % (k, self._render_labelvalue(v))
  36. for k, v in zip(self.labels, values)])
  37. )
  38. def render(self):
  39. return map_concat(self.render_item, sorted(self.counts.keys()))
  40. class CounterMetric(BaseMetric):
  41. """The simplest kind of metric; one that stores a monotonically-increasing
  42. integer that counts events."""
  43. def __init__(self, *args, **kwargs):
  44. super(CounterMetric, self).__init__(*args, **kwargs)
  45. self.counts = {}
  46. # Scalar metrics are never empty
  47. if self.is_scalar():
  48. self.counts[()] = 0
  49. def inc_by(self, incr, *values):
  50. if len(values) != self.dimension():
  51. raise ValueError("Expected as many values to inc() as labels (%d)" %
  52. (self.dimension())
  53. )
  54. # TODO: should assert that the tag values are all strings
  55. if values not in self.counts:
  56. self.counts[values] = incr
  57. else:
  58. self.counts[values] += incr
  59. def inc(self, *values):
  60. self.inc_by(1, *values)
  61. def render_item(self, k):
  62. return ["%s%s %d" % (self.name, self._render_key(k), self.counts[k])]
  63. class CallbackMetric(BaseMetric):
  64. """A metric that returns the numeric value returned by a callback whenever
  65. it is rendered. Typically this is used to implement gauges that yield the
  66. size or other state of some in-memory object by actively querying it."""
  67. def __init__(self, name, callback, labels=[]):
  68. super(CallbackMetric, self).__init__(name, labels=labels)
  69. self.callback = callback
  70. def render(self):
  71. value = self.callback()
  72. if self.is_scalar():
  73. return ["%s %d" % (self.name, value)]
  74. return ["%s%s %d" % (self.name, self._render_key(k), value[k])
  75. for k in sorted(value.keys())]
  76. class DistributionMetric(object):
  77. """A combination of an event counter and an accumulator, which counts
  78. both the number of events and accumulates the total value. Typically this
  79. could be used to keep track of method-running times, or other distributions
  80. of values that occur in discrete occurances.
  81. TODO(paul): Try to export some heatmap-style stats?
  82. """
  83. def __init__(self, name, *args, **kwargs):
  84. self.counts = CounterMetric(name + ":count", **kwargs)
  85. self.totals = CounterMetric(name + ":total", **kwargs)
  86. def inc_by(self, inc, *values):
  87. self.counts.inc(*values)
  88. self.totals.inc_by(inc, *values)
  89. def render(self):
  90. return self.counts.render() + self.totals.render()
  91. class CacheMetric(object):
  92. """A combination of two CounterMetrics, one to count cache hits and one to
  93. count a total, and a callback metric to yield the current size.
  94. This metric generates standard metric name pairs, so that monitoring rules
  95. can easily be applied to measure hit ratio."""
  96. def __init__(self, name, size_callback, labels=[]):
  97. self.name = name
  98. self.hits = CounterMetric(name + ":hits", labels=labels)
  99. self.total = CounterMetric(name + ":total", labels=labels)
  100. self.size = CallbackMetric(name + ":size",
  101. callback=size_callback,
  102. labels=labels,
  103. )
  104. def inc_hits(self, *values):
  105. self.hits.inc(*values)
  106. self.total.inc(*values)
  107. def inc_misses(self, *values):
  108. self.total.inc(*values)
  109. def render(self):
  110. return self.hits.render() + self.total.render() + self.size.render()