Skip to content

Commit 7a40c96

Browse files
committed
fix(django): trace mro chain for Django views (#1625)
1 parent 1a07ccc commit 7a40c96

File tree

7 files changed

+134
-14
lines changed

7 files changed

+134
-14
lines changed

CHANGELOG.md

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## 0.41.2 (25/08/2020)
4+
5+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
6+
7+
---
8+
39
## 0.41.1 (25/08/2020)
410

511
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
@@ -27,8 +33,17 @@
2733

2834
https://github.com/DataDog/dd-trace-py/compare/v0.40.0...v0.41.0
2935

36+
3037
---
3138

39+
40+
## 0.40.2 (25/08/2020)
41+
42+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
43+
44+
---
45+
46+
3247
## 0.40.1 (25/08/2020)
3348

3449
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
@@ -49,7 +64,7 @@ This release includes performance improvements in the core library, updates prof
4964

5065
* Use `ddtrace.config.redis["service"]` to set the service name for the `redis` integration. The environment variable `DD_REDIS_SERVICE` can also be used.
5166

52-
## httplib
67+
## httplib
5368

5469
* To enable distributed tracing, use `ddtrace.config.httplib["distributed_tracing"]`. By default, distributed tracing for `httplib` is disabled.
5570

@@ -83,6 +98,12 @@ Read the [full changeset](https://github.com/DataDog/dd-trace-py/compare/v0.39.0
8398

8499
---
85100

101+
## 0.39.2 (25/08/2020)
102+
103+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
104+
105+
---
106+
86107
## 0.39.1 (25/08/2020)
87108

88109
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
@@ -180,6 +201,13 @@ https://github.com/DataDog/dd-trace-py/milestone/57?closed=1
180201

181202
---
182203

204+
205+
## 0.38.4 (25/08/2020)
206+
207+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
208+
209+
---
210+
183211
## 0.38.3 (25/08/2020)
184212

185213
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
@@ -268,6 +296,12 @@ version. (#1468)
268296

269297
---
270298

299+
## 0.37.3 (25/08/2020)
300+
301+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
302+
303+
---
304+
271305
## 0.37.2 (25/08/2020)
272306

273307
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
@@ -343,6 +377,12 @@ New environment variables have been added to allow you to easily configure and d
343377

344378
---
345379

380+
## 0.36.3 (25/08/2020)
381+
382+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
383+
384+
---
385+
346386
## 0.36.2 (25/08/2020)
347387

348388
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
@@ -431,7 +471,7 @@ If you are using the profiler, please note that `ddtrace.profile` has been renam
431471

432472
## 0.35.1 (25/08/2020)
433473

434-
- reintroduce wrapt for patching Django view methods. ([#1622](https://github.com/DataDog/dd-trace-py/pull/1622))
474+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
435475

436476
---
437477

@@ -495,6 +535,12 @@ This release adds:
495535
Read the [full changeset](https://github.com/DataDog/dd-trace-py/compare/v0.34.1...v0.35.0).
496536
---
497537

538+
## 0.34.2 (25/08/2020)
539+
540+
- Fix for an issue introduced by patching classes in the MRO of a Django View class (#1625).
541+
542+
---
543+
498544
## 0.34.1 (09/03/2020)
499545
## Changes
500546

ddtrace/contrib/django/patch.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99
import sys
1010

11-
from inspect import isclass, isfunction
11+
from inspect import isclass, isfunction, getmro
1212

1313
from ddtrace import config, Pin
1414
from ddtrace.vendor import debtcollector, six, wrapt
@@ -476,17 +476,31 @@ def traced_template_render(django, pin, wrapped, instance, args, kwargs):
476476

477477

478478
def instrument_view(django, view):
479+
"""
480+
Helper to wrap Django views.
481+
482+
We want to wrap all lifecycle/http method functions for every class in the MRO for this view
483+
"""
484+
if isfunction(view):
485+
return _instrument_view(django, view)
486+
487+
for cls in reversed(getmro(view)):
488+
_instrument_view(django, cls)
489+
490+
return view
491+
492+
493+
def _instrument_view(django, view):
479494
"""Helper to wrap Django views."""
480495
# All views should be callable, double check before doing anything
481-
if not callable(view) or isinstance(view, wrapt.ObjectProxy):
496+
if not callable(view):
482497
return view
483498

484499
# Patch view HTTP methods and lifecycle methods
485500
http_method_names = getattr(view, "http_method_names", ("get", "delete", "post", "options", "head"))
486501
lifecycle_methods = ("setup", "dispatch", "http_method_not_allowed")
487502
for name in list(http_method_names) + list(lifecycle_methods):
488503
try:
489-
# View methods can be staticmethods
490504
func = getattr(view, name, None)
491505
if not func or isinstance(func, wrapt.ObjectProxy):
492506
continue
@@ -514,8 +528,10 @@ def instrument_view(django, view):
514528
except Exception:
515529
log.debug("Failed to instrument Django response %r function %s", response_cls, name, exc_info=True)
516530

517-
# Return a wrapped version of this view
518-
return wrapt.FunctionWrapper(view, traced_func(django, "django.view", resource=func_name(view)))
531+
# If the view itself is not wrapped, wrap it
532+
if not isinstance(view, wrapt.ObjectProxy):
533+
view = wrapt.FunctionWrapper(view, traced_func(django, "django.view", resource=func_name(view)))
534+
return view
519535

520536

521537
@with_traced_module

tests/contrib/django/django1_app/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.conf.urls import url
2+
from django.views.generic import TemplateView
23
from django.views.decorators.cache import cache_page
34

45
from .. import views
@@ -20,6 +21,9 @@
2021
url(r"^template-view/$", views.template_view, name="template-view"),
2122
url(r"^template-simple-view/$", views.template_simple_view, name="template-simple-view"),
2223
url(r"^template-list-view/$", views.template_list_view, name="template-list-view"),
24+
# This must precede composed tests.
25+
url(r"some-static-view/", TemplateView.as_view(template_name="my-template.html")),
2326
url(r"^composed-template-view/$", views.ComposedTemplateView.as_view(), name="composed-template-view"),
2427
url(r"^composed-get-view/$", views.ComposedGetView.as_view(), name="composed-get-view"),
28+
url(r"^composed-view/$", views.ComposedView.as_view(), name="composed-view"),
2529
]

tests/contrib/django/django_app/urls.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from django.contrib.auth import login
33
from django.contrib.auth.models import User
44
from django.http import HttpResponse
5+
from django.views.generic import TemplateView
56
from django.views.decorators.cache import cache_page
67
from django.urls import include, path, re_path
78

@@ -49,6 +50,9 @@ def authenticated_view(request):
4950
re_path(r"re-path.*/", repath_view),
5051
path("path/", path_view),
5152
path("include/", include("tests.contrib.django.django_app.extra_urls")),
53+
# This must precede composed-view.
54+
url(r"^some-static-view/$", TemplateView.as_view(template_name="my-template.html")),
5255
url(r"^composed-template-view/$", views.ComposedTemplateView.as_view(), name="composed-template-view"),
5356
url(r"^composed-get-view/$", views.ComposedGetView.as_view(), name="composed-get-view"),
57+
url(r"^composed-view/$", views.ComposedView.as_view(), name="composed-view"),
5458
]

tests/contrib/django/test_django.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import django
2+
from django.views.generic import TemplateView
23
from django.test import modify_settings, override_settings
34
import os
45
import pytest
56

67
from ddtrace import config
78
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY, SAMPLING_PRIORITY_KEY
9+
from ddtrace.contrib.django.patch import instrument_view
810
from ddtrace.ext import http, errors
911
from ddtrace.ext.priority import USER_KEEP
1012
from ddtrace.propagation.http import HTTP_HEADER_TRACE_ID, HTTP_HEADER_PARENT_ID, HTTP_HEADER_SAMPLING_PRIORITY
@@ -581,7 +583,7 @@ def test_simple_view_options(client, test_spans):
581583
assert len(spans) == 1
582584
span = spans[0]
583585
span.assert_matches(
584-
resource="tests.contrib.django.views.BasicView.options", error=0,
586+
resource="django.views.generic.base.View.options", error=0,
585587
)
586588

587589

@@ -1377,7 +1379,9 @@ def test_custom_dispatch_template_view(client, test_spans):
13771379

13781380
spans = test_spans.get_spans()
13791381
assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
1380-
"tests.contrib.django.views.ComposedTemplateView.dispatch",
1382+
"tests.contrib.django.views.CustomDispatchMixin.dispatch",
1383+
"tests.contrib.django.views.AnotherCustomDispatchMixin.dispatch",
1384+
"django.views.generic.base.View.dispatch",
13811385
]
13821386

13831387

@@ -1392,8 +1396,40 @@ def test_custom_dispatch_get_view(client, test_spans):
13921396

13931397
spans = test_spans.get_spans()
13941398
assert [s.resource for s in spans if s.resource.endswith("dispatch")] == [
1395-
"tests.contrib.django.views.ComposedGetView.dispatch",
1399+
"tests.contrib.django.views.CustomDispatchMixin.dispatch",
1400+
"django.views.generic.base.View.dispatch",
13961401
]
13971402
assert [s.resource for s in spans if s.resource.endswith("get")] == [
13981403
"tests.contrib.django.views.ComposedGetView.get",
1404+
"tests.contrib.django.views.CustomGetView.get",
13991405
]
1406+
1407+
1408+
def test_view_mixin(client, test_spans):
1409+
from tests.contrib.django import views
1410+
1411+
assert views.DISPATCH_CALLED is False
1412+
resp = client.get("/composed-view/")
1413+
assert views.DISPATCH_CALLED is True
1414+
1415+
assert resp.status_code == 200
1416+
assert resp.content.strip() == b"custom dispatch"
1417+
1418+
1419+
def test_template_view_patching():
1420+
"""
1421+
Test to ensure that patching a view does not give it properties it did not have before
1422+
"""
1423+
# DEV: `vars(cls)` will give you only the properties defined on that class,
1424+
# it will not traverse through __mro__ to find the property from a parent class
1425+
1426+
# We are not starting with a "dispatch" property
1427+
assert "dispatch" not in vars(TemplateView)
1428+
1429+
# Manually call internal method for patching
1430+
instrument_view(django, TemplateView)
1431+
assert "dispatch" not in vars(TemplateView)
1432+
1433+
# Patch via `as_view()`
1434+
TemplateView.as_view()
1435+
assert "dispatch" not in vars(TemplateView)

tests/contrib/django/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,3 +142,17 @@ def get(self, request):
142142
if self.dispatch_call_counter == 1:
143143
return super(ComposedGetView, self).get(request)
144144
raise Exception("Custom dispatch not called.")
145+
146+
147+
DISPATCH_CALLED = False
148+
149+
150+
class CustomDispatchView(View):
151+
def dispatch(self, request):
152+
global DISPATCH_CALLED
153+
DISPATCH_CALLED = True
154+
return super(CustomDispatchView, self).dispatch(request)
155+
156+
157+
class ComposedView(TemplateView, CustomDispatchView):
158+
template_name = "custom_dispatch.html"

tests/contrib/djangorestframework/test_djangorestframework.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ def test_trace_exceptions(client, test_spans): # noqa flake8 complains about sh
2323
assert_span_http_status_code(sp, 500)
2424
assert sp.get_tag("http.method") == "GET"
2525

26-
# the DRF integration should set the traceback on the django.view span
26+
# the DRF integration should set the traceback on the django.view.dispatch span
2727
# (as it's the current span when the exception info is set)
28-
view_spans = list(test_spans.filter_spans(name="django.view"))
29-
assert len(view_spans) == 1
30-
err_span = view_spans[0]
28+
view_dispatch_spans = list(test_spans.filter_spans(name="django.view.dispatch"))
29+
assert len(view_dispatch_spans) == 1
30+
err_span = view_dispatch_spans[0]
3131
assert err_span.error == 1
3232
assert err_span.get_tag("error.msg") == "Authentication credentials were not provided."
3333
assert "NotAuthenticated" in err_span.get_tag("error.stack")

0 commit comments

Comments
 (0)