Skip to content

Commit 864320a

Browse files
committed
Add Django ASGI support
This diff adds `asgi` as an extra, and uses its methods if the current request is an `ASGIRequest`. I still need to dig deeper in the current test suite, to find a way to duplicate the tests in `instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py`, but using an [`AsyncClient`](https://docs.djangoproject.com/en/3.1/topics/testing/tools/#testing-asynchronous-code). Fixes open-telemetry#165, open-telemetry#185, open-telemetry#280, open-telemetry#334.
1 parent 3d7cc64 commit 864320a

File tree

3 files changed

+56
-11
lines changed

3 files changed

+56
-11
lines changed

Diff for: instrumentation/opentelemetry-instrumentation-django/setup.cfg

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ install_requires =
4545
opentelemetry-semantic-conventions == 0.23.dev0
4646

4747
[options.extras_require]
48+
asgi =
49+
opentelemetry-instrumentation-asgi == 0.22.dev0
4850
test =
4951
opentelemetry-test == 0.23.dev0
5052

Diff for: instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py

+51-8
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from opentelemetry.instrumentation.utils import extract_attributes_from_object
2727
from opentelemetry.instrumentation.wsgi import (
2828
add_response_attributes,
29-
collect_request_attributes,
29+
collect_request_attributes as wsgi_collect_request_attributes,
3030
wsgi_getter,
3131
)
3232
from opentelemetry.propagate import extract
@@ -47,6 +47,25 @@
4747
except ImportError:
4848
MiddlewareMixin = object
4949

50+
try:
51+
from django.core.handlers.asgi import ASGIRequest
52+
except ImportError:
53+
ASGIRequest = None
54+
55+
try:
56+
from opentelemetry.instrumentation.asgi import (
57+
asgi_getter,
58+
collect_request_attributes as asgi_collect_request_attributes,
59+
set_status_code,
60+
)
61+
_is_asgi_supported = True
62+
except ImportError:
63+
asgi_getter = None
64+
asgi_collect_request_attributes = None
65+
set_status_code = None
66+
_is_asgi_supported = False
67+
68+
5069
_logger = getLogger(__name__)
5170
_attributes_by_preference = [
5271
[
@@ -111,6 +130,9 @@ def _get_span_name(request):
111130
except Resolver404:
112131
return "HTTP {}".format(request.method)
113132

133+
def _is_asgi_request(self, request):
134+
return ASGIRequest and isinstance(request, ASGIRequest)
135+
114136
def process_request(self, request):
115137
# request.META is a dictionary containing all available HTTP headers
116138
# Read more about request.META here:
@@ -119,12 +141,23 @@ def process_request(self, request):
119141
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
120142
return
121143

144+
is_asgi_request = self._is_asgi_request(request)
145+
if is_asgi_request and not _is_asgi_supported:
146+
return
147+
122148
# pylint:disable=W0212
123149
request._otel_start_time = time()
124150

125151
request_meta = request.META
126152

127-
token = attach(extract(request_meta, getter=wsgi_getter))
153+
if is_asgi_request:
154+
carrier_getter = asgi_getter
155+
collect_request_attributes = asgi_collect_request_attributes
156+
else:
157+
carrier_getter = wsgi_getter
158+
collect_request_attributes = wsgi_collect_request_attributes
159+
160+
token = attach(extract(request_meta, getter=carrier_getter))
128161

129162
span = self._tracer.start_span(
130163
self._get_span_name(request),
@@ -186,15 +219,22 @@ def process_response(self, request, response):
186219
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
187220
return response
188221

222+
is_asgi_request = self._is_asgi_request(request)
223+
if is_asgi_request and not _is_asgi_supported:
224+
return
225+
189226
activation = request.META.pop(self._environ_activation_key, None)
190227
span = request.META.pop(self._environ_span_key, None)
191228

192229
if activation and span:
193-
add_response_attributes(
194-
span,
195-
"{} {}".format(response.status_code, response.reason_phrase),
196-
response,
197-
)
230+
if is_asgi_request:
231+
set_status_code(request.META[self._environ_span_key], response.status_code)
232+
else:
233+
add_response_attributes(
234+
span,
235+
"{} {}".format(response.status_code, response.reason_phrase),
236+
response,
237+
)
198238

199239
propagator = get_global_response_propagator()
200240
if propagator:
@@ -217,7 +257,10 @@ def process_response(self, request, response):
217257
activation.__exit__(None, None, None)
218258

219259
if self._environ_token in request.META.keys():
220-
detach(request.environ.get(self._environ_token))
260+
if is_asgi_request:
261+
detach(request.META.get(self._environ_token))
262+
else:
263+
detach(request.environ.get(self._environ_token))
221264
request.META.pop(self._environ_token)
222265

223266
return response

Diff for: tox.ini

+3-3
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ commands_pre =
234234

235235
falcon,flask,django,pyramid,tornado,starlette,fastapi: pip install {toxinidir}/util/opentelemetry-util-http[test]
236236
wsgi,falcon,flask,django,pyramid: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-wsgi[test]
237-
asgi,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
237+
asgi,django,starlette,fastapi: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asgi[test]
238238

239239
asyncpg: pip install {toxinidir}/instrumentation/opentelemetry-instrumentation-asyncpg[test]
240240

@@ -329,7 +329,7 @@ commands =
329329

330330
[testenv:lint]
331331
basepython: python3.9
332-
recreate = False
332+
recreate = False
333333
deps =
334334
-c dev-requirements.txt
335335
flaky
@@ -439,4 +439,4 @@ deps =
439439

440440
commands =
441441
{toxinidir}/scripts/generate_setup.py
442-
{toxinidir}/scripts/generate_instrumentation_bootstrap.py
442+
{toxinidir}/scripts/generate_instrumentation_bootstrap.py

0 commit comments

Comments
 (0)