mirror of
https://github.com/sprockets/sprockets.mixins.metrics.git
synced 2024-11-21 19:28:34 +00:00
Implement timer metric in StatsdMixin.
This commit is contained in:
parent
b1b63644be
commit
a125c4ddeb
10 changed files with 218 additions and 0 deletions
22
docs/api.rst
Normal file
22
docs/api.rst
Normal file
|
@ -0,0 +1,22 @@
|
|||
Reference Documentation
|
||||
=======================
|
||||
This library defines mix-ins that record application metrics. Each mix-in
|
||||
implements the same interface:
|
||||
|
||||
.. class:: sprockets.mixins.metrics.Mixin
|
||||
|
||||
.. data:: SETTINGS_KEY
|
||||
|
||||
Key in ``self.application.settings`` that contains this particular
|
||||
mix-in's configuration data.
|
||||
|
||||
.. method:: record_timing(path, milliseconds)
|
||||
|
||||
:param str path: timing path to record
|
||||
:param float milliseconds: number of milliseconds to record
|
||||
|
||||
|
||||
Statsd Implementation
|
||||
---------------------
|
||||
.. autoclass:: sprockets.mixins.metrics.StatsdMixin
|
||||
:members:
|
|
@ -5,5 +5,6 @@ Release History
|
|||
|
||||
`Next Release`_
|
||||
---------------
|
||||
- Add :class:`sprockets.mixins.metrics.StatsdMixin`
|
||||
|
||||
.. _Next Release: https://github.com/sprockets/sprockets.mixins.metrics/compare/0.0.0...master
|
||||
|
|
|
@ -7,6 +7,7 @@ License
|
|||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
api
|
||||
examples
|
||||
contributing
|
||||
history
|
||||
|
|
0
examples/__init__.py
Normal file
0
examples/__init__.py
Normal file
|
@ -1 +1,3 @@
|
|||
-r testing.txt
|
||||
coverage>=3.7,<4.1
|
||||
Sphinx
|
||||
|
|
2
requires/testing.txt
Normal file
2
requires/testing.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
nose>=1.3,<2
|
||||
tornado>=4.2,<4.3
|
3
setup.cfg
Normal file
3
setup.cfg
Normal file
|
@ -0,0 +1,3 @@
|
|||
[nosetests]
|
||||
cover-package = sprockets.mixins.metrics
|
||||
cover-branches = 1
|
|
@ -1,2 +1,5 @@
|
|||
from .statsd import StatsdMixin
|
||||
|
||||
version_info = (0, 0, 0)
|
||||
__version__ = '.'.join(str(v) for v in version_info)
|
||||
__all__ = ['StatsdMixin']
|
||||
|
|
82
sprockets/mixins/metrics/statsd.py
Normal file
82
sprockets/mixins/metrics/statsd.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
import socket
|
||||
|
||||
|
||||
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.
|
||||
|
||||
"""
|
||||
settings = self.settings[self.SETTINGS_KEY]
|
||||
normalized = '.'.join(str(p).replace('.', '-') for p in path)
|
||||
msg = '{0}.{1}:{2}|ms'.format(settings['namespace'], normalized,
|
||||
milliseconds)
|
||||
settings['socket'].sendto(msg.encode('ascii'),
|
||||
(settings['host'], int(settings['port'])))
|
||||
|
||||
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)
|
102
tests.py
102
tests.py
|
@ -0,0 +1,102 @@
|
|||
import socket
|
||||
|
||||
from tornado import testing, web
|
||||
|
||||
from sprockets.mixins import metrics
|
||||
import examples.statsd
|
||||
|
||||
|
||||
class FakeStatsdServer(object):
|
||||
"""
|
||||
Implements something resembling a statsd server.
|
||||
|
||||
Received datagrams are saved off in the ``datagrams`` attribute
|
||||
for later examination.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, iol):
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM,
|
||||
socket.IPPROTO_UDP)
|
||||
self.socket.bind(('127.0.0.1', 0))
|
||||
self.sockaddr = self.socket.getsockname()
|
||||
self.datagrams = []
|
||||
|
||||
iol.add_handler(self.socket, self._handle_events, iol.READ)
|
||||
self._iol = iol
|
||||
|
||||
def close(self):
|
||||
if self.socket is not None:
|
||||
if self._iol is not None:
|
||||
self._iol.remove_handler(self.socket)
|
||||
self._iol = None
|
||||
self.socket.close()
|
||||
self.socket = None
|
||||
|
||||
def _handle_events(self, fd, events):
|
||||
if fd != self.socket:
|
||||
return
|
||||
if self._iol is None:
|
||||
raise RuntimeError
|
||||
|
||||
if events & self._iol.READ:
|
||||
data, _ = self.socket.recvfrom(4096)
|
||||
self.datagrams.append(data)
|
||||
|
||||
|
||||
class StatsdMethodTimingTests(testing.AsyncHTTPTestCase):
|
||||
|
||||
def get_app(self):
|
||||
self.application = web.Application([
|
||||
web.url('/', examples.statsd.SimpleHandler),
|
||||
])
|
||||
return self.application
|
||||
|
||||
def setUp(self):
|
||||
self.application = None
|
||||
super(StatsdMethodTimingTests, self).setUp()
|
||||
self.statsd = FakeStatsdServer(self.io_loop)
|
||||
self.application.settings[metrics.StatsdMixin.SETTINGS_KEY] = {
|
||||
'host': self.statsd.sockaddr[0],
|
||||
'port': self.statsd.sockaddr[1],
|
||||
'namespace': 'testing',
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
self.statsd.close()
|
||||
super(StatsdMethodTimingTests, self).tearDown()
|
||||
|
||||
@property
|
||||
def settings(self):
|
||||
return self.application.settings[metrics.StatsdMixin.SETTINGS_KEY]
|
||||
|
||||
def test_that_http_method_call_is_recorded(self):
|
||||
response = self.fetch('/')
|
||||
self.assertEqual(response.code, 204)
|
||||
|
||||
expected = 'testing.SimpleHandler.GET.204:'
|
||||
for bin_msg in self.statsd.datagrams:
|
||||
text_msg = bin_msg.decode('ascii')
|
||||
if text_msg.startswith(expected):
|
||||
_, _, tail = text_msg.partition(':')
|
||||
measurement, _, measurement_type = tail.partition('|')
|
||||
self.assertTrue(250.0 <= float(measurement) < 500.0,
|
||||
'{} looks wrong'.format(measurement))
|
||||
self.assertTrue(measurement_type, 'ms')
|
||||
break
|
||||
else:
|
||||
self.fail('Expected metric starting with {} in {!r}'.format(
|
||||
expected, self.statsd.datagrams))
|
||||
|
||||
def test_that_cached_socket_is_used(self):
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0)
|
||||
self.settings['socket'] = sock
|
||||
self.fetch('/')
|
||||
self.assertIs(self.settings['socket'], sock)
|
||||
|
||||
def test_that_default_prefix_is_stored(self):
|
||||
del self.settings['namespace']
|
||||
self.fetch('/')
|
||||
self.assertEqual(
|
||||
self.settings['namespace'],
|
||||
'applications.' + examples.statsd.SimpleHandler.__module__)
|
Loading…
Reference in a new issue