sprockets.mixins.metrics/sprockets/mixins/metrics/statsd.py

129 lines
4.6 KiB
Python
Raw Normal View History

2016-01-19 16:02:04 +00:00
import contextlib
2016-01-19 12:51:09 +00:00
import socket
2016-01-19 16:02:04 +00:00
import time
2016-01-19 12:51:09 +00:00
2016-03-10 20:45:50 +00:00
SETTINGS_KEY = 'sprockets.mixins.metrics.statsd'
"""``self.settings`` key that configures this mix-in."""
2016-01-19 12:51:09 +00:00
class StatsdMixin(object):
"""
Mix this class in to record metrics to a Statsd server.
**Configuration**
:namespace:
Path to prefix metrics with. If undefined, this defaults to
``applications`` + ``self.__class__.__module__``
:host:
Host name of the StatsD server to send metrics to. If undefined,
this defaults to ``127.0.0.1``.
:port:
Port number that the StatsD server is listening on. If undefined,
this defaults to ``8125``.
"""
def initialize(self):
super(StatsdMixin, self).initialize()
2016-03-10 20:45:50 +00:00
settings = self.settings.setdefault(SETTINGS_KEY, {})
2016-01-19 12:51:09 +00:00
if 'socket' not in settings:
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
settings['socket'] = sock
if 'namespace' not in settings:
settings['namespace'] = 'applications.{}'.format(
self.__class__.__module__)
settings.setdefault('host', '127.0.0.1')
settings.setdefault('port', '8125')
self.__status_code = None
def set_metric_tag(self, tag, value):
"""Ignored for statsd since it does not support tagging."""
2016-01-19 12:51:09 +00:00
def set_status(self, status_code, reason=None):
# Extended to track status code to avoid referencing the
# _status internal variable
self.__status_code = status_code
super(StatsdMixin, self).set_status(status_code, reason=reason)
def record_timing(self, duration, *path):
2016-01-19 12:51:09 +00:00
"""
Record a timing.
:param float duration: timing to record in seconds
2016-01-19 12:51:09 +00:00
:param path: elements of the metric path to record
This method records a timing to the application's namespace
followed by a calculated path. Each element of `path` is
converted to a string and normalized before joining the
elements by periods. The normalization process is little
more than replacing periods with dashes.
"""
self._send(self._build_path(path), duration * 1000.0, 'ms')
2016-01-19 15:43:02 +00:00
def increase_counter(self, *path, **kwargs):
"""
Increase a counter.
:param path: elements of the metric path to incr
:keyword int amount: amount to increase the counter by. If
omitted, the counter is increased by one.
This method increases a counter within the application's
namespace. Each element of `path` is converted to a string
and normalized before joining the elements by periods. The
normalization process is little more than replacing periods
with dashes.
"""
self._send(self._build_path(path), kwargs.get('amount', '1'), 'c')
2016-01-19 12:51:09 +00:00
2016-01-19 16:02:04 +00:00
@contextlib.contextmanager
def execution_timer(self, *path):
"""
Record the time it takes to perform an arbitrary code block.
:param path: elements of the metric path to record
This method returns a context manager that records the amount
of time spent inside of the context and submits a timing metric
to the specified `path` using (:meth:`record_timing`).
"""
start = time.time()
try:
yield
finally:
2016-03-10 20:45:50 +00:00
self.record_timing(max(start, time.time()) - start, *path)
2016-01-19 16:02:04 +00:00
2016-01-19 12:51:09 +00:00
def on_finish(self):
"""
Records the time taken to process the request.
This method records the amount of time taken to process the request
(as reported by
:meth:`~tornado.httputil.HTTPServerRequest.request_time`) under the
2016-01-19 12:51:09 +00:00
path defined by the class's module, it's name, the request method,
and the status code. The :meth:`.record_timing` method is used
to send the metric, so the configured namespace is used as well.
"""
super(StatsdMixin, self).on_finish()
self.record_timing(self.request.request_time(),
2016-01-19 12:51:09 +00:00
self.__class__.__name__, self.request.method,
self.__status_code)
2016-01-19 15:43:02 +00:00
def _build_path(self, path):
"""Return a normalized path."""
2016-03-10 20:45:50 +00:00
return '{}.{}'.format(self.settings[SETTINGS_KEY]['namespace'],
2016-01-19 15:43:02 +00:00
'.'.join(str(p).replace('.', '-') for p in path))
def _send(self, path, value, stat_type):
"""Send a metric to Statsd."""
2016-03-10 20:45:50 +00:00
settings = self.settings[SETTINGS_KEY]
2016-01-19 15:43:02 +00:00
msg = '{0}:{1}|{2}'.format(path, value, stat_type)
settings['socket'].sendto(msg.encode('ascii'),
(settings['host'], int(settings['port'])))