update to lastest 097ca7f146e6fec7fb598382dfecce8762c8c6cc 2017/05/31 (#4969)

This commit is contained in:
Ludovic Hautier 2017-06-22 11:45:26 +02:00
parent 0ce2b24bea
commit 5b6e52e816
18 changed files with 702 additions and 36 deletions

View File

@ -14,5 +14,5 @@ Portions of this are from commonware:
https://github.com/jsocol/commonware/blob/master/LICENSE https://github.com/jsocol/commonware/blob/master/LICENSE
.. |Build Status| image:: https://travis-ci.org/andymckay/django-statsd.svg?branch=master .. |Build Status| image:: https://travis-ci.org/django-statsd/django-statsd.svg?branch=master
:target: https://travis-ci.org/andymckay/django-statsd :target: https://travis-ci.org/django-statsd/django-statsd

View File

@ -1,4 +1,4 @@
from __future__ import absolute_import
from django_statsd.clients import statsd from django_statsd.clients import statsd
import time import time

View File

@ -1,6 +1,10 @@
import socket import socket
from django.utils.importlib import import_module try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
from django.conf import settings from django.conf import settings
_statsd = None _statsd = None
@ -22,7 +26,7 @@ def get_client():
# host = socket.gethostbyaddr(host)[2][0] # host = socket.gethostbyaddr(host)[2][0]
port = get('STATSD_PORT', 8125) port = get('STATSD_PORT', 8125)
prefix = get('STATSD_PREFIX', None) prefix = get('STATSD_PREFIX', None)
return import_module(client).StatsClient(host, port, prefix) return import_module(client).StatsClient(host=host, port=port, prefix=prefix)
if not _statsd: if not _statsd:
_statsd = get_client() _statsd = get_client()

View File

@ -22,4 +22,5 @@ class StatsClient(StatsClient):
def gauge(self, stat, value, rate=1, delta=False): def gauge(self, stat, value, rate=1, delta=False):
"""Set a gauge value.""" """Set a gauge value."""
log.info('Gauge: %s, %s%s, %s' % (stat, '' if not delta else 'diff ', value , rate)) log.info('Gauge: %s, %s%s, %s' % (
stat, '' if not delta else 'diff ', value, rate))

View File

@ -1,12 +1,20 @@
import inspect import inspect
import time import time
from django.conf import settings
from django.http import Http404 from django.http import Http404
from django_statsd.clients import statsd from django_statsd.clients import statsd
class GraphiteMiddleware(object): try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
class MiddlewareMixin(object):
pass
class GraphiteMiddleware(MiddlewareMixin):
def process_response(self, request, response): def process_response(self, request, response):
statsd.incr('response.%s' % response.status_code) statsd.incr('response.%s' % response.status_code)
@ -21,7 +29,7 @@ class GraphiteMiddleware(object):
statsd.incr('response.auth.500') statsd.incr('response.auth.500')
class GraphiteRequestTimingMiddleware(object): class GraphiteRequestTimingMiddleware(MiddlewareMixin):
"""statsd's timing data per view.""" """statsd's timing data per view."""
def process_view(self, request, view_func, view_args, view_kwargs): def process_view(self, request, view_func, view_args, view_kwargs):
@ -48,8 +56,9 @@ class GraphiteRequestTimingMiddleware(object):
data = dict(module=request._view_module, name=request._view_name, data = dict(module=request._view_module, name=request._view_name,
method=request.method) method=request.method)
statsd.timing('view.{module}.{name}.{method}'.format(**data), ms) statsd.timing('view.{module}.{name}.{method}'.format(**data), ms)
statsd.timing('view.{module}.{method}'.format(**data), ms) if getattr(settings, 'STATSD_VIEW_TIMER_DETAILS', True):
statsd.timing('view.{method}'.format(**data), ms) statsd.timing('view.{module}.{method}'.format(**data), ms)
statsd.timing('view.{method}'.format(**data), ms)
class TastyPieRequestTimingMiddleware(GraphiteRequestTimingMiddleware): class TastyPieRequestTimingMiddleware(GraphiteRequestTimingMiddleware):

View File

@ -35,6 +35,6 @@ def model_delete(sender, **kwargs):
instance._meta.object_name, instance._meta.object_name,
)) ))
if getattr(settings, 'STATSD_MODEL_SIGNALS', True): if getattr(settings, 'STATSD_MODEL_SIGNALS', False):
post_save.connect(model_save) post_save.connect(model_save)
post_delete.connect(model_delete) post_delete.connect(model_delete)

