Skip to content

Commit e609427

Browse files
author
Emanuele Palazzetti
authored
[django] provide a Django app (#67) from palazzem/django-app-conversion
2 parents adbdd71 + 442de87 commit e609427

File tree

11 files changed

+284
-128
lines changed

11 files changed

+284
-128
lines changed

ddtrace/contrib/django/__init__.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,63 @@
11
"""
2-
The Django middleware will trace requests, database calls and template
2+
The Django integration will trace requests, database calls and template
33
renders.
44
55
To install the Django tracing middleware, add it to the list of your
6-
application's installed in middleware in settings.py::
6+
installed apps and in your middleware classes in ``settings.py``::
77
8+
INSTALLED_APPS = [
9+
# ...
10+
11+
# the order is not important
12+
'ddtrace.contrib.django',
13+
]
814
915
MIDDLEWARE_CLASSES = (
10-
...
16+
# the tracer must be the first middleware
1117
'ddtrace.contrib.django.TraceMiddleware',
1218
...
1319
)
1420
15-
DATADOG_SERVICE = 'my-app'
21+
The configuration of this integration is all namespaced inside a single
22+
Django setting, named ``DATADOG_TRACE``. For example, your ``settings.py``
23+
may contain::
1624
17-
"""
25+
DATADOG_TRACE = {
26+
'DEFAULT_SERVICE': 'my-django-app',
27+
}
28+
29+
If you need to access to the tracing settings, you should::
30+
31+
from ddtrace.contrib.django.conf import settings
32+
33+
tracer = settings.TRACER
34+
tracer.trace("something")
35+
# your code ...
1836
37+
The available settings are:
38+
39+
* ``TRACER`` (default ``ddtrace.tracer``): set the default tracer
40+
instance that is used to trace Django internals. By default the ``ddtrace``
41+
tracer is used.
42+
* ``DEFAULT_SERVICE`` (default: ``django``): set the service name used by the
43+
tracer. Usually this configuration must be updated with a meaningful name.
44+
* ``ENABLED``: (default: ``not django_settings.DEBUG``): set if the tracer
45+
is enabled or not. When a tracer is disabled, Django internals are not
46+
automatically instrumented and the requests are not traced even if the
47+
``TraceMiddleware`` is properly installed. This settings cannot be changed
48+
at runtime and a restart is required. By default the tracer is disabled
49+
when in ``DEBUG`` mode, enabled otherwise.
50+
"""
1951
from ..util import require_modules
2052

53+
2154
required_modules = ['django']
2255

2356
with require_modules(required_modules) as missing_modules:
2457
if not missing_modules:
2558
from .middleware import TraceMiddleware
2659
__all__ = ['TraceMiddleware']
60+
61+
62+
# define the Django app configuration
63+
default_app_config = 'ddtrace.contrib.django.apps.TracerConfig'

ddtrace/contrib/django/apps.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import logging
2+
3+
# 3rd party
4+
from django.apps import AppConfig
5+
6+
# project
7+
from .db import patch_db
8+
from .conf import settings
9+
from .templates import patch_template
10+
11+
from ...ext import AppTypes
12+
13+
14+
log = logging.getLogger(__name__)
15+
16+
17+
class TracerConfig(AppConfig):
18+
name = 'ddtrace.contrib.django'
19+
20+
def ready(self):
21+
"""
22+
Ready is called as soon as the registry is fully populated.
23+
Tracing capabilities must be enabled in this function so that
24+
all Django internals are properly configured.
25+
"""
26+
if settings.ENABLED:
27+
tracer = settings.TRACER
28+
29+
# define the service details
30+
tracer.set_service_info(
31+
service=settings.DEFAULT_SERVICE,
32+
app='django',
33+
app_type=AppTypes.web,
34+
)
35+
36+
# trace Django internals
37+
try:
38+
patch_db(tracer)
39+
except Exception:
40+
log.exception('error patching Django database connections')
41+
42+
try:
43+
patch_template(tracer)
44+
except Exception:
45+
log.exception('error patching Django template rendering')

ddtrace/contrib/django/conf.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""
2+
Settings for Datadog tracer are all namespaced in the DATADOG_TRACE setting.
3+
For example your project's `settings.py` file might look like this:
4+
5+
DATADOG_TRACE = {
6+
'TRACER': 'myapp.tracer',
7+
}
8+
9+
This module provides the `setting` object, that is used to access
10+
Datadog settings, checking for user settings first, then falling
11+
back to the defaults.
12+
"""
13+
from __future__ import unicode_literals
14+
15+
import importlib
16+
17+
from django.conf import settings as django_settings
18+
19+
from django.test.signals import setting_changed
20+
21+
22+
USER_SETTINGS = getattr(django_settings, 'DATADOG_TRACE', None)
23+
24+
# List of available settings with their defaults
25+
DEFAULTS = {
26+
'TRACER': 'ddtrace.tracer',
27+
'DEFAULT_SERVICE': 'django',
28+
'ENABLED': not django_settings.DEBUG,
29+
}
30+
31+
# List of settings that may be in string import notation.
32+
IMPORT_STRINGS = (
33+
'TRACER',
34+
)
35+
36+
# List of settings that have been removed
37+
REMOVED_SETTINGS = ()
38+
39+
40+
def import_from_string(val, setting_name):
41+
"""
42+
Attempt to import a class from a string representation.
43+
"""
44+
try:
45+
# Nod to tastypie's use of importlib.
46+
parts = val.split('.')
47+
module_path, class_name = '.'.join(parts[:-1]), parts[-1]
48+
module = importlib.import_module(module_path)
49+
return getattr(module, class_name)
50+
except (ImportError, AttributeError) as e:
51+
msg = 'Could not import "{}" for setting "{}". {}: {}.'.format(
52+
val, setting_name,
53+
e.__class__.__name__, e
54+
)
55+
56+
raise ImportError(msg)
57+
58+
59+
class DatadogSettings(object):
60+
"""
61+
A settings object, that allows Datadog settings to be accessed as properties.
62+
For example:
63+
64+
from ddtrace.contrib.django.conf import settings
65+
66+
tracer = settings.TRACER
67+
68+
Any setting with string import paths will be automatically resolved
69+
and return the class, rather than the string literal.
70+
"""
71+
def __init__(self, user_settings=None, defaults=None, import_strings=None):
72+
if user_settings:
73+
self._user_settings = self.__check_user_settings(user_settings)
74+
self.defaults = defaults or DEFAULTS
75+
self.import_strings = import_strings or IMPORT_STRINGS
76+
77+
@property
78+
def user_settings(self):
79+
if not hasattr(self, '_user_settings'):
80+
self._user_settings = getattr(settings, 'DATADOG_TRACE', {})
81+
return self._user_settings
82+
83+
def __getattr__(self, attr):
84+
if attr not in self.defaults:
85+
raise AttributeError('Invalid setting: "{}"'.format(attr))
86+
87+
try:
88+
# Check if present in user settings
89+
val = self.user_settings[attr]
90+
except KeyError:
91+
# Otherwise, fall back to defaults
92+
val = self.defaults[attr]
93+
94+
# Coerce import strings into classes
95+
if attr in self.import_strings:
96+
val = import_from_string(val, attr)
97+
98+
# Cache the result
99+
setattr(self, attr, val)
100+
return val
101+
102+
def __check_user_settings(self, user_settings):
103+
SETTINGS_DOC = 'http://pypi.datadoghq.com/trace-dev/docs/#module-ddtrace.contrib.django'
104+
for setting in REMOVED_SETTINGS:
105+
if setting in user_settings:
106+
raise RuntimeError(
107+
'The "{}" setting has been removed, check "{}".'.format(setting, SETTINGS_DOC)
108+
)
109+
return user_settings
110+
111+
112+
settings = DatadogSettings(USER_SETTINGS, DEFAULTS, IMPORT_STRINGS)
113+
114+
115+
def reload_settings(*args, **kwargs):
116+
"""
117+
Triggers a reload when Django emits the reloading signal
118+
"""
119+
global settings
120+
setting, value = kwargs['setting'], kwargs['value']
121+
if setting == 'DATADOG_TRACE':
122+
settings = DatadogSettings(value, DEFAULTS, IMPORT_STRINGS)
123+
124+
125+
setting_changed.connect(reload_settings)

ddtrace/contrib/django/middleware.py

Lines changed: 17 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,44 @@
11
import logging
22

33
# project
4-
from ...ext import http, AppTypes
5-
from ...contrib import func_name
4+
from .conf import settings
65

7-
from .db import patch_db
8-
from .settings import import_from_string
9-
from .templates import patch_template
6+
from ...ext import http
7+
from ...contrib import func_name
108

119
# 3p
1210
from django.apps import apps
13-
from django.conf import settings
11+
from django.core.exceptions import MiddlewareNotUsed
1412

1513

1614
log = logging.getLogger(__name__)
1715

1816

1917
class TraceMiddleware(object):
18+
"""
19+
Middleware that traces Django requests
20+
"""
2021
def __init__(self):
21-
# TODO[manu]: maybe we can formalize better DJANGO_SETTINGS_* stuff
22-
# providing defaults or raise ImproperlyConfigured errors
23-
tracer_import = getattr(settings, 'DATADOG_TRACER', 'ddtrace.tracer')
24-
self.tracer = import_from_string(tracer_import, 'DATADOG_TRACER')
25-
self.service = getattr(settings, 'DATADOG_SERVICE', 'django')
26-
27-
self.tracer.set_service_info(
28-
service=self.service,
29-
app='django',
30-
app_type=AppTypes.web,
31-
)
32-
33-
try:
34-
# TODO[manu]: maybe it's better to provide a Django app that
35-
# will patch everything once instead of trying that for
36-
# each request (in the case of patch_db)?
37-
patch_template(self.tracer)
38-
except Exception:
39-
log.exception("error patching template class")
22+
# disable the middleware if the tracer is not enabled
23+
if not settings.ENABLED:
24+
raise MiddlewareNotUsed
4025

4126
def process_request(self, request):
42-
try:
43-
patch_db(self.tracer) # ensure that connections are always patched.
27+
tracer = settings.TRACER
4428

45-
span = self.tracer.trace(
46-
"django.request",
47-
service=self.service,
48-
resource="unknown", # will be filled by process view
29+
try:
30+
span = tracer.trace(
31+
'django.request',
32+
service=settings.DEFAULT_SERVICE,
33+
resource='unknown', # will be filled by process view
4934
span_type=http.TYPE,
5035
)
5136

5237
span.set_tag(http.METHOD, request.method)
5338
span.set_tag(http.URL, request.path)
5439
_set_req_span(request, span)
5540
except Exception:
56-
log.exception("error tracing request")
41+
log.exception('error tracing request')
5742

5843
def process_view(self, request, view_func, *args, **kwargs):
5944
span = _get_req_span(request)

ddtrace/contrib/django/settings.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

tests/contrib/django/app/settings.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
USE_I18N = True
2121
USE_L10N = True
2222
STATIC_URL = '/static/'
23-
ROOT_URLCONF = 'app.views'
23+
ROOT_URLCONF = 'tests.contrib.django.app.views'
2424

2525
TEMPLATES = [
2626
{
@@ -41,6 +41,9 @@
4141
]
4242

4343
MIDDLEWARE_CLASSES = [
44+
# tracer middleware
45+
'ddtrace.contrib.django.TraceMiddleware',
46+
4447
'django.contrib.sessions.middleware.SessionMiddleware',
4548
'django.middleware.common.CommonMiddleware',
4649
'django.middleware.csrf.CsrfViewMiddleware',
@@ -56,4 +59,13 @@
5659
'django.contrib.auth',
5760
'django.contrib.contenttypes',
5861
'django.contrib.sessions',
62+
63+
# tracer app
64+
'ddtrace.contrib.django',
5965
]
66+
67+
DATADOG_TRACE = {
68+
# tracer with a DummyWriter
69+
'TRACER': 'tests.contrib.django.utils.tracer',
70+
'ENABLED': True,
71+
}

tests/contrib/django/runtests.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,16 @@
44

55

66
if __name__ == "__main__":
7+
# define django defaults
78
app_to_test = "tests/contrib/django"
89
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
910

11+
# append the project root to the PYTHONPATH:
12+
# this is required because we don't want to put the current file
13+
# in the project_root
14+
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
15+
project_root = os.path.join(current_dir, '..', '..')
16+
sys.path.append(project_root)
17+
1018
from django.core.management import execute_from_command_line
1119
execute_from_command_line([sys.argv[0], "test", app_to_test])

0 commit comments

Comments
 (0)