Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements vanilla StatsD backend #20

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Goals

Markus makes it easier to add metrics generation to your app by:

* providing multiple backends (Datadog statsd, logging, and so on) for sending
data to multiple places
* providing multiple backends (Datadog statsd, statsd, logging, and so on) for
sending data to multiple places
* sending metrics to multiple backends
* providing a testing framework for easy testing
* providing a decoupled infrastructure making it easier to use metrics without
Expand Down
97 changes: 97 additions & 0 deletions markus/backends/statsd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

from __future__ import absolute_import

import logging

from statsd import StatsClient
from markus.backends import BackendBase


logger = logging.getLogger(__name__)


class StatsdMetrics(BackendBase):
"""Uses pystatsd client for statsd pings.

This requires the pystatsd module and requirements to be installed.
To install those bits, do::

$ pip install markus[statsd]


To use, add this to your backends list::

{
'class': 'markus.backends.statsd.StatsdMetrics',
'options': {
'statsd_host': 'statsd.example.com',
'statsd_port': 8125,
'statsd_prefix': None,
'statsd_maxudpsize': 512,
}
}


Options:

* statsd_host: the hostname for the statsd daemon to connect to

Defaults to ``'localhost'``.

* statsd_port: the port for the statsd daemon to connect to

Defaults to ``8125``.

* statsd_prefix: the prefix to use for statsd data

Defaults to ``None``.

* statsd_maxudpsize: the maximum data to send per packet

Defaults to ``512``.


.. seealso::

http://docs.datadoghq.com/guides/metrics/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seealso should probably be a different url so people who follow it don't get confused by Datadog statsd enhancements.

Maybe this url?: http://statsd.readthedocs.io/en/v3.2.1/configure.html


"""
def __init__(self, options):
self.host = options.get('statsd_host', 'localhost')
self.port = options.get('statsd_port', 8125)
self.prefix = options.get('statsd_prefix')
self.maxudpsize = options.get('statsd_maxudpsize', 512)

self.client = self.get_client(
self.host, self.port, self.prefix, self.maxudpsize)

logger.info(
'%s configured: %s:%s %s',
self.__class__.__name__,
self.host,
self.port,
self.prefix,
)

def get_client(self, host, port, prefix, maxudpsize):
return StatsClient(
host=host, port=port, prefix=prefix, maxudpsize=maxudpsize)

def incr(self, stat, value=1, tags=None):
"""Increment a counter"""
self.client.incr(stat=stat, count=value)

def gauge(self, stat, value, tags=None):
"""Set a gauge"""
self.client.gauge(stat=stat, value=value)

def timing(self, stat, value, tags=None):
"""Measure a timing for statistical distribution"""
self.client.timing(stat=stat, delta=value)

def histogram(self, stat, value, tags=None):
"""Measure a value for statistical distribution"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should mention that it's turning around and using the timing, too. Kind of like how I did this:

https://github.com/willkg/markus/blob/fe2d5a365beb2a4dfa650c1790332c03ee14c376/markus/backends/cloudwatch.py#L61

self.client.timing(stat=stat, delta=value)
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
-e .

datadog==0.15.0
statsd==3.2.1

freezegun==0.3.8
pytest==3.0.7
Expand Down
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@ def get_file(fn):
'six',
],
extra_requires={
'datadog': ['datadog']
'datadog': ['datadog'],
'statsd': ['statd'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be;

'statsd': ['statsd'],

},
tests_requires=['pytest'],
packages=[
Expand Down
114 changes: 114 additions & 0 deletions tests/test_statsd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

import pytest

from markus.backends import statsd


class MockStatsd(object):
def __init__(self, *args, **kwargs):
self.initargs = args
self.initkwargs = kwargs
self.calls = []

def incr(self, *args, **kwargs):
self.calls.append(
('incr', args, kwargs),
)

def gauge(self, *args, **kwargs):
self.calls.append(
('gauge', args, kwargs)
)

def timing(self, *args, **kwargs):
self.calls.append(
('timing', args, kwargs)
)


@pytest.yield_fixture
def mockstatsd():
"""Mocks Statsd class to capture method call data"""
_old_statsd = statsd.StatsClient
mock = MockStatsd
statsd.StatsClient = mock
yield
statsd.StatsClient = _old_statsd


def test_default_options(mockstatsd):
ddm = statsd.StatsdMetrics({})

assert ddm.host == 'localhost'
assert ddm.port == 8125
assert ddm.prefix == None
assert ddm.maxudpsize == 512

# NOTE: ddm.client is the mock instance
assert ddm.client.initargs == ()
assert ddm.client.initkwargs == {'host': 'localhost', 'port': 8125, 'prefix': None, 'maxudpsize': 512}


def test_options(mockstatsd):
ddm = statsd.StatsdMetrics({
'statsd_host': 'example.com',
'statsd_port': 5000,
'statsd_prefix': 'joe',
'statsd_maxudpsize': 256,
})

assert ddm.host == 'example.com'
assert ddm.port == 5000
assert ddm.prefix == 'joe'
assert ddm.maxudpsize == 256

# NOTE: ddm.client is the mock instance
assert ddm.client.initargs == ()
assert ddm.client.initkwargs == {'host': 'example.com', 'port': 5000, 'prefix': 'joe', 'maxudpsize': 256}


def test_incr(mockstatsd):
ddm = statsd.StatsdMetrics({})

ddm.incr('foo', value=10, tags=['key1:val'])

assert (
ddm.client.calls ==
[('incr', (), {'stat': 'foo', 'count': 10})]
)


def test_gauge(mockstatsd):
ddm = statsd.StatsdMetrics({})

ddm.gauge('foo', value=100, tags=['key1:val'])

assert (
ddm.client.calls ==
[('gauge', (), {'stat': 'foo', 'value': 100})]
)


def test_timing(mockstatsd):
ddm = statsd.StatsdMetrics({})

ddm.timing('foo', value=1234, tags=['key1:val'])

assert (
ddm.client.calls ==
[('timing', (), {'stat': 'foo', 'delta': 1234})]
)


def test_histogram(mockstatsd):
ddm = statsd.StatsdMetrics({})

ddm.histogram('foo', value=4321, tags=['key1:val'])

assert (
ddm.client.calls ==
[('timing', (), {'stat': 'foo', 'delta': 4321})]
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests are great!

Having said that, if you have better ideas for how to test backends in Markus, I'm all ears.