update to lastest 097ca7f146e6fec7fb598382dfecce8762c8c6cc 2017/05/31 (#4969)
This commit is contained in:
parent
0ce2b24bea
commit
5b6e52e816
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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', [])
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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'),
|
]
|
||||||
)
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
====
|
====
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
mock
|
mock
|
||||||
nose
|
nose
|
||||||
unittest2
|
pytest-django
|
||||||
statsd>=2.0.0
|
statsd==3.2.1
|
||||||
django<1.7
|
|
||||||
|
|
8
setup.py
8
setup.py
|
@ -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,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
|
|
@ -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')
|
|
@ -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
|
Loading…
Reference in New Issue