summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Socol <me@jamessocol.com>2014-01-03 01:00:10 -0500
committerJames Socol <me@jamessocol.com>2014-01-03 01:08:08 -0500
commit26302de5a9a2993ccc5a29953fbba4eb3a20d5fe (patch)
tree01a26f42fe7ab3483bebce4db0dbee50ba80ed3e
parentd2b477620661a6029a773a1a351cedff81cae300 (diff)
downloadpystatsd-26302de5a9a2993ccc5a29953fbba4eb3a20d5fe.tar.gz
Give timers manual start/stop methods.
-rw-r--r--docs/timing.rst52
-rw-r--r--statsd/__init__.py2
-rw-r--r--statsd/client.py25
-rw-r--r--statsd/tests.py47
4 files changed, 115 insertions, 11 deletions
diff --git a/docs/timing.rst b/docs/timing.rst
index 025647b..133da91 100644
--- a/docs/timing.rst
+++ b/docs/timing.rst
@@ -8,6 +8,8 @@ Using Timers
application performance. Statsd provides a number of ways to use them to
instrument your code.
+There are four ways to use timers.
+
Calling ``timing`` manually
===========================
@@ -67,3 +69,53 @@ be sent to the statsd server.
# Timing information will be sent every time the function is called.
myfunc(1, 2)
myfunc(3, 7)
+
+
+Using a Timer directly
+======================
+
+:py:class:`statsd.client.Timer` objects function as context managers and
+as decorators, but they can also be used directly. (Flat is, after all,
+better than nested.)
+
+::
+
+ from statsd import StatsClient
+
+ statsd = StatsClient()
+
+ foo_timer = statsd.timer('foo')
+ foo_timer.start()
+ # Do something fun.
+ foo_timer.stop()
+
+When :py:meth:`statsd.client.Timer.stop` is called, a `timing stat
+<timer-type>`_ will automatically be sent to StatsD. You can over ride
+this behavior with the ``send=False`` keyword argument to ``stop()``::
+
+ foo_timer.stop(send=False)
+ foo_timer.send()
+
+Use :py:meth:`statsd.client.Timer.send` to send the stat when you're
+ready.
+
+.. note::
+ This use of timers is compatible with `Pipelines <pipeline-chapter>`_
+ but be careful with the ``send()`` method. It *must* be called for
+ the stat to be included when the Pipeline finally sends data, but
+ ``send()`` will *not* immediately cause data to be sent in the
+ context of a Pipeline. For example::
+
+ with statsd.pipeline() as pipe:
+ foo_timer = pipe.timer('foo').start()
+ # Do something...
+ pipe.incr('bar')
+ foo_timer.stop() # Will be sent when the managed block exits.
+
+ with statsd.pipeline() as pipe:
+ foo_timer = pipe.timer('foo').start()
+ # Do something...
+ pipe.incr('bar')
+ foo_timer.stop(send=False) # Will not be sent.
+ foo_timer.send() # Will be sent when the managed block exits.
+ # Do something else...
diff --git a/statsd/__init__.py b/statsd/__init__.py
index e47a620..6af74c5 100644
--- a/statsd/__init__.py
+++ b/statsd/__init__.py
@@ -16,7 +16,7 @@ except ImportError:
settings = None
from .client import StatsClient
-from ._version import __version__
+from ._version import __version__ # noqa
__all__ = ['StatsClient', 'statsd']
diff --git a/statsd/client.py b/statsd/client.py
index 78c3a99..0770247 100644
--- a/statsd/client.py
+++ b/statsd/client.py
@@ -25,19 +25,31 @@ class Timer(object):
return wrapper
def __enter__(self):
- self.start = time.time()
- return self
+ return self.start()
def __exit__(self, typ, value, tb):
- dt = time.time() - self.start
- self.ms = int(round(1000 * dt)) # Convert to ms.
+ self.stop()
+
+ def start(self):
+ self._start_time = time.time()
+ return self
+
+ def stop(self, send=True):
+ dt = time.time() - self._start_time
+ self.ms = int(round(1000 * dt)) # Convert to milliseconds.
+ if send:
+ self.send()
+ return self
+
+ def send(self):
self.client.timing(self.stat, self.ms, self.rate)
class StatsClient(object):
"""A client for statsd."""
- def __init__(self, host='localhost', port=8125, prefix=None, maxudpsize=512):
+ def __init__(self, host='localhost', port=8125, prefix=None,
+ maxudpsize=512):
"""Create a new client."""
self._addr = (socket.gethostbyname(host), port)
self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
@@ -106,7 +118,8 @@ class Pipeline(StatsClient):
self._stats = []
def _after(self, data):
- self._stats.append(data)
+ if data is not None:
+ self._stats.append(data)
def __enter__(self):
return self
diff --git a/statsd/tests.py b/statsd/tests.py
index 75bee3d..6c7d7ad 100644
--- a/statsd/tests.py
+++ b/statsd/tests.py
@@ -18,13 +18,11 @@ def _client(prefix=None):
return sc
-def _sock_check(cl, count, val):
+def _sock_check(cl, count, val=None):
eq_(cl._sock.sendto.call_count, count)
- if val:
+ if val is not None:
val = val.encode('ascii')
eq_(cl._sock.sendto.call_args, ((val, ADDR), {}))
- else:
- eq_(cl._sock.sendto.call_args, None)
class assert_raises(object):
@@ -288,6 +286,37 @@ def test_timer_decorator_exceptions():
_timer_check(sc, 1, 'foo', 'ms')
+def test_imperative_timer():
+ sc = _client()
+
+ t = sc.timer('foo').start()
+ t.stop()
+
+ _timer_check(sc, 1, 'foo', 'ms')
+
+
+def test_imperative_timer_no_send():
+ sc = _client()
+
+ t = sc.timer('foo').start()
+ t.stop(send=False)
+ _sock_check(sc, 0)
+
+ t.send()
+ _timer_check(sc, 1, 'foo', 'ms')
+
+
+@mock.patch.object(random, 'random', lambda: -1)
+def test_imperative_timer_rate():
+ sc = _client()
+
+ t = sc.timer('foo', rate=0.5)
+ t.start()
+ t.stop()
+
+ _timer_check(sc, 1, 'foo', 'ms@0.5')
+
+
def test_pipeline():
sc = _client()
pipe = sc.pipeline()
@@ -303,6 +332,7 @@ def test_pipeline_null():
sc = _client()
pipe = sc.pipeline()
pipe.send()
+ _sock_check(sc, 0)
def test_pipeline_manager():
@@ -332,6 +362,15 @@ def test_pipeline_timer_decorator():
_timer_check(sc, 1, 'foo', 'ms')
+def test_pipeline_timer_imperative():
+ sc = _client()
+ with sc.pipeline() as pipe:
+ t = pipe.timer('foo').start()
+ t.stop()
+ _sock_check(sc, 0)
+ _timer_check(sc, 1, 'foo', 'ms')
+
+
def test_pipeline_empty():
"""Pipelines should be empty after a send() call."""
sc = _client()