Skip to content

Commit 6a4bc2b

Browse files
authored
feat: Spans for Django middleware calls (#498)
* feat: Spans for Django middleware calls * fix: Work under Django 1.6 * test: Add tests for django middlewares
1 parent ab3de3a commit 6a4bc2b

File tree

3 files changed

+160
-2
lines changed

3 files changed

+160
-2
lines changed

sentry_sdk/integrations/django/__init__.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from sentry_sdk.integrations._wsgi_common import RequestExtractor
4949
from sentry_sdk.integrations.django.transactions import LEGACY_RESOLVER
5050
from sentry_sdk.integrations.django.templates import get_template_frame_from_exception
51+
from sentry_sdk.integrations.django.middleware import patch_django_middlewares
5152

5253

5354
if DJANGO_VERSION < (1, 10):
@@ -68,16 +69,18 @@ class DjangoIntegration(Integration):
6869
identifier = "django"
6970

7071
transaction_style = None
72+
middleware_spans = None
7173

72-
def __init__(self, transaction_style="url"):
73-
# type: (str) -> None
74+
def __init__(self, transaction_style="url", middleware_spans=True):
75+
# type: (str, bool) -> None
7476
TRANSACTION_STYLE_VALUES = ("function_name", "url")
7577
if transaction_style not in TRANSACTION_STYLE_VALUES:
7678
raise ValueError(
7779
"Invalid value for transaction_style: %s (must be in %s)"
7880
% (transaction_style, TRANSACTION_STYLE_VALUES)
7981
)
8082
self.transaction_style = transaction_style
83+
self.middleware_spans = middleware_spans
8184

8285
@staticmethod
8386
def setup_once():
@@ -208,6 +211,8 @@ def _django_queryset_repr(value, hint):
208211
id(value),
209212
)
210213

214+
patch_django_middlewares()
215+
211216

212217
_DRF_PATCHED = False
213218
_DRF_PATCH_LOCK = threading.Lock()
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
"""
2+
Create spans from Django middleware invocations
3+
"""
4+
5+
from functools import wraps
6+
7+
from django import VERSION as DJANGO_VERSION # type: ignore
8+
9+
from sentry_sdk import Hub
10+
from sentry_sdk.utils import ContextVar, transaction_from_function
11+
12+
_import_string_should_wrap_middleware = ContextVar(
13+
"import_string_should_wrap_middleware"
14+
)
15+
16+
if DJANGO_VERSION < (1, 7):
17+
import_string_name = "import_by_path"
18+
else:
19+
import_string_name = "import_string"
20+
21+
22+
def patch_django_middlewares():
23+
from django.core.handlers import base
24+
25+
old_import_string = getattr(base, import_string_name)
26+
27+
def sentry_patched_import_string(dotted_path):
28+
rv = old_import_string(dotted_path)
29+
30+
if _import_string_should_wrap_middleware.get(None):
31+
rv = _wrap_middleware(rv, dotted_path)
32+
33+
return rv
34+
35+
setattr(base, import_string_name, sentry_patched_import_string)
36+
37+
old_load_middleware = base.BaseHandler.load_middleware
38+
39+
def sentry_patched_load_middleware(self):
40+
_import_string_should_wrap_middleware.set(True)
41+
try:
42+
return old_load_middleware(self)
43+
finally:
44+
_import_string_should_wrap_middleware.set(False)
45+
46+
base.BaseHandler.load_middleware = sentry_patched_load_middleware
47+
48+
49+
def _wrap_middleware(middleware, middleware_name):
50+
from sentry_sdk.integrations.django import DjangoIntegration
51+
52+
def _get_wrapped_method(old_method):
53+
@wraps(old_method)
54+
def sentry_wrapped_method(*args, **kwargs):
55+
hub = Hub.current
56+
integration = hub.get_integration(DjangoIntegration)
57+
if integration is None or not integration.middleware_spans:
58+
return old_method(*args, **kwargs)
59+
60+
function_name = transaction_from_function(old_method)
61+
62+
description = middleware_name
63+
function_basename = getattr(old_method, "__name__", None)
64+
if function_basename:
65+
description = "{}.{}".format(description, function_basename)
66+
67+
with hub.start_span(
68+
op="django.middleware", description=description
69+
) as span:
70+
span.set_tag("django.function_name", function_name)
71+
span.set_tag("django.middleware_name", middleware_name)
72+
return old_method(*args, **kwargs)
73+
74+
return sentry_wrapped_method
75+
76+
class SentryWrappingMiddleware(object):
77+
def __init__(self, *args, **kwargs):
78+
self._inner = middleware(*args, **kwargs)
79+
self._call_method = None
80+
81+
# We need correct behavior for `hasattr()`, which we can only determine
82+
# when we have an instance of the middleware we're wrapping.
83+
def __getattr__(self, method_name):
84+
if method_name not in (
85+
"process_request",
86+
"process_view",
87+
"process_template_response",
88+
"process_response",
89+
"process_exception",
90+
):
91+
raise AttributeError()
92+
93+
old_method = getattr(self._inner, method_name)
94+
rv = _get_wrapped_method(old_method)
95+
self.__dict__[method_name] = rv
96+
return rv
97+
98+
def __call__(self, *args, **kwargs):
99+
if self._call_method is None:
100+
self._call_method = _get_wrapped_method(self._inner.__call__)
101+
return self._call_method(*args, **kwargs)
102+
103+
if hasattr(middleware, "__name__"):
104+
SentryWrappingMiddleware.__name__ = middleware.__name__
105+
106+
return SentryWrappingMiddleware

