From c26bc1aeb4d6c56a7dfe157bf5aa83b4f80f9922 Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Thu, 24 Jan 2019 10:28:25 +0000 Subject: [PATCH 1/4] Add a context manager and decorator for timing Include flake8-isort & assorted tidying. --- setup.py | 1 - telegraf/__init__.py | 4 +-- telegraf/client.py | 66 ++++++++++++++++++++++++++++++++++++- telegraf/context.py | 20 +++++++++++ telegraf/defaults/django.py | 3 +- telegraf/tests.py | 7 ++-- telegraf/utils.py | 7 ++++ 7 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 telegraf/context.py diff --git a/setup.py b/setup.py index 7521fb8..caf80cf 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,6 @@ from setuptools import find_packages, setup - here = os.path.abspath(os.path.dirname(__file__)) about = {} diff --git a/telegraf/__init__.py b/telegraf/__init__.py index 692dc94..1c9dd60 100644 --- a/telegraf/__init__.py +++ b/telegraf/__init__.py @@ -1,6 +1,6 @@ from __future__ import absolute_import -from .__version__ import __title__, __description__, __version__ # noqa -from .client import TelegrafClient, HttpClient +from .__version__ import __description__, __title__, __version__ # noqa +from .client import HttpClient, TelegrafClient __all__ = ('TelegrafClient', 'HttpClient') diff --git a/telegraf/client.py b/telegraf/client.py index 8a6ba2c..1b27592 100644 --- a/telegraf/client.py +++ b/telegraf/client.py @@ -1,6 +1,20 @@ +import socket from abc import abstractmethod +from functools import wraps +from time import time + from telegraf.protocol import Line -import socket +from telegraf.utils import is_higher_py35 + +if is_higher_py35(): + from telegraf.context import wrapped_coroutine + from asyncio import iscoroutinefunction +else: + def wrapped_coroutine(self, func): + raise NotImplementedError(u"Async timer decorator requires Python 3.5 or higher.") + + def iscoroutinefunction(*args, **kwargs): + return False class ClientBase(object): @@ -28,6 +42,9 @@ def metric(self, measurement_name, values, tags=None, timestamp=None): line = Line(measurement_name, values, all_tags, timestamp) self.send(line.to_line_protocol()) + def timer(self, measurement_name, tags=None, use_ms=False): + return TimerHelper(self, measurement_name=measurement_name, tags=tags, use_ms=use_ms) + @abstractmethod def send(self, data): pass @@ -77,3 +94,50 @@ def send(self, data): this issues the request in the background. """ self.future_session.post(url=self.url, data=data) + + +class TimerHelper(object): + def __init__(self, client, measurement_name=None, tags=None, use_ms=False): + self.client = client + self.measurement_name = measurement_name + self.tags = tags or {} + self.use_ms = use_ms + + def __call__(self, func): + """Decorator helper for timing function calls.""" + if not self.measurement_name: + self.measurement_name = '%s.%s' % (func.__module__, func.__name__) + + # Coroutines + if iscoroutinefunction(func): + return wrapped_coroutine(self, func) + + @wraps(func) + def wrapped(*args, **kwargs): + start = time() + try: + return func(*args, **kwargs) + finally: + self._send(start) + return wrapped + + def __enter__(self): + if not self.measurement_name: + raise TypeError("No metric name specified.") + self.start_time = time() + return self + + def __exit__(self, type, value, traceback): + # Report the elapsed time of the context manager. + self._send(self.start_time) + + def _send(self, start_time): + elapsed = time() - start_time + tags = self.tags.copy() + if self.use_ms: + elapsed = int(round(1000 * elapsed)) + tags['units'] = 'ms' + else: + tags['units'] = 's' + line = Line(self.measurement_name, elapsed, tags, None) + self.client.send(line.to_line_protocol()) diff --git a/telegraf/context.py b/telegraf/context.py new file mode 100644 index 0000000..5ea734d --- /dev/null +++ b/telegraf/context.py @@ -0,0 +1,20 @@ +""" +Decorator for async methods. + +Requires Python 3.5 or higher. +""" +# stdlib +from functools import wraps +from time import time + + +def wrapped_coroutine(self, func): + """Timing wrapper for async functions.""" + @wraps(func) + async def wrapped_co(*args, **kwargs): + start = time() + try: + return await func(*args, **kwargs) + finally: + self._send(start) + return wrapped_co diff --git a/telegraf/defaults/django.py b/telegraf/defaults/django.py index 905fc97..320fcdc 100644 --- a/telegraf/defaults/django.py +++ b/telegraf/defaults/django.py @@ -1,10 +1,9 @@ from __future__ import absolute_import -from django.conf import settings +from django.conf import settings from telegraf import defaults from telegraf.client import TelegrafClient - telegraf = None if telegraf is None: diff --git a/telegraf/tests.py b/telegraf/tests.py index b0c21d7..c9b5f0e 100644 --- a/telegraf/tests.py +++ b/telegraf/tests.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -from telegraf.client import ClientBase, TelegrafClient, HttpClient -from telegraf.protocol import Line -from telegraf.utils import format_string, format_value import unittest + import mock +from telegraf.client import ClientBase, HttpClient, TelegrafClient +from telegraf.protocol import Line +from telegraf.utils import format_string, format_value class TestLine(unittest.TestCase): diff --git a/telegraf/utils.py b/telegraf/utils.py index d48d565..cc500ba 100644 --- a/telegraf/utils.py +++ b/telegraf/utils.py @@ -1,3 +1,5 @@ +import sys + try: basestring except NameError: @@ -46,3 +48,8 @@ def format_value(value): elif isinstance(value, float): value = str(value) return value + + +def is_higher_py35(): + """Check that the Python is version 3.5 or higher.""" + return sys.version_info >= (3, 5) From b19db8ef4c0d370e5c0587d5cc38edbceeb2acb3 Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Thu, 24 Jan 2019 10:33:05 +0000 Subject: [PATCH 2/4] Update examples --- README.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.md b/README.md index 11aa54a..0e9ecd4 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,19 @@ client.metric('some_metric', {'value_a': 100, 'value_b': 100, 'value_c': True}) # Records a single value with one tag client.metric('some_metric', 123, tags={'server_name': 'my-server'}) + +# Times a function call. The measurement name defaults to __module__.__func__ +@client.timer('some_metric', tags={'server_name':'my-server'}) +def some_operation(): + pass + +# Uses a context manager to time an operation +with client.timer('some_metric'): + time.sleep(2) + +# As above, but time in milliseconds +with client.timer('some_metric', use_ms=True): + time.sleep(2) ``` #### Global tags From 82c8baf81e72137d1be38ff26237ed8bb14e872e Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Thu, 24 Jan 2019 10:54:12 +0000 Subject: [PATCH 3/4] Fix flake8 for <3.5 --- telegraf/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegraf/context.py b/telegraf/context.py index 5ea734d..e89faa7 100644 --- a/telegraf/context.py +++ b/telegraf/context.py @@ -11,7 +11,7 @@ def wrapped_coroutine(self, func): """Timing wrapper for async functions.""" @wraps(func) - async def wrapped_co(*args, **kwargs): + async def wrapped_co(*args, **kwargs): # noqa E999 start = time() try: return await func(*args, **kwargs) From 51815dae843c74feebced6fa15171e5e13109484 Mon Sep 17 00:00:00 2001 From: Simon Fraser Date: Thu, 24 Jan 2019 10:57:14 +0000 Subject: [PATCH 4/4] Fix flake8 for <3.5 --- telegraf/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telegraf/context.py b/telegraf/context.py index e89faa7..502713d 100644 --- a/telegraf/context.py +++ b/telegraf/context.py @@ -11,7 +11,7 @@ def wrapped_coroutine(self, func): """Timing wrapper for async functions.""" @wraps(func) - async def wrapped_co(*args, **kwargs): # noqa E999 + async def wrapped_co(*args, **kwargs): # noqa start = time() try: return await func(*args, **kwargs)