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
|
|
|
|
|
|
|
|
|
|
|
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``.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
SETTINGS_KEY = 'sprockets.mixins.metrics.statsd'
|
|
|
|
"""``self.settings`` key that configures this mix-in."""
|
|
|
|
|
|
|
|
def initialize(self):
|
|
|
|
super(StatsdMixin, self).initialize()
|
|
|
|
settings = self.settings.setdefault(self.SETTINGS_KEY, {})
|
|
|
|
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_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, milliseconds, *path):
|
|
|
|
"""
|
|
|
|
Record a timing.
|
|
|
|
|
|
|
|
:param float milliseconds: millisecond timing to record
|
|
|
|
: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.
|
|
|
|
|
|
|
|
"""
|
2016-01-19 15:43:02 +00:00
|
|
|
self._send(self._build_path(path), milliseconds, 'ms')
|
|
|
|
|
|
|
|
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:
|
|
|
|
fini = max(start, time.time())
|
|
|
|
self.record_timing((fini - start) * 1000.0, *path)
|
|
|
|
|
2016-01-19 12:51:09 +00:00
|
|
|
def on_finish(self):
|
|
|
|
"""
|
|
|
|
Records the time taken to process the request.
|
|
|
|
|
|
|
|
This method records the number of milliseconds that were used
|
|
|
|
to process the request (as reported by
|
|
|
|
:meth:`tornado.web.HTTPRequest.request_time` * 1000) under the
|
|
|
|
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() * 1000,
|
|
|
|
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."""
|
|
|
|
return '{}.{}'.format(self.settings[self.SETTINGS_KEY]['namespace'],
|
|
|
|
'.'.join(str(p).replace('.', '-') for p in path))
|
|
|
|
|
|
|
|
def _send(self, path, value, stat_type):
|
|
|
|
"""Send a metric to Statsd."""
|
|
|
|
settings = self.settings[self.SETTINGS_KEY]
|
|
|
|
msg = '{0}:{1}|{2}'.format(path, value, stat_type)
|
|
|
|
settings['socket'].sendto(msg.encode('ascii'),
|
|
|
|
(settings['host'], int(settings['port'])))
|