Skip to content

Commit 5105820

Browse files
authored
Add Django ASGI support (#391)
1 parent 36275f3 commit 5105820

File tree

8 files changed

+515
-28
lines changed

8 files changed

+515
-28
lines changed

CHANGELOG.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3030
([#706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/706))
3131
- `opentelemetry-instrumentation-requests` added exclude urls functionality
3232
([#714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/714))
33+
- `opentelemetry-instrumentation-django` Add ASGI support
34+
([#391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/391))
3335

3436
### Changed
3537
- `opentelemetry-instrumentation-botocore` Make common span attributes compliant with semantic conventions
@@ -64,12 +66,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6466
### Added
6567
- `opentelemetry-sdk-extension-aws` Add AWS resource detectors to extension package
6668
([#586](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/586))
67-
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`,
68-
`opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks
69+
- `opentelemetry-instrumentation-asgi`, `opentelemetry-instrumentation-aiohttp-client`, `openetelemetry-instrumentation-fastapi`,
70+
`opentelemetry-instrumentation-starlette`, `opentelemetry-instrumentation-urllib`, `opentelemetry-instrumentation-urllib3` Added `request_hook` and `response_hook` callbacks
6971
([#576](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/576))
7072
- `opentelemetry-instrumentation-pika` added RabbitMQ's pika module instrumentation.
7173
([#680](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/680))
72-
74+
7375
### Changed
7476

7577
- `opentelemetry-instrumentation-fastapi` Allow instrumentation of newer FastAPI versions.

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def get_host_port_url_tuple(scope):
121121
"""Returns (host, port, full_url) tuple."""
122122
server = scope.get("server") or ["0.0.0.0", 80]
123123
port = server[1]
124-
server_host = server[0] + (":" + str(port) if port != 80 else "")
124+
server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
125125
full_path = scope.get("root_path", "") + scope.get("path", "")
126126
http_url = scope.get("scheme", "http") + "://" + server_host + full_path
127127
return server_host, port, http_url

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.24b0
4646

4747
[options.extras_require]
48+
asgi =
49+
opentelemetry-instrumentation-asgi == 0.24b0
4850
test =
4951
opentelemetry-test == 0.24b0
5052

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

+70-11
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import types
1516
from logging import getLogger
1617
from time import time
1718
from typing import Callable
@@ -24,11 +25,11 @@
2425
get_global_response_propagator,
2526
)
2627
from opentelemetry.instrumentation.utils import extract_attributes_from_object
28+
from opentelemetry.instrumentation.wsgi import add_response_attributes
2729
from opentelemetry.instrumentation.wsgi import (
28-
add_response_attributes,
29-
collect_request_attributes,
30-
wsgi_getter,
30+
collect_request_attributes as wsgi_collect_request_attributes,
3131
)
32+
from opentelemetry.instrumentation.wsgi import wsgi_getter
3233
from opentelemetry.propagate import extract
3334
from opentelemetry.semconv.trace import SpanAttributes
3435
from opentelemetry.trace import Span, SpanKind, use_span
@@ -43,6 +44,7 @@
4344
from django.urls import Resolver404, resolve
4445

4546
DJANGO_2_0 = django_version >= (2, 0)
47+
DJANGO_3_0 = django_version >= (3, 0)
4648

4749
if DJANGO_2_0:
4850
# Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style
@@ -67,6 +69,26 @@ def __call__(self, request):
6769
except ImportError:
6870
MiddlewareMixin = object
6971

72+
if DJANGO_3_0:
73+
from django.core.handlers.asgi import ASGIRequest
74+
else:
75+
ASGIRequest = None
76+
77+
# try/except block exclusive for optional ASGI imports.
78+
try:
79+
from opentelemetry.instrumentation.asgi import asgi_getter
80+
from opentelemetry.instrumentation.asgi import (
81+
collect_request_attributes as asgi_collect_request_attributes,
82+
)
83+
from opentelemetry.instrumentation.asgi import set_status_code
84+
85+
_is_asgi_supported = True
86+
except ImportError:
87+
asgi_getter = None
88+
asgi_collect_request_attributes = None
89+
set_status_code = None
90+
_is_asgi_supported = False
91+
7092

7193
_logger = getLogger(__name__)
7294
_attributes_by_preference = [
@@ -91,6 +113,10 @@ def __call__(self, request):
91113
]
92114

93115

116+
def _is_asgi_request(request: HttpRequest) -> bool:
117+
return ASGIRequest is not None and isinstance(request, ASGIRequest)
118+
119+
94120
class _DjangoMiddleware(MiddlewareMixin):
95121
"""Django Middleware for OpenTelemetry"""
96122

@@ -140,12 +166,25 @@ def process_request(self, request):
140166
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
141167
return
142168

169+
is_asgi_request = _is_asgi_request(request)
170+
if not _is_asgi_supported and is_asgi_request:
171+
return
172+
143173
# pylint:disable=W0212
144174
request._otel_start_time = time()
145175

146176
request_meta = request.META
147177

148-
token = attach(extract(request_meta, getter=wsgi_getter))
178+
if is_asgi_request:
179+
carrier = request.scope
180+
carrier_getter = asgi_getter
181+
collect_request_attributes = asgi_collect_request_attributes
182+
else:
183+
carrier = request_meta
184+
carrier_getter = wsgi_getter
185+
collect_request_attributes = wsgi_collect_request_attributes
186+
187+
token = attach(extract(request_meta, getter=carrier_getter))
149188

150189
span = self._tracer.start_span(
151190
self._get_span_name(request),
@@ -155,12 +194,25 @@ def process_request(self, request):
155194
),
156195
)
157196

158-
attributes = collect_request_attributes(request_meta)
197+
attributes = collect_request_attributes(carrier)
159198

160199
if span.is_recording():
161200
attributes = extract_attributes_from_object(
162201
request, self._traced_request_attrs, attributes
163202
)
203+
if is_asgi_request:
204+
# ASGI requests include extra attributes in request.scope.headers.
205+
attributes = extract_attributes_from_object(
206+
types.SimpleNamespace(
207+
**{
208+
name.decode("latin1"): value.decode("latin1")
209+
for name, value in request.scope.get("headers", [])
210+
}
211+
),
212+
self._traced_request_attrs,
213+
attributes,
214+
)
215+
164216
for key, value in attributes.items():
165217
span.set_attribute(key, value)
166218

@@ -207,15 +259,22 @@ def process_response(self, request, response):
207259
if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
208260
return response
209261

262+
is_asgi_request = _is_asgi_request(request)
263+
if not _is_asgi_supported and is_asgi_request:
264+
return response
265+
210266
activation = request.META.pop(self._environ_activation_key, None)
211267
span = request.META.pop(self._environ_span_key, None)
212268

213269
if activation and span:
214-
add_response_attributes(
215-
span,
216-
f"{response.status_code} {response.reason_phrase}",
217-
response,
218-
)
270+
if is_asgi_request:
271+
set_status_code(span, response.status_code)
272+
else:
273+
add_response_attributes(
274+
span,
275+
f"{response.status_code} {response.reason_phrase}",
276+
response,
277+
)
219278

220279
propagator = get_global_response_propagator()
221280
if propagator:
@@ -238,7 +297,7 @@ def process_response(self, request, response):
238297
activation.__exit__(None, None, None)
239298

240299
if self._environ_token in request.META.keys():
241-
detach(request.environ.get(self._environ_token))
300+
detach(request.META.get(self._environ_token))
242301
request.META.pop(self._environ_token)
243302

244303
return response

instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py

+22-12
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@
1515
from sys import modules
1616
from unittest.mock import Mock, patch
1717

18-
from django import VERSION
19-
from django.conf import settings
20-
from django.conf.urls import url
18+
from django import VERSION, conf
2119
from django.http import HttpRequest, HttpResponse
22-
from django.test import Client
20+
from django.test.client import Client
2321
from django.test.utils import setup_test_environment, teardown_test_environment
22+
from django.urls import re_path
2423

2524
from opentelemetry.instrumentation.django import (
2625
DjangoInstrumentor,
@@ -57,22 +56,22 @@
5756
DJANGO_2_2 = VERSION >= (2, 2)
5857

5958
urlpatterns = [
60-
url(r"^traced/", traced),
61-
url(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
62-
url(r"^error/", error),
63-
url(r"^excluded_arg/", excluded),
64-
url(r"^excluded_noarg/", excluded_noarg),
65-
url(r"^excluded_noarg2/", excluded_noarg2),
66-
url(r"^span_name/([0-9]{4})/$", route_span_name),
59+
re_path(r"^traced/", traced),
60+
re_path(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
61+
re_path(r"^error/", error),
62+
re_path(r"^excluded_arg/", excluded),
63+
re_path(r"^excluded_noarg/", excluded_noarg),
64+
re_path(r"^excluded_noarg2/", excluded_noarg2),
65+
re_path(r"^span_name/([0-9]{4})/$", route_span_name),
6766
]
6867
_django_instrumentor = DjangoInstrumentor()
6968

7069

7170
class TestMiddleware(TestBase, WsgiTestBase):
7271
@classmethod
7372
def setUpClass(cls):
73+
conf.settings.configure(ROOT_URLCONF=modules[__name__])
7474
super().setUpClass()
75-
settings.configure(ROOT_URLCONF=modules[__name__])
7675

7776
def setUp(self):
7877
super().setUp()
@@ -105,6 +104,11 @@ def tearDown(self):
105104
teardown_test_environment()
106105
_django_instrumentor.uninstrument()
107106

107+
@classmethod
108+
def tearDownClass(cls):
109+
super().tearDownClass()
110+
conf.settings = conf.LazySettings()
111+
108112
def test_templated_route_get(self):
109113
Client().get("/route/2020/template/")
110114

@@ -357,6 +361,7 @@ def test_trace_response_headers(self):
357361
class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
358362
@classmethod
359363
def setUpClass(cls):
364+
conf.settings.configure(ROOT_URLCONF=modules[__name__])
360365
super().setUpClass()
361366

362367
def setUp(self):
@@ -375,6 +380,11 @@ def tearDown(self):
375380
teardown_test_environment()
376381
_django_instrumentor.uninstrument()
377382

383+
@classmethod
384+
def tearDownClass(cls):
385+
super().tearDownClass()
386+
conf.settings = conf.LazySettings()
387+
378388
def test_tracer_provider_traced(self):
379389
Client().post("/traced/")
380390

0 commit comments

Comments
 (0)