View File

@ -58,7 +58,7 @@ def times_summary(stats):
for stat in stats: for stat in stats:
timings[stat[0].split('|')[0]].append(stat[2]) timings[stat[0].split('|')[0]].append(stat[2])
for stat, v in timings.iteritems(): for stat, v in timings.items():
if not v: if not v:
continue continue
v.sort() v.sort()

View File

@ -1,5 +1,9 @@
from django.conf import settings from django.conf import settings
from django.utils.importlib import import_module
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
patches = getattr(settings, 'STATSD_PATCHES', []) patches = getattr(settings, 'STATSD_PATCHES', [])

View File

@ -1,5 +1,8 @@
import django import django
from django.db.backends import util try:
from django.db.backends import utils as util
except ImportError:
from django.db.backends import util
from django_statsd.patches.utils import wrap, patch_method from django_statsd.patches.utils import wrap, patch_method
from django_statsd.clients import statsd from django_statsd.clients import statsd
@ -33,6 +36,7 @@ def patched_execute(orig_execute, self, query, *args, **kwargs):
with statsd.timer(key(self.db, 'execute.%s' % _get_query_type(query))): with statsd.timer(key(self.db, 'execute.%s' % _get_query_type(query))):
return orig_execute(self, query, *args, **kwargs) return orig_execute(self, query, *args, **kwargs)
def patched_executemany(orig_executemany, self, query, *args, **kwargs): def patched_executemany(orig_executemany, self, query, *args, **kwargs):
with statsd.timer(key(self.db, 'executemany.%s' % _get_query_type(query))): with statsd.timer(key(self.db, 'executemany.%s' % _get_query_type(query))):
return orig_executemany(self, query, *args, **kwargs) return orig_executemany(self, query, *args, **kwargs)

View File

@ -1,10 +1,7 @@
try: from django.conf.urls import url
from django.conf.urls import patterns, url
except ImportError: # django < 1.4
from django.conf.urls.defaults import patterns, url
import django_statsd.views
urlpatterns = patterns( urlpatterns = [
'', url('^record$', django_statsd.views.record, name='django_statsd.record'),
url('^record$', 'django_statsd.views.record', name='django_statsd.record'), ]
)

View File

