Skip to content

Commit

Permalink
Added separator to middleware constructor.
Browse files Browse the repository at this point in the history
  • Loading branch information
wlansu committed Jan 22, 2015
1 parent dcdba1f commit 5e3d8cc
Show file tree
Hide file tree
Showing 4 changed files with 39 additions and 18 deletions.
5 changes: 5 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
Changelog
=========

0.2.4
-----

* Add separator parameter to StatsdTimingMiddleware.__init__().

0.2.3
-----

Expand Down
23 changes: 16 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ In your wsgi.py file wrap your WSGI application as follows:
.. note::

If an unhandled exception happens, it will not be timed by default.
This is the design decision to separate error reporting and actual statistical measurements.
This is a design decision to separate error reporting and actual statistical measurements.
To enable exception timing, pass `time_exception=True` to the middleware constructor:


Expand All @@ -68,16 +68,25 @@ What it does
The middleware uses the statsd timer function, using the environ['PATH_INFO'], environ['REQUEST_METHOD'] and
the status code variables as the name for the key and the amount of time the request took as the value.

That's it.

If you want more granular reporting you'll have to work with the ``prefix`` argument. You can pass any string you want
and the middleware will pass it along to statsd.

Using the ``foo`` prefix and calling the ``www.spam.com/bar`` page will result in ``foo.bar.GET.200`` having a value
Using the ``foo`` prefix and calling the ``www.spam.com/bar`` page will result in ``foo_bar_GET_200`` having a value
equal to the time it took to handle the request.

If you passed `time_exceptions=True` and exception happened during the response, then the key name will be postfixed
with the exception class name: ``foo.bar.GET.500.ValueError``
with the exception class name: ``foo_bar_GET_500_ValueError``

.. note::

wsgi-statsd uses underscores as a separator for the key that is sent to statsd as that makes it easy to retrieve the
data from graphite. You can override this default by passing a ``separator`` value to the middleware constructor:


.. code-block:: python
StatsdTimingMiddleware(application, client, separator='.')
Customizing for your needs
Expand Down Expand Up @@ -108,9 +117,9 @@ the `GitHub project page <http://github.com/paylogic/wsgi-statsd>`_.
License
-------

This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_
This software is licensed under the `MIT license <http://en.wikipedia.org/wiki/MIT_License>`_.

Please refer to the `license file <https://github.com/paylogic/wsgi-statsd/blob/master/LICENSE.txt>`_
Please refer to the `license file <https://github.com/paylogic/wsgi-statsd/blob/master/LICENSE.txt>`_.


© 2015 Wouter Lansu, Paylogic International and others.
12 changes: 6 additions & 6 deletions tests/test_timer.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@ def raising_application(environ, start_response):


@mock.patch('statsd.StatsClient')
def test_timer1(mock_client):
def test_timer(mock_client):
"""Test the timer functionality.
Check the following:
- timer.stop() is called
- timer.ms is not None
- the key is generated as expected, i.e. PATH_INFO.REQUEST_METHOD.RESPONSE_CODE.
- the key is generated as expected, i.e. PATH_INFO_REQUEST_METHOD_RESPONSE_CODE.
"""
with mock.patch.object(mock_client, 'timer', autospec=True) as mock_timer:
timed_app = StatsdTimingMiddleware(application, mock_client)
app = TestApp(timed_app)
app.get('/test')
app.get('/test/some/thing.ext?param=one&two=3')

assert mock_timer.return_value.stop.called
assert mock_timer.return_value.ms is not None
assert mock_timer.call_args[0] == ('test.GET.200',)
assert mock_timer.call_args[0] == ('test_some_thing_ext_GET_200',)


@mock.patch('statsd.StatsClient')
Expand Down Expand Up @@ -82,7 +82,7 @@ def test_exception_response(mock_client, mock_close, time_exceptions):
assert mock_timer.return_value.stop.called == time_exceptions
assert not mock_close.called
if time_exceptions:
assert mock_timer.call_args[0] == ('test.GET.200.Exception',)
assert mock_timer.call_args[0] == ('test_GET_200_Exception',)


@pytest.mark.parametrize('time_exceptions', [False, True])
Expand All @@ -109,4 +109,4 @@ def response_next(self):
assert mock_close.called
assert mock_close.next_called
if time_exceptions:
assert mock_timer.call_args[0] == ('test.GET.200.Exception',)
assert mock_timer.call_args[0] == ('test_GET_200_Exception',)
17 changes: 12 additions & 5 deletions wsgi_statsd/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
"""StatsdTimingMiddleware object."""
import time

__version__ = '0.2.3'
__version__ = '0.2.4'

import re

CHAR_RE = re.compile(r'[^\w]')


class StatsdTimingMiddleware(object):

"""The Statsd timing middleware."""

def __init__(self, app, client, time_exceptions=False):
def __init__(self, app, client, time_exceptions=False, separator='_'):
"""Initialize the middleware with an application and a Statsd client.
:param app: The application object.
:param client: `statsd.StatsClient` object.
:param time_exceptions: send stats when exception happens or not, `False` by default.
:type time_exceptions: bool
:param separator: separator of the parts of key sent to statsd, defaults to '_'
:type separator: str
"""
self.app = app
self.statsd_client = client
self.time_exceptions = time_exceptions
self.separator = separator

def __call__(self, environ, start_response):
"""Call the application and time it.
Expand Down Expand Up @@ -61,17 +68,17 @@ def get_key_name(self, environ, response_interception, exception=None):
:param exception: optional exception happened during the iteration of the response
:type exception: Exception
:return: string in form 'DOTTED_PATH.METHOD.STATUS_CODE'
:return: string in form '{{PATH}}_{{METHOD}}_{{STATUS_CODE}}'
:rtype: str
"""
status = response_interception.get('status')
status_code = status.split()[0] # Leave only the status code.
# PATH_INFO can be empty, so falling back to '/' in that case
path = (environ['PATH_INFO'] or '/').replace('/', '.')[1:]
path = CHAR_RE.sub(self.separator, (environ['PATH_INFO'] or '/')[1:])
parts = [path, environ['REQUEST_METHOD'], status_code]
if exception:
parts.append(exception.__class__.__name__)
return '.'.join(parts)
return self.separator.join(parts)

def send_stats(self, start, environ, response_interception, exception=None):
"""Send the actual timing stats.
Expand Down

0 comments on commit 5e3d8cc

Please sign in to comment.