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`_
|
`Next Release`_
|
||||||
---------------
|
---------------
|
||||||
|
- Add :class:`sprockets.mixins.metrics.StatsdMixin`
|
||||||
|
|
||||||
.. _Next Release: https://github.com/sprockets/sprockets.mixins.metrics/compare/0.0.0...master
|
.. _Next Release: https://github.com/sprockets/sprockets.mixins.metrics/compare/0.0.0...master
|
||||||
|
|
|
@ -7,6 +7,7 @@ License
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:hidden:
|
:hidden:
|
||||||
|
|
||||||
|
api
|
||||||
examples
|
examples
|
||||||
contributing
|
contributing
|
||||||
history
|
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
|
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_info = (0, 0, 0)
|
||||||
__version__ = '.'.join(str(v) for v in version_info)
|
__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