Skip to content

Commit e314030

Browse files
committed
add instrument_middleware
1 parent 7f01b35 commit e314030

2 files changed

Lines changed: 249 additions & 0 deletions

File tree

instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/__init__.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,21 @@ def response_hook(span, request, response):
227227
Note:
228228
The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change.
229229
230+
Middleware spans
231+
****************
232+
You can optionally enable per-middleware span creation to trace the execution
233+
of each Django middleware individually:
234+
235+
.. code:: python
236+
237+
DjangoInstrumentor().instrument(is_middleware_spans_enabled=True)
238+
239+
When enabled, each middleware in ``settings.MIDDLEWARE`` produces an INTERNAL span
240+
nested under the HTTP server span.
241+
242+
Warning:
243+
Middleware added to ``settings.MIDDLEWARE`` after calling ``instrument()`` or before the ``middleware_position`` will not be automatically traced.
244+
230245
SQLCommenter
231246
************
232247
You can optionally enable sqlcommenter which enriches the query with contextual
@@ -306,6 +321,10 @@ def response_hook(span, request, response):
306321
from opentelemetry.instrumentation.django.environment_variables import (
307322
OTEL_PYTHON_DJANGO_INSTRUMENT,
308323
)
324+
from opentelemetry.instrumentation.django.instrument_middleware import (
325+
instrument_middleware_classes,
326+
uninstrument_middleware_classes,
327+
)
309328
from opentelemetry.instrumentation.django.middleware.otel_middleware import (
310329
_DjangoMiddleware,
311330
)
@@ -460,11 +479,20 @@ def _instrument(self, **kwargs):
460479
settings_middleware = list(settings_middleware)
461480

462481
is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None)
482+
is_middleware_spans_enabled = kwargs.pop(
483+
"is_middleware_spans_enabled", None
484+
)
463485

464486
middleware_position = _get_django_otel_middleware_position(
465487
len(settings_middleware), kwargs.pop("middleware_position", 0)
466488
)
467489

490+
if is_middleware_spans_enabled:
491+
instrument_middleware_classes(
492+
settings_middleware[middleware_position:],
493+
tracer,
494+
)
495+
468496
if is_sql_commentor_enabled:
469497
settings_middleware.insert(
470498
middleware_position, self._sql_commenter_middleware
@@ -477,6 +505,8 @@ def _instrument(self, **kwargs):
477505
setattr(settings, _django_middleware_setting, settings_middleware)
478506

479507
def _uninstrument(self, **kwargs):
508+
uninstrument_middleware_classes()
509+
480510
settings_middleware = getattr(
481511
settings, _django_middleware_setting, None
482512
)
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Copyright The OpenTelemetry Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
from __future__ import annotations
5+
6+
import inspect
7+
from functools import wraps
8+
from importlib import import_module
9+
from inspect import iscoroutinefunction
10+
from logging import getLogger
11+
from typing import Any
12+
13+
from django.utils.deprecation import MiddlewareMixin
14+
from django.utils.module_loading import import_string
15+
16+
from opentelemetry.instrumentation.utils import unwrap
17+
from opentelemetry.trace import SpanKind, Tracer
18+
19+
try:
20+
from wrapt import wrap_function_wrapper
21+
except ImportError:
22+
pass
23+
24+
_logger = getLogger(__name__)
25+
26+
_wrapped_targets: list[tuple[Any, str]] = []
27+
28+
29+
def instrument_middleware_classes(
30+
settings_middleware: list[str],
31+
tracer: Tracer,
32+
) -> None:
33+
"""Wrap all middleware in settings_middleware with span creation.
34+
35+
Categorizes each middleware and applies the appropriate wrapping:
36+
- MiddlewareMixin subclasses: single class-level wrap on MiddlewareMixin.__call__/__acall__
37+
- Pure new-style classes: per-class wrap on __call__/__acall__
38+
- Function-based factories: wrap the factory to return a traced callable
39+
40+
"""
41+
has_mixin_subclass = False
42+
43+
for mw_path in settings_middleware:
44+
try:
45+
cls_or_func = import_string(mw_path)
46+
except ImportError:
47+
_logger.debug(
48+
"Could not import middleware %s, skipping", mw_path
49+
)
50+
continue
51+
52+
if not inspect.isclass(cls_or_func):
53+
_wrap_function_middleware(mw_path, tracer)
54+
elif issubclass(cls_or_func, MiddlewareMixin):
55+
has_mixin_subclass = True
56+
else:
57+
_wrap_newstyle_middleware(mw_path, cls_or_func, tracer)
58+
59+
if has_mixin_subclass:
60+
_wrap_middleware_mixin(tracer)
61+
62+
63+
def uninstrument_middleware_classes() -> None:
64+
"""Remove all middleware span wrappers applied by instrument_middleware_classes."""
65+
for obj, attr in _wrapped_targets:
66+
unwrap(obj, attr)
67+
_wrapped_targets.clear()
68+
69+
70+
# -- Category 1: MiddlewareMixin-based middleware --
71+
72+
73+
def _wrap_middleware_mixin(tracer: Tracer) -> None:
74+
wrap_function_wrapper(
75+
"django.utils.deprecation",
76+
"MiddlewareMixin.__call__",
77+
_make_mixin_call_wrapper(tracer),
78+
)
79+
_wrapped_targets.append((MiddlewareMixin, "__call__"))
80+
81+
if hasattr(MiddlewareMixin, "__acall__"):
82+
wrap_function_wrapper(
83+
"django.utils.deprecation",
84+
"MiddlewareMixin.__acall__",
85+
_make_mixin_acall_wrapper(tracer),
86+
)
87+
_wrapped_targets.append((MiddlewareMixin, "__acall__"))
88+
89+
90+
def _make_mixin_call_wrapper(tracer: Tracer):
91+
def _traced_call(wrapped, instance, args, kwargs):
92+
if getattr(instance, "async_mode", False) or getattr(instance, "is_async", False):
93+
return wrapped(*args, **kwargs)
94+
middleware_name = type(instance).__qualname__
95+
with tracer.start_as_current_span(
96+
f"django.middleware {middleware_name}",
97+
kind=SpanKind.INTERNAL,
98+
):
99+
return wrapped(*args, **kwargs)
100+
101+
return _traced_call
102+
103+
104+
def _make_mixin_acall_wrapper(tracer: Tracer):
105+
async def _traced_acall(wrapped, instance, args, kwargs):
106+
middleware_name = type(instance).__qualname__
107+
with tracer.start_as_current_span(
108+
f"django.middleware {middleware_name}",
109+
kind=SpanKind.INTERNAL,
110+
):
111+
return await wrapped(*args, **kwargs)
112+
113+
return _traced_acall
114+
115+
116+
# -- Category 2: Pure new-style class middleware --
117+
118+
119+
def _wrap_newstyle_middleware(
120+
mw_path: str, cls: type, tracer: Tracer
121+
) -> None:
122+
module_path, class_name = mw_path.rsplit(".", 1)
123+
124+
wrap_function_wrapper(
125+
module_path,
126+
f"{class_name}.__call__",
127+
_make_newstyle_call_wrapper(tracer),
128+
)
129+
_wrapped_targets.append((cls, "__call__"))
130+
131+
if hasattr(cls, "__acall__") and "__acall__" in cls.__dict__:
132+
wrap_function_wrapper(
133+
module_path,
134+
f"{class_name}.__acall__",
135+
_make_newstyle_acall_wrapper(tracer),
136+
)
137+
_wrapped_targets.append((cls, "__acall__"))
138+
139+
140+
def _make_newstyle_call_wrapper(tracer: Tracer):
141+
def _traced_call(wrapped, instance, args, kwargs):
142+
middleware_name = type(instance).__qualname__
143+
144+
if iscoroutinefunction(wrapped):
145+
146+
async def _traced():
147+
with tracer.start_as_current_span(
148+
f"django.middleware {middleware_name}",
149+
kind=SpanKind.INTERNAL,
150+
):
151+
return await wrapped(*args, **kwargs)
152+
153+
return _traced()
154+
155+
with tracer.start_as_current_span(
156+
f"django.middleware {middleware_name}",
157+
kind=SpanKind.INTERNAL,
158+
):
159+
return wrapped(*args, **kwargs)
160+
161+
return _traced_call
162+
163+
164+
def _make_newstyle_acall_wrapper(tracer: Tracer):
165+
async def _traced_acall(wrapped, instance, args, kwargs):
166+
middleware_name = type(instance).__qualname__
167+
with tracer.start_as_current_span(
168+
f"django.middleware {middleware_name}",
169+
kind=SpanKind.INTERNAL,
170+
):
171+
return await wrapped(*args, **kwargs)
172+
173+
return _traced_acall
174+
175+
176+
# -- Category 3: Function-based middleware factory --
177+
178+
179+
def _wrap_function_middleware(mw_path: str, tracer: Tracer) -> None:
180+
module_path, func_name = mw_path.rsplit(".", 1)
181+
module = import_module(module_path)
182+
183+
wrap_function_wrapper(
184+
module_path,
185+
func_name,
186+
_make_factory_wrapper(tracer),
187+
)
188+
_wrapped_targets.append((module, func_name))
189+
190+
191+
def _make_factory_wrapper(tracer: Tracer):
192+
def _traced_factory(wrapped, instance, args, kwargs):
193+
inner_callable = wrapped(*args, **kwargs)
194+
middleware_name = wrapped.__qualname__
195+
196+
if iscoroutinefunction(inner_callable):
197+
198+
@wraps(inner_callable)
199+
async def traced_async(request):
200+
with tracer.start_as_current_span(
201+
f"django.middleware {middleware_name}",
202+
kind=SpanKind.INTERNAL,
203+
):
204+
return await inner_callable(request)
205+
206+
return traced_async
207+
else:
208+
209+
@wraps(inner_callable)
210+
def traced_sync(request):
211+
with tracer.start_as_current_span(
212+
f"django.middleware {middleware_name}",
213+
kind=SpanKind.INTERNAL,
214+
):
215+
return inner_callable(request)
216+
217+
return traced_sync
218+
219+
return _traced_factory

0 commit comments

Comments
 (0)