@ -135,6 +135,7 @@ clients = {
@csrf_exempt @csrf_exempt
@require_http_methods(["GET", "POST"])
def record(request): def record(request):
""" """
This is a Django method you can link to in your URLs that process This is a Django method you can link to in your URLs that process
@ -146,10 +147,11 @@ def record(request):
you need for imposing security on this method, so that not just anyone you need for imposing security on this method, so that not just anyone
can post to it. can post to it.
""" """
if 'client' not in request.REQUEST: data = request.POST or request.GET
if 'client' not in data:
return http.HttpResponseBadRequest() return http.HttpResponseBadRequest()
client = request.REQUEST['client'] client = data.get('client')
if client not in clients: if client not in clients:
return http.HttpResponseBadRequest() return http.HttpResponseBadRequest()

View File

@ -16,6 +16,12 @@ Credits:
Changes Changes
------- -------
0.3.15:
- push from travis to pypi to keep files clean
- allow less statsd in the middleware
- fix to a specific statsd version
0.3.14: 0.3.14:
- pypy testing support - pypy testing support
@ -168,6 +174,14 @@ To get timings for your database or your cache, put in some monkeypatches::
'django_statsd.patches.cache', 'django_statsd.patches.cache',
] ]
You can change the host that stats are sent to with the `STATSD_HOST` setting::
STATSD_HOST = 'localhost'
Similarly, you can use the `STATSD_PORT`setting to customize the port number (which defaults to `8125`)::
STATSD_PORT = 8125
Toolbar integration Toolbar integration
------------------- -------------------
@ -250,11 +264,11 @@ First, make sure you can record the timings in your Django site urls. This
could be done by pointing straight to the view or including the URL for could be done by pointing straight to the view or including the URL for
example:: example::
from django_statsd.urls import urlpatterns as statsd_patterns from django_statsd.urls import urlpatterns as statsd_patterns
urlpatterns = patterns('', urlpatterns = [
('^services/timing/', include(statsd_patterns)), url(r'^services/timing/', include(statsd_patterns)),
) ]
In this case the URL to the record view will be `/services/timing/record`. In this case the URL to the record view will be `/services/timing/record`.
@ -326,6 +340,13 @@ everyone not in INTERNAL_IPS::
STATSD_RECORD_GUARD = internal_only STATSD_RECORD_GUARD = internal_only
STATSD_VIEW_TIMER_DETAILS (optional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The middleware sends timing pings for the almost the same thing three times
when accessing a view: `module.name.method`, `module.method` and `method` by
default. Setting this to `False` just does the former.
Logging errors Logging errors
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
@ -341,9 +362,20 @@ do this by adding in the handler. For example in your logging configuration::
Testing Testing
======= =======
You can run tests with the following command: You need to install tox_ to run the tests.
You can run the full test matrix with:
DJANGO_SETTINGS_MODULE='django_statsd.test_settings' nosetests tox
or choose a specific environment - let's say Python 3.4 and Django 1.11 - with:
tox -e py34-django111
You can list all the available environments with:
tox -l
.. _tox: http://tox.readthedocs.io/en/latest/index.html
Nose Nose
==== ====

View File

@ -1,5 +1,4 @@
mock mock
nose nose
unittest2 pytest-django
statsd>=2.0.0 statsd==3.2.1
django<1.7

View File

@ -4,13 +4,13 @@ from setuptools import setup
setup( setup(
# Because django-statsd was taken, I called this django-statsd-mozilla. # Because django-statsd was taken, I called this django-statsd-mozilla.
name='django-statsd-mozilla', name='django-statsd-mozilla',
version='0.3.14', version='0.3.16',
description='Django interface with statsd', description='Django interface with statsd',
long_description=open('README.rst').read(), long_description=open('README.rst').read(),
author='Andy McKay', author='Andy McKay',
author_email='andym@mozilla.com', author_email='andym@mozilla.com',
license='BSD', license='BSD',
install_requires=['statsd>=2.0.0'], install_requires=['statsd >= 2.1.2, != 3.2 , <= 4.0'],
packages=['django_statsd', packages=['django_statsd',
'django_statsd/patches', 'django_statsd/patches',
'django_statsd/clients', 'django_statsd/clients',
@ -29,6 +29,8 @@ setup(
'Intended Audience :: Developers', 'Intended Audience :: Developers',
'Natural Language :: English', 'Natural Language :: English',
'Operating System :: OS Independent', 'Operating System :: OS Independent',
'Framework :: Django' 'Framework :: Django',
'Programming Language :: Python',
'Programming Language :: Python :: 3'
] ]
) )

0
tests/__init__.py Normal file
View File

34
tests/conftest.py Normal file
View File

@ -0,0 +1,34 @@
def pytest_configure():
from django.conf import settings
settings.configure(
DEBUG_PROPAGATE_EXCEPTIONS=True,
DATABASES={
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:'
}
},
SITE_ID=1,
SECRET_KEY='not very secret in tests',
ROOT_URLCONF='django_statsd.urls',
INSTALLED_APPS=(
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.staticfiles',
'django_statsd',
'tests',
),
STATSD_CLIENT='django_statsd.clients.null',
STATSD_PREFIX=None,
METLOG=None,
)
try:
import django
django.setup()
except AttributeError:
pass

546
tests/test_django_statsd.py Normal file
View File

@ -0,0 +1,546 @@
import json
import logging.config
import sys
import unittest
from django.conf import settings
from nose.exc import SkipTest
from nose import tools as nose_tools
try:
from django.urls import reverse
except ImportError:
from django.core.urlresolvers import reverse
from django.http import HttpResponse, HttpResponseForbidden
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils.http import urlencode
import mock
from nose.tools import eq_
from django_statsd.clients import get_client, statsd
from django_statsd.patches import utils
from django_statsd.patches.db import (
patched_callproc,
patched_execute,
patched_executemany,
)
from django_statsd import middleware
cfg = {
'version': 1,
'formatters': {},
'handlers': {
'test_statsd_handler': {
'class': 'django_statsd.loggers.errors.StatsdHandler',
},
},
'loggers': {
'test.logging': {
'handlers': ['test_statsd_handler'],
},
},
}
@mock.patch.object(middleware.statsd, 'incr')
class TestIncr(TestCase):
def setUp(self):
self.req = RequestFactory().get('/')
self.res = HttpResponse()
def test_graphite_response(self, incr):
gmw = middleware.GraphiteMiddleware()
gmw.process_response(self.req, self.res)
assert incr.called
def test_graphite_response_authenticated(self, incr):
self.req.user = mock.Mock()
self.req.user.is_authenticated.return_value = True
gmw = middleware.GraphiteMiddleware()
gmw.process_response(self.req, self.res)
eq_(incr.call_count, 2)
def test_graphite_exception(self, incr):
gmw = middleware.GraphiteMiddleware()
gmw.process_exception(self.req, None)
assert incr.called
def test_graphite_exception_authenticated(self, incr):
self.req.user = mock.Mock()
self.req.user.is_authenticated.return_value = True
gmw = middleware.GraphiteMiddleware()
gmw.process_exception(self.req, None)
eq_(incr.call_count, 2)
@mock.patch.object(middleware.statsd, 'timing')
class TestTiming(unittest.TestCase):
def setUp(self):
self.req = RequestFactory().get('/')
self.res = HttpResponse()
def test_request_timing(self, timing):
func = lambda x: x
gmw = middleware.GraphiteRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), dict())
gmw.process_response(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.%s.%s.GET' % (func.__module__, func.__name__),
'view.%s.GET' % func.__module__,
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
def test_request_timing_exception(self, timing):
func = lambda x: x
gmw = middleware.GraphiteRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), dict())
gmw.process_exception(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.%s.%s.GET' % (func.__module__, func.__name__),
'view.%s.GET' % func.__module__,
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
def test_request_timing_tastypie(self, timing):
func = lambda x: x
gmw = middleware.TastyPieRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), {
'api_name': 'my_api_name',
'resource_name': 'my_resource_name'
})
gmw.process_response(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.my_api_name.my_resource_name.GET',
'view.my_api_name.GET',
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
def test_request_timing_tastypie_fallback(self, timing):
func = lambda x: x
gmw = middleware.TastyPieRequestTimingMiddleware()
gmw.process_view(self.req, func, tuple(), dict())
gmw.process_response(self.req, self.res)
eq_(timing.call_count, 3)
names = ['view.%s.%s.GET' % (func.__module__, func.__name__),
'view.%s.GET' % func.__module__,
'view.GET']
for expected, (args, kwargs) in zip(names, timing.call_args_list):
eq_(expected, args[0])
class TestClient(unittest.TestCase):
@mock.patch.object(settings, 'STATSD_CLIENT', 'statsd.client')
def test_normal(self):
eq_(get_client().__module__, 'statsd.client')
@mock.patch.object(settings, 'STATSD_CLIENT',
'django_statsd.clients.null')
def test_null(self):
eq_(get_client().__module__, 'django_statsd.clients.null')
@mock.patch.object(settings, 'STATSD_CLIENT',
'django_statsd.clients.toolbar')
def test_toolbar(self):
eq_(get_client().__module__, 'django_statsd.clients.toolbar')
@mock.patch.object(settings, 'STATSD_CLIENT',
'django_statsd.clients.toolbar')
def test_toolbar_send(self):
client = get_client()
eq_(client.cache, {})
client.incr('testing')
eq_(client.cache, {'testing|count': [[1, 1]]})
class TestMetlogClient(TestCase):
def check_metlog(self):
try:
from metlog.config import client_from_dict_config
return client_from_dict_config
except ImportError:
raise SkipTest("Metlog is not installed")
@nose_tools.raises(AttributeError)
def test_no_metlog(self):
with self.settings(STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
get_client()
def _create_client(self):
client_from_dict_config = self.check_metlog()
# Need to load within the test in case metlog is not installed
from metlog.config import client_from_dict_config
METLOG_CONF = {
'logger': 'django-statsd',
'sender': {
'class': 'metlog.senders.DebugCaptureSender',
},
}
return client_from_dict_config(METLOG_CONF)
def test_get_client(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(client.__module__, 'django_statsd.clients.moz_metlog')
def test_metlog_incr(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.incr('testing')
eq_(len(client.metlog.sender.msgs), 1)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '1')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'moz_metlog.testing')
eq_(msg['type'], 'counter')
def test_metlog_decr(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.decr('testing')
eq_(len(client.metlog.sender.msgs), 1)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '-1')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'moz_metlog.testing')
eq_(msg['type'], 'counter')
def test_metlog_timing(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='moz_metlog',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.timing('testing', 512, rate=2)
eq_(len(client.metlog.sender.msgs), 1)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '512')
eq_(msg['fields']['rate'], 2)
eq_(msg['fields']['name'], 'moz_metlog.testing')
eq_(msg['type'], 'timer')
@nose_tools.raises(AttributeError)
def test_metlog_no_prefixes(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
client.incr('foo', 2)
def test_metlog_prefixes(self):
metlog = self._create_client()
with self.settings(METLOG=metlog,
STATSD_PREFIX='some_prefix',
STATSD_CLIENT='django_statsd.clients.moz_metlog'):
client = get_client()
eq_(len(client.metlog.sender.msgs), 0)
client.timing('testing', 512, rate=2)
client.incr('foo', 2)
client.decr('bar', 5)
eq_(len(client.metlog.sender.msgs), 3)
msg = json.loads(client.metlog.sender.msgs[0])
eq_(msg['severity'], 6)
eq_(msg['payload'], '512')
eq_(msg['fields']['rate'], 2)
eq_(msg['fields']['name'], 'some_prefix.testing')
eq_(msg['type'], 'timer')
msg = json.loads(client.metlog.sender.msgs[1])
eq_(msg['severity'], 6)
eq_(msg['payload'], '2')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'some_prefix.foo')
eq_(msg['type'], 'counter')
msg = json.loads(client.metlog.sender.msgs[2])
eq_(msg['severity'], 6)
eq_(msg['payload'], '-5')
eq_(msg['fields']['rate'], 1)
eq_(msg['fields']['name'], 'some_prefix.bar')
eq_(msg['type'], 'counter')
# This is primarily for Zamboni, which loads in the custom middleware
# classes, one of which, breaks posts to our url. Let's stop that.
@mock.patch.object(settings, 'MIDDLEWARE_CLASSES', [])
class TestRecord(TestCase):
urls = 'django_statsd.urls'
def setUp(self):
super(TestRecord, self).setUp()
self.url = reverse('django_statsd.record')
settings.STATSD_RECORD_GUARD = None
self.good = {'client': 'boomerang', 'nt_nav_st': 1,
'nt_domcomp': 3}
self.stick = {'client': 'stick',
'window.performance.timing.domComplete': 123,
'window.performance.timing.domInteractive': 456,
'window.performance.timing.domLoading': 789,
'window.performance.timing.navigationStart': 0,
'window.performance.navigation.redirectCount': 3,
'window.performance.navigation.type': 1}
def test_no_client(self):
response = self.client.get(self.url)
assert response.status_code == 400
def test_no_valid_client(self):
response = self.client.get(self.url, {'client': 'no'})
assert response.status_code == 400
def test_boomerang_almost(self):
response = self.client.get(self.url, {'client': 'boomerang'})
assert response.status_code == 400
def test_boomerang_minimum(self):
content = self.client.get(
self.url, {
'client': 'boomerang',
'nt_nav_st': 1,
}).content.decode()
assert content == 'recorded'
@mock.patch('django_statsd.views.process_key')
def test_boomerang_something(self, process_key):
content = self.client.get(self.url, self.good).content.decode()
assert content == 'recorded'
assert process_key.called
def test_boomerang_post(self):
assert self.client.post(self.url + '?' + urlencode(self.good), self.good).status_code == 405
def test_good_guard(self):
settings.STATSD_RECORD_GUARD = lambda r: None
response = self.client.get(self.url, self.good)
assert response.status_code == 200
def test_bad_guard(self):
settings.STATSD_RECORD_GUARD = lambda r: HttpResponseForbidden()
response = self.client.get(self.url, self.good)
assert response.status_code == 403
def test_stick_get(self):
assert self.client.get(self.url, self.stick).status_code == 405
@mock.patch('django_statsd.views.process_key')
def test_stick(self, process_key):
assert self.client.post(self.url, self.stick).status_code == 200
assert process_key.called
def test_stick_start(self):
data = self.stick.copy()
del data['window.performance.timing.navigationStart']
assert self.client.post(self.url, data).status_code == 400
@mock.patch('django_statsd.views.process_key')
def test_stick_missing(self, process_key):
data = self.stick.copy()
del data['window.performance.timing.domInteractive']
assert self.client.post(self.url, data).status_code == 200
assert process_key.called
def test_stick_garbage(self):
data = self.stick.copy()
data['window.performance.timing.domInteractive'] = '<alert>'
assert self.client.post(self.url, data).status_code == 400
def test_stick_some_garbage(self):
data = self.stick.copy()
data['window.performance.navigation.redirectCount'] = '<alert>'
assert self.client.post(self.url, data).status_code == 400
def test_stick_more_garbage(self):
data = self.stick.copy()
data['window.performance.navigation.type'] = '<alert>'
assert self.client.post(self.url, data).status_code == 400
@mock.patch.object(middleware.statsd, 'incr')
class TestErrorLog(TestCase):
def setUp(self):
logging.config.dictConfig(cfg)
self.log = logging.getLogger('test.logging')
def division_error(self):
try:
1 / 0
except:
return sys.exc_info()
def test_emit(self, incr):
self.log.error('blargh!', exc_info=self.division_error())
assert incr.call_args[0][0] == 'error.zerodivisionerror'
def test_not_emit(self, incr):
self.log.error('blargh!')
assert not incr.called
class TestPatchMethod(TestCase):
def setUp(self):
super(TestPatchMethod, self).setUp()
class DummyClass(object):
def sumargs(self, a, b, c=3, d=4):
return a + b + c + d
def badfn(self, a, b=2):
raise ValueError
self.cls = DummyClass
def test_late_patching(self):
"""
Objects created before patching should get patched as well.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return original_fn(self, *args, **kwargs) + 10
obj = self.cls()
self.assertEqual(obj.sumargs(1, 2, 3, 4), 10)
utils.patch_method(self.cls, 'sumargs')(patch_fn)
self.assertEqual(obj.sumargs(1, 2, 3, 4), 20)
def test_doesnt_call_original_implicitly(self):
"""
Original fn must be called explicitly from patched to be
executed.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return 10
with self.assertRaises(ValueError):
obj = self.cls()
obj.badfn(1, 2)
utils.patch_method(self.cls, 'badfn')(patch_fn)
self.assertEqual(obj.badfn(1, 2), 10)
def test_args_kwargs_are_honored(self):
"""
Args and kwargs must be honored between calls from the patched to
the original version.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return original_fn(self, *args, **kwargs)
utils.patch_method(self.cls, 'sumargs')(patch_fn)
obj = self.cls()
self.assertEqual(obj.sumargs(1, 2), 10)
self.assertEqual(obj.sumargs(1, 1, d=1), 6)
self.assertEqual(obj.sumargs(1, 1, 1, 1), 4)
def test_patched_fn_can_receive_arbitrary_arguments(self):
"""
Args and kwargs can be received arbitrarily with no contraints on
the patched fn, even if the original_fn had a fixed set of
allowed args and kwargs.
"""
def patch_fn(original_fn, self, *args, **kwargs):
return args, kwargs
utils.patch_method(self.cls, 'badfn')(patch_fn)
obj = self.cls()
self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
self.assertEqual(obj.badfn(1, d=2), ((1,), {'d': 2}))
self.assertEqual(obj.badfn(1, 2, c=1, d=2), ((1, 2), {'c': 1, 'd': 2}))
class TestCursorWrapperPatching(TestCase):
example_queries = {
'select': 'select * from something;',
'insert': 'insert (1, 2) into something;',
'update': 'update something set a=1;',
}
def test_patched_callproc_calls_timer(self):
for operation, query in list(self.example_queries.items()):
with mock.patch.object(statsd, 'timer') as timer:
client = mock.Mock(executable_name='client_executable_name')
db = mock.Mock(executable_name='name', alias='alias', client=client)
instance = mock.Mock(db=db)
patched_callproc(lambda *args, **kwargs: None, instance, query)
self.assertEqual(timer.call_count, 1)
self.assertEqual(timer.call_args[0][0], 'db.client_executable_name.alias.callproc.%s' % operation)
def test_patched_execute_calls_timer(self):
for operation, query in list(self.example_queries.items()):
with mock.patch.object(statsd, 'timer') as timer:
client = mock.Mock(executable_name='client_executable_name')
db = mock.Mock(executable_name='name', alias='alias', client=client)
instance = mock.Mock(db=db)
patched_execute(lambda *args, **kwargs: None, instance, query)
self.assertEqual(timer.call_count, 1)
self.assertEqual(timer.call_args[0][0], 'db.client_executable_name.alias.execute.%s' % operation)
def test_patched_executemany_calls_timer(self):
for operation, query in list(self.example_queries.items()):
with mock.patch.object(statsd, 'timer') as timer:
client = mock.Mock(executable_name='client_executable_name')
db = mock.Mock(executable_name='name', alias='alias', client=client)
instance = mock.Mock(db=db)
patched_executemany(lambda *args, **kwargs: None, instance, query)
self.assertEqual(timer.call_count, 1)
self.assertEqual(timer.call_args[0][0], 'db.client_executable_name.alias.executemany.%s' % operation)
@mock.patch('django_statsd.patches.db.patched_callproc')
@mock.patch('django_statsd.patches.db.patched_executemany')
@mock.patch('django_statsd.patches.db.patched_execute')
@mock.patch('django.db.backends.utils.CursorWrapper')
def test_cursorwrapper_patching(self, CursorWrapper, execute, executemany, callproc):
from django_statsd.patches.db import patch
execute.__name__ = 'execute'
executemany.__name__ = 'executemany'
callproc.__name__ = 'callproc'
execute.return_value = 'execute'
executemany.return_value = 'executemany'
callproc.return_value = 'callproc'
patch()
self.assertEqual(CursorWrapper.execute(), 'execute')
self.assertEqual(CursorWrapper.executemany(), 'executemany')
self.assertEqual(CursorWrapper.callproc(), 'callproc')

32
tox.ini Normal file
View File

@ -0,0 +1,32 @@
[pytest]
addopts=--tb=short
[tox]
envlist =
{py27,py33,py34,py35,pypy,pypy3}-django18,
{py27,py34,py35,pypy,pypy3}-django{19,110},
{py27,py34,py35,py36,pypy,pypy3}-django111,
{py35,py36,pypy,pypy3}-djangomaster
[travis:env]
DJANGO =
1.8: django18
1.9: django19
1.10: django110
1.11: django111
master: djangomaster
[testenv]
commands = py.test
setenv =
PYTHONDONTWRITEBYTECODE=1
PYTHONWARNINGS=once
deps =
py27: -roptional.txt
django18: Django>=1.8,<1.9
django19: Django>=1.9,<1.10
django110: Django>=1.10,<1.11
django111: Django>=1.11,<2.0
djangomaster: https://github.com/django/django/archive/master.tar.gz
-rrequirements.txt