Skip to content

Commit 4ab6373

Browse files
author
Emanuele Palazzetti
authored
Merge pull request #70 from palazzem/django-cache
[django] instrument the Django cache framework
2 parents cf3377a + 80a257d commit 4ab6373

File tree

13 files changed

+985
-4
lines changed

13 files changed

+985
-4
lines changed

ddtrace/contrib/django/apps.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# project
77
from .db import patch_db
88
from .conf import settings
9+
from .cache import patch_cache
910
from .templates import patch_template
1011

1112
from ...ext import AppTypes
@@ -28,9 +29,9 @@ def ready(self):
2829

2930
# define the service details
3031
tracer.set_service_info(
31-
service=settings.DEFAULT_SERVICE,
3232
app='django',
3333
app_type=AppTypes.web,
34+
service=settings.DEFAULT_SERVICE,
3435
)
3536

3637
# trace Django internals
@@ -43,3 +44,8 @@ def ready(self):
4344
patch_template(tracer)
4445
except Exception:
4546
log.exception('error patching Django template rendering')
47+
48+
try:
49+
patch_cache(tracer)
50+
except Exception:
51+
log.exception('error patching Django cache')

ddtrace/contrib/django/cache.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import logging
2+
3+
from functools import wraps
4+
5+
from django.conf import settings as django_settings
6+
7+
from .conf import settings, import_from_string
8+
from .utils import quantize_key_values, _resource_from_cache_prefix
9+
10+
11+
log = logging.getLogger(__name__)
12+
13+
# code instrumentation
14+
DATADOG_NAMESPACE = '__datadog_original_{method}'
15+
TRACED_METHODS = [
16+
'get',
17+
'set',
18+
'add',
19+
'delete',
20+
'incr',
21+
'decr',
22+
'get_many',
23+
'set_many',
24+
'delete_many',
25+
]
26+
27+
# standard tags
28+
TYPE = 'cache'
29+
CACHE_BACKEND = 'django.cache.backend'
30+
CACHE_COMMAND_KEY = 'django.cache.key'
31+
32+
33+
def patch_cache(tracer):
34+
"""
35+
Function that patches the inner cache system. Because the cache backend
36+
can have different implementations and connectors, this function must
37+
handle all possible interactions with the Django cache. What follows
38+
is currently traced:
39+
* in-memory cache
40+
* the cache client wrapper that could use any of the common
41+
Django supported cache servers (Redis, Memcached, Database, Custom)
42+
"""
43+
# discover used cache backends
44+
cache_backends = [cache['BACKEND'] for cache in django_settings.CACHES.values()]
45+
46+
def _trace_operation(fn, method_name):
47+
"""
48+
Return a wrapped function that traces a cache operation
49+
"""
50+
@wraps(fn)
51+
def wrapped(self, *args, **kwargs):
52+
# get the original function method
53+
method = getattr(self, DATADOG_NAMESPACE.format(method=method_name))
54+
with tracer.trace('django.cache',
55+
span_type=TYPE, service=settings.DEFAULT_SERVICE) as span:
56+
# update the resource name and tag the cache backend
57+
span.resource = _resource_from_cache_prefix(method_name, self)
58+
cache_backend = '{}.{}'.format(self.__module__, self.__class__.__name__)
59+
span.set_tag(CACHE_BACKEND, cache_backend)
60+
61+
if args:
62+
keys = quantize_key_values(args[0])
63+
span.set_tag(CACHE_COMMAND_KEY, keys)
64+
65+
return method(*args, **kwargs)
66+
return wrapped
67+
68+
def _wrap_method(cls, method_name):
69+
"""
70+
For the given class, wraps the method name with a traced operation
71+
so that the original method is executed, while the span is properly
72+
created
73+
"""
74+
# check if the backend owns the given bounded method
75+
if not hasattr(cls, method_name):
76+
return
77+
78+
# prevent patching each backend's method more than once
79+
if hasattr(cls, DATADOG_NAMESPACE.format(method=method_name)):
80+
log.debug('{} already traced'.format(method_name))
81+
else:
82+
method = getattr(cls, method_name)
83+
setattr(cls, DATADOG_NAMESPACE.format(method=method_name), method)
84+
setattr(cls, method_name, _trace_operation(method, method_name))
85+
86+
# trace all backends
87+
for cache_module in cache_backends:
88+
cache = import_from_string(cache_module, cache_module)
89+
90+
for method in TRACED_METHODS:
91+
_wrap_method(cache, method)

ddtrace/contrib/django/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def __getattr__(self, attr):
102102
return val
103103

104104
def __check_user_settings(self, user_settings):
105-
SETTINGS_DOC = 'http://pypi.datadoghq.com/trace-dev/docs/#module-ddtrace.contrib.django'
105+
SETTINGS_DOC = 'http://pypi.datadoghq.com/trace/docs/#module-ddtrace.contrib.django'
106106
for setting in REMOVED_SETTINGS:
107107
if setting in user_settings:
108108
raise RuntimeError(

ddtrace/contrib/django/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
def _resource_from_cache_prefix(resource, cache):
2+
"""
3+
Combine the resource name with the cache prefix (if any)
4+
"""
5+
if getattr(cache, "key_prefix", None):
6+
name = "{} {}".format(resource, cache.key_prefix)
7+
else:
8+
name = resource
9+
10+
# enforce lowercase to make the output nicer to read
11+
return name.lower()
12+
13+
14+
def quantize_key_values(key):
15+
"""
16+
Used in the Django trace operation method, it ensures that if a dict
17+
with values is used, we removes the values from the span meta
18+
attributes. For example::
19+
20+
>>> quantize_key_values({'key', 'value'})
21+
# returns ['key']
22+
"""
23+
if isinstance(key, dict):
24+
return key.keys()
25+
26+
return key

tests/contrib/django/app/settings.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,34 @@
1515
}
1616
}
1717

18+
CACHES = {
19+
'default': {
20+
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
21+
'LOCATION': 'unique-snowflake',
22+
},
23+
'redis': {
24+
'BACKEND': 'django_redis.cache.RedisCache',
25+
'LOCATION': 'redis://127.0.0.1:56379/1',
26+
},
27+
'pylibmc': {
28+
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
29+
'LOCATION': '127.0.0.1:51211',
30+
},
31+
'python_memcached': {
32+
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
33+
'LOCATION': '127.0.0.1:51211',
34+
},
35+
'django_pylibmc': {
36+
'BACKEND': 'django_pylibmc.memcached.PyLibMCCache',
37+
'LOCATION': '127.0.0.1:51211',
38+
'BINARY': True,
39+
'OPTIONS': {
40+
'tcp_nodelay': True,
41+
'ketama': True
42+
}
43+
},
44+
}
45+
1846
SITE_ID = 1
1947
SECRET_KEY = 'not_very_secret_in_tests'
2048
USE_I18N = True
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{% load cache %}
2+
3+
{% cache 60 users_list %}
4+
{% for user in object_list %}
5+
{{ user }}
6+
{% endfor %}
7+
{% endcache %}

tests/contrib/django/app/views.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
"""
44
from django.http import HttpResponse
55
from django.conf.urls import url
6-
from django.views.generic import ListView, TemplateView
76
from django.contrib.auth.models import User
7+
from django.views.generic import ListView, TemplateView
8+
from django.views.decorators.cache import cache_page
89

910

1011
class UserList(ListView):
1112
model = User
1213
template_name = 'users_list.html'
1314

1415

16+
class TemplateCachedUserList(ListView):
17+
model = User
18+
template_name = 'cached_list.html'
19+
20+
1521
class ForbiddenView(TemplateView):
1622
def get(self, request, *args, **kwargs):
1723
return HttpResponse(status=403)
@@ -20,5 +26,7 @@ def get(self, request, *args, **kwargs):
2026
# use this url patterns for tests
2127
urlpatterns = [
2228
url(r'^users/$', UserList.as_view(), name='users-list'),
29+
url(r'^cached-template/$', TemplateCachedUserList.as_view(), name='cached-template-list'),
30+
url(r'^cached-users/$', cache_page(60)(UserList.as_view()), name='cached-users-list'),
2331
url(r'^fail-view/$', ForbiddenView.as_view(), name='forbidden-view'),
2432
]

0 commit comments

Comments
 (0)