tests/integrations/django/test_basic.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import json
33

44
from werkzeug.test import Client
5+
from django import VERSION as DJANGO_VERSION
56
from django.contrib.auth.models import User
67
from django.core.management import execute_from_command_line
78
from django.db.utils import OperationalError, ProgrammingError, DataError
@@ -495,3 +496,49 @@ def test_does_not_capture_403(sentry_init, client, capture_events, endpoint):
495496
assert status.lower() == "403 forbidden"
496497

497498
assert not events
499+
500+
501+
def test_middleware_spans(sentry_init, client, capture_events):
502+
sentry_init(integrations=[DjangoIntegration()], traces_sample_rate=1.0)
503+
events = capture_events()
504+
505+
_content, status, _headers = client.get(reverse("message"))
506+
507+
message, transaction = events
508+
509+
assert message["message"] == "hi"
510+
511+
for middleware in transaction["spans"]:
512+
assert middleware["op"] == "django.middleware"
513+
514+
if DJANGO_VERSION >= (1, 10):
515+
reference_value = [
516+
"tests.integrations.django.myapp.settings.TestMiddleware.__call__",
517+
"django.contrib.auth.middleware.AuthenticationMiddleware.__call__",
518+
"django.contrib.sessions.middleware.SessionMiddleware.__call__",
519+
]
520+
else:
521+
reference_value = [
522+
"django.contrib.sessions.middleware.SessionMiddleware.process_request",
523+
"django.contrib.auth.middleware.AuthenticationMiddleware.process_request",
524+
"tests.integrations.django.myapp.settings.TestMiddleware.process_request",
525+
"tests.integrations.django.myapp.settings.TestMiddleware.process_response",
526+
"django.contrib.sessions.middleware.SessionMiddleware.process_response",
527+
]
528+
529+
assert [t["description"] for t in transaction["spans"]] == reference_value
530+
531+
532+
def test_middleware_spans_disabled(sentry_init, client, capture_events):
533+
sentry_init(
534+
integrations=[DjangoIntegration(middleware_spans=False)], traces_sample_rate=1.0
535+
)
536+
events = capture_events()
537+
538+
_content, status, _headers = client.get(reverse("message"))
539+
540+
message, transaction = events
541+
542+
assert message["message"] == "hi"
543+
544+
assert not transaction["spans"]

0 commit comments

Comments
 (0)