Repository: open-telemetry/opentelemetry-python-contrib

Issue:
Django ASGI not supported
**Describe your environment**
Python 3.8
Docker(python:3.8 image)
daphne-2.5.0/uvicorn 0.11.8/hypercorn 0.10.2
Django-3.1.0

**Steps to reproduce**
```sh
git clone https://github.com/HiveTraum/opentelemetry-django-asgi-example
cd opentelemetry-django-asgi-example
docker-compose up -d
curl -I 127.0.0.1:8000/api/
curl -I 127.0.0.1:8001/api/
curl -I 127.0.0.1:8002/api/
```

**What is the expected behavior?**
Correct request processing

**What is the actual behavior?**
KeyError when trying to get `wsgi.url_scheme` from environ

**Additional context**

Every wsgi server fill this variable inside request.META, but every asgi doesn't. Actually I don't know precisely to whom this bug can be  addressed. For now opentelemetry-instrumentation-django relies on opentelemetry-instrumentation-wsgi, but ignores opentelemetry-instrumentation-asgi, maybe this can be key to solve this issue

```python
Traceback (most recent call last):
  File "/usr/local/lib/python3.8/site-packages/asgiref/sync.py", line 330, in thread_handler
    raise exc_info[1]
  File "/usr/local/lib/python3.8/site-packages/django/core/handlers/exception.py", line 38, in inner
    response = await get_response(request)
  File "/usr/local/lib/python3.8/site-packages/django/core/handlers/base.py", line 221, in _get_response_async
    response = await middleware_method(request, callback, callback_args, callback_kwargs)
  File "/usr/local/lib/python3.8/site-packages/asgiref/sync.py", line 296, in __call__
    ret = await asyncio.wait_for(future, timeout=None)
  File "/usr/local/lib/python3.8/asyncio/tasks.py", line 455, in wait_for
    return await fut
  File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/usr/local/lib/python3.8/site-packages/asgiref/sync.py", line 334, in thread_handler
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.8/site-packages/opentelemetry/instrumentation/django/middleware.py", line 73, in process_view
    attributes = collect_request_attributes(environ)
  File "/usr/local/lib/python3.8/site-packages/opentelemetry/instrumentation/wsgi/__init__.py", line 111, in collect_request_attributes
    result["http.url"] = wsgiref_util.request_uri(environ)
  File "/usr/local/lib/python3.8/wsgiref/util.py", line 72, in request_uri
    url = application_uri(environ)
  File "/usr/local/lib/python3.8/wsgiref/util.py", line 52, in application_uri
    url = environ['wsgi.url_scheme']+'://'
KeyError: 'wsgi.url_scheme'
```

ASGI support for django instrumentation
Things should be done for adding support for ASGI
1. Collect the request attributes from scope
2. HTTP headers are list of key, value pairs in byte format, so needs to make necessary changes for context propagation


Discussion:
The same error


Resolving patch:
diff --git a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
--- a/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
+++ b/instrumentation/opentelemetry-instrumentation-asgi/src/opentelemetry/instrumentation/asgi/__init__.py
@@ -121,7 +121,7 @@ def get_host_port_url_tuple(scope):
     """Returns (host, port, full_url) tuple."""
     server = scope.get("server") or ["0.0.0.0", 80]
     port = server[1]
-    server_host = server[0] + (":" + str(port) if port != 80 else "")
+    server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
     full_path = scope.get("root_path", "") + scope.get("path", "")
     http_url = scope.get("scheme", "http") + "://" + server_host + full_path
     return server_host, port, http_url
diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py
--- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py
+++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import types
 from logging import getLogger
 from time import time
 from typing import Callable
@@ -24,11 +25,11 @@
     get_global_response_propagator,
 )
 from opentelemetry.instrumentation.utils import extract_attributes_from_object
+from opentelemetry.instrumentation.wsgi import add_response_attributes
 from opentelemetry.instrumentation.wsgi import (
-    add_response_attributes,
-    collect_request_attributes,
-    wsgi_getter,
+    collect_request_attributes as wsgi_collect_request_attributes,
 )
+from opentelemetry.instrumentation.wsgi import wsgi_getter
 from opentelemetry.propagate import extract
 from opentelemetry.semconv.trace import SpanAttributes
 from opentelemetry.trace import Span, SpanKind, use_span
@@ -43,6 +44,7 @@
     from django.urls import Resolver404, resolve
 
 DJANGO_2_0 = django_version >= (2, 0)
+DJANGO_3_0 = django_version >= (3, 0)
 
 if DJANGO_2_0:
     # Since Django 2.0, only `settings.MIDDLEWARE` is supported, so new-style
@@ -67,6 +69,26 @@ def __call__(self, request):
     except ImportError:
         MiddlewareMixin = object
 
+if DJANGO_3_0:
+    from django.core.handlers.asgi import ASGIRequest
+else:
+    ASGIRequest = None
+
+# try/except block exclusive for optional ASGI imports.
+try:
+    from opentelemetry.instrumentation.asgi import asgi_getter
+    from opentelemetry.instrumentation.asgi import (
+        collect_request_attributes as asgi_collect_request_attributes,
+    )
+    from opentelemetry.instrumentation.asgi import set_status_code
+
+    _is_asgi_supported = True
+except ImportError:
+    asgi_getter = None
+    asgi_collect_request_attributes = None
+    set_status_code = None
+    _is_asgi_supported = False
+
 
 _logger = getLogger(__name__)
 _attributes_by_preference = [
@@ -91,6 +113,10 @@ def __call__(self, request):
 ]
 
 
+def _is_asgi_request(request: HttpRequest) -> bool:
+    return ASGIRequest is not None and isinstance(request, ASGIRequest)
+
+
 class _DjangoMiddleware(MiddlewareMixin):
     """Django Middleware for OpenTelemetry"""
 
@@ -140,12 +166,25 @@ def process_request(self, request):
         if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
             return
 
+        is_asgi_request = _is_asgi_request(request)
+        if not _is_asgi_supported and is_asgi_request:
+            return
+
         # pylint:disable=W0212
         request._otel_start_time = time()
 
         request_meta = request.META
 
-        token = attach(extract(request_meta, getter=wsgi_getter))
+        if is_asgi_request:
+            carrier = request.scope
+            carrier_getter = asgi_getter
+            collect_request_attributes = asgi_collect_request_attributes
+        else:
+            carrier = request_meta
+            carrier_getter = wsgi_getter
+            collect_request_attributes = wsgi_collect_request_attributes
+
+        token = attach(extract(request_meta, getter=carrier_getter))
 
         span = self._tracer.start_span(
             self._get_span_name(request),
@@ -155,12 +194,25 @@ def process_request(self, request):
             ),
         )
 
-        attributes = collect_request_attributes(request_meta)
+        attributes = collect_request_attributes(carrier)
 
         if span.is_recording():
             attributes = extract_attributes_from_object(
                 request, self._traced_request_attrs, attributes
             )
+            if is_asgi_request:
+                # ASGI requests include extra attributes in request.scope.headers.
+                attributes = extract_attributes_from_object(
+                    types.SimpleNamespace(
+                        **{
+                            name.decode("latin1"): value.decode("latin1")
+                            for name, value in request.scope.get("headers", [])
+                        }
+                    ),
+                    self._traced_request_attrs,
+                    attributes,
+                )
+
             for key, value in attributes.items():
                 span.set_attribute(key, value)
 
@@ -207,15 +259,22 @@ def process_response(self, request, response):
         if self._excluded_urls.url_disabled(request.build_absolute_uri("?")):
             return response
 
+        is_asgi_request = _is_asgi_request(request)
+        if not _is_asgi_supported and is_asgi_request:
+            return response
+
         activation = request.META.pop(self._environ_activation_key, None)
         span = request.META.pop(self._environ_span_key, None)
 
         if activation and span:
-            add_response_attributes(
-                span,
-                f"{response.status_code} {response.reason_phrase}",
-                response,
-            )
+            if is_asgi_request:
+                set_status_code(span, response.status_code)
+            else:
+                add_response_attributes(
+                    span,
+                    f"{response.status_code} {response.reason_phrase}",
+                    response,
+                )
 
             propagator = get_global_response_propagator()
             if propagator:
@@ -238,7 +297,7 @@ def process_response(self, request, response):
                 activation.__exit__(None, None, None)
 
         if self._environ_token in request.META.keys():
-            detach(request.environ.get(self._environ_token))
+            detach(request.META.get(self._environ_token))
             request.META.pop(self._environ_token)
 
         return response


Test patch:
diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py
--- a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py
+++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware.py
@@ -15,12 +15,11 @@
 from sys import modules
 from unittest.mock import Mock, patch
 
-from django import VERSION
-from django.conf import settings
-from django.conf.urls import url
+from django import VERSION, conf
 from django.http import HttpRequest, HttpResponse
-from django.test import Client
+from django.test.client import Client
 from django.test.utils import setup_test_environment, teardown_test_environment
+from django.urls import re_path
 
 from opentelemetry.instrumentation.django import (
     DjangoInstrumentor,
@@ -57,13 +56,13 @@
 DJANGO_2_2 = VERSION >= (2, 2)
 
 urlpatterns = [
-    url(r"^traced/", traced),
-    url(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
-    url(r"^error/", error),
-    url(r"^excluded_arg/", excluded),
-    url(r"^excluded_noarg/", excluded_noarg),
-    url(r"^excluded_noarg2/", excluded_noarg2),
-    url(r"^span_name/([0-9]{4})/$", route_span_name),
+    re_path(r"^traced/", traced),
+    re_path(r"^route/(?P<year>[0-9]{4})/template/$", traced_template),
+    re_path(r"^error/", error),
+    re_path(r"^excluded_arg/", excluded),
+    re_path(r"^excluded_noarg/", excluded_noarg),
+    re_path(r"^excluded_noarg2/", excluded_noarg2),
+    re_path(r"^span_name/([0-9]{4})/$", route_span_name),
 ]
 _django_instrumentor = DjangoInstrumentor()
 
@@ -71,8 +70,8 @@
 class TestMiddleware(TestBase, WsgiTestBase):
     @classmethod
     def setUpClass(cls):
+        conf.settings.configure(ROOT_URLCONF=modules[__name__])
         super().setUpClass()
-        settings.configure(ROOT_URLCONF=modules[__name__])
 
     def setUp(self):
         super().setUp()
@@ -105,6 +104,11 @@ def tearDown(self):
         teardown_test_environment()
         _django_instrumentor.uninstrument()
 
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        conf.settings = conf.LazySettings()
+
     def test_templated_route_get(self):
         Client().get("/route/2020/template/")
 
@@ -357,6 +361,7 @@ def test_trace_response_headers(self):
 class TestMiddlewareWithTracerProvider(TestBase, WsgiTestBase):
     @classmethod
     def setUpClass(cls):
+        conf.settings.configure(ROOT_URLCONF=modules[__name__])
         super().setUpClass()
 
     def setUp(self):
@@ -375,6 +380,11 @@ def tearDown(self):
         teardown_test_environment()
         _django_instrumentor.uninstrument()
 
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        conf.settings = conf.LazySettings()
+
     def test_tracer_provider_traced(self):
         Client().post("/traced/")
 
diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py
new file mode 100644
--- /dev/null
+++ b/instrumentation/opentelemetry-instrumentation-django/tests/test_middleware_asgi.py
@@ -0,0 +1,382 @@
+# Copyright The OpenTelemetry Authors
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from sys import modules
+from unittest.mock import Mock, patch
+
+import pytest
+from django import VERSION, conf
+from django.http import HttpRequest, HttpResponse
+from django.test import SimpleTestCase
+from django.test.utils import setup_test_environment, teardown_test_environment
+from django.urls import re_path
+
+from opentelemetry.instrumentation.django import (
+    DjangoInstrumentor,
+    _DjangoMiddleware,
+)
+from opentelemetry.instrumentation.propagators import (
+    TraceResponsePropagator,
+    set_global_response_propagator,
+)
+from opentelemetry.sdk import resources
+from opentelemetry.sdk.trace import Span
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.test.test_base import TestBase
+from opentelemetry.trace import (
+    SpanKind,
+    StatusCode,
+    format_span_id,
+    format_trace_id,
+)
+from opentelemetry.util.http import get_excluded_urls, get_traced_request_attrs
+
+# pylint: disable=import-error
+from .views import (
+    async_error,
+    async_excluded,
+    async_excluded_noarg,
+    async_excluded_noarg2,
+    async_route_span_name,
+    async_traced,
+    async_traced_template,
+)
+
+DJANGO_3_1 = VERSION >= (3, 1)
+
+urlpatterns = [
+    re_path(r"^traced/", async_traced),
+    re_path(r"^route/(?P<year>[0-9]{4})/template/$", async_traced_template),
+    re_path(r"^error/", async_error),
+    re_path(r"^excluded_arg/", async_excluded),
+    re_path(r"^excluded_noarg/", async_excluded_noarg),
+    re_path(r"^excluded_noarg2/", async_excluded_noarg2),
+    re_path(r"^span_name/([0-9]{4})/$", async_route_span_name),
+]
+_django_instrumentor = DjangoInstrumentor()
+
+
+@pytest.mark.skipif(
+    not DJANGO_3_1, reason="AsyncClient implemented since Django 3.1"
+)
+class TestMiddlewareAsgi(SimpleTestCase, TestBase):
+    @classmethod
+    def setUpClass(cls):
+        conf.settings.configure(ROOT_URLCONF=modules[__name__])
+        super().setUpClass()
+
+    def setUp(self):
+        super().setUp()
+        setup_test_environment()
+        _django_instrumentor.instrument()
+        self.env_patch = patch.dict(
+            "os.environ",
+            {
+                "OTEL_PYTHON_DJANGO_EXCLUDED_URLS": "http://testserver/excluded_arg/123,excluded_noarg",
+                "OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS": "path_info,content_type,non_existing_variable",
+            },
+        )
+        self.env_patch.start()
+        self.exclude_patch = patch(
+            "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._excluded_urls",
+            get_excluded_urls("DJANGO"),
+        )
+        self.traced_patch = patch(
+            "opentelemetry.instrumentation.django.middleware._DjangoMiddleware._traced_request_attrs",
+            get_traced_request_attrs("DJANGO"),
+        )
+        self.exclude_patch.start()
+        self.traced_patch.start()
+
+    def tearDown(self):
+        super().tearDown()
+        self.env_patch.stop()
+        self.exclude_patch.stop()
+        self.traced_patch.stop()
+        teardown_test_environment()
+        _django_instrumentor.uninstrument()
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        conf.settings = conf.LazySettings()
+
+    async def test_templated_route_get(self):
+        await self.async_client.get("/route/2020/template/")
+
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+
+        span = spans[0]
+
+        self.assertEqual(span.name, "^route/(?P<year>[0-9]{4})/template/$")
+        self.assertEqual(span.kind, SpanKind.SERVER)
+        self.assertEqual(span.status.status_code, StatusCode.UNSET)
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_URL],
+            "http://127.0.0.1/route/2020/template/",
+        )
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_ROUTE],
+            "^route/(?P<year>[0-9]{4})/template/$",
+        )
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
+
+    async def test_traced_get(self):
+        await self.async_client.get("/traced/")
+
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+
+        span = spans[0]
+
+        self.assertEqual(span.name, "^traced/")
+        self.assertEqual(span.kind, SpanKind.SERVER)
+        self.assertEqual(span.status.status_code, StatusCode.UNSET)
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_URL],
+            "http://127.0.0.1/traced/",
+        )
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/"
+        )
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
+
+    async def test_not_recording(self):
+        mock_tracer = Mock()
+        mock_span = Mock()
+        mock_span.is_recording.return_value = False
+        mock_tracer.start_span.return_value = mock_span
+        with patch("opentelemetry.trace.get_tracer") as tracer:
+            tracer.return_value = mock_tracer
+            await self.async_client.get("/traced/")
+            self.assertFalse(mock_span.is_recording())
+            self.assertTrue(mock_span.is_recording.called)
+            self.assertFalse(mock_span.set_attribute.called)
+            self.assertFalse(mock_span.set_status.called)
+
+    async def test_traced_post(self):
+        await self.async_client.post("/traced/")
+
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+
+        span = spans[0]
+
+        self.assertEqual(span.name, "^traced/")
+        self.assertEqual(span.kind, SpanKind.SERVER)
+        self.assertEqual(span.status.status_code, StatusCode.UNSET)
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "POST")
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_URL],
+            "http://127.0.0.1/traced/",
+        )
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_ROUTE], "^traced/"
+        )
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 200)
+
+    async def test_error(self):
+        with self.assertRaises(ValueError):
+            await self.async_client.get("/error/")
+
+        spans = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+
+        span = spans[0]
+
+        self.assertEqual(span.name, "^error/")
+        self.assertEqual(span.kind, SpanKind.SERVER)
+        self.assertEqual(span.status.status_code, StatusCode.ERROR)
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_METHOD], "GET")
+        self.assertEqual(
+            span.attributes[SpanAttributes.HTTP_URL],
+            "http://127.0.0.1/error/",
+        )
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_ROUTE], "^error/")
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_SCHEME], "http")
+        self.assertEqual(span.attributes[SpanAttributes.HTTP_STATUS_CODE], 500)
+
+        self.assertEqual(len(span.events), 1)
+        event = span.events[0]
+        self.assertEqual(event.name, "exception")
+        self.assertEqual(
+            event.attributes[SpanAttributes.EXCEPTION_TYPE], "ValueError"
+        )
+        self.assertEqual(
+            event.attributes[SpanAttributes.EXCEPTION_MESSAGE], "error"
+        )
+
+    async def test_exclude_lists(self):
+        await self.async_client.get("/excluded_arg/123")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 0)
+
+        await self.async_client.get("/excluded_arg/125")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+        await self.async_client.get("/excluded_noarg/")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+        await self.async_client.get("/excluded_noarg2/")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+    async def test_span_name(self):
+        # test no query_string
+        await self.async_client.get("/span_name/1234/")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+        span = span_list[0]
+        self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
+
+    async def test_span_name_for_query_string(self):
+        """
+        request not have query string
+        """
+        await self.async_client.get("/span_name/1234/?query=test")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+        span = span_list[0]
+        self.assertEqual(span.name, "^span_name/([0-9]{4})/$")
+
+    async def test_span_name_404(self):
+        await self.async_client.get("/span_name/1234567890/")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+        span = span_list[0]
+        self.assertEqual(span.name, "HTTP GET")
+
+    async def test_traced_request_attrs(self):
+        await self.async_client.get("/span_name/1234/", CONTENT_TYPE="test/ct")
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+
+        span = span_list[0]
+        self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
+        self.assertEqual(span.attributes["content_type"], "test/ct")
+        self.assertNotIn("non_existing_variable", span.attributes)
+
+    async def test_hooks(self):
+        request_hook_args = ()
+        response_hook_args = ()
+
+        def request_hook(span, request):
+            nonlocal request_hook_args
+            request_hook_args = (span, request)
+
+        def response_hook(span, request, response):
+            nonlocal response_hook_args
+            response_hook_args = (span, request, response)
+            response["hook-header"] = "set by hook"
+
+        _DjangoMiddleware._otel_request_hook = request_hook
+        _DjangoMiddleware._otel_response_hook = response_hook
+
+        response = await self.async_client.get("/span_name/1234/")
+        _DjangoMiddleware._otel_request_hook = (
+            _DjangoMiddleware._otel_response_hook
+        ) = None
+
+        self.assertEqual(response["hook-header"], "set by hook")
+
+        span_list = self.memory_exporter.get_finished_spans()
+        self.assertEqual(len(span_list), 1)
+        span = span_list[0]
+        self.assertEqual(span.attributes["path_info"], "/span_name/1234/")
+
+        self.assertEqual(len(request_hook_args), 2)
+        self.assertEqual(request_hook_args[0].name, span.name)
+        self.assertIsInstance(request_hook_args[0], Span)
+        self.assertIsInstance(request_hook_args[1], HttpRequest)
+
+        self.assertEqual(len(response_hook_args), 3)
+        self.assertEqual(request_hook_args[0], response_hook_args[0])
+        self.assertIsInstance(response_hook_args[1], HttpRequest)
+        self.assertIsInstance(response_hook_args[2], HttpResponse)
+        self.assertEqual(response_hook_args[2], response)
+
+    async def test_trace_response_headers(self):
+        response = await self.async_client.get("/span_name/1234/")
+
+        self.assertNotIn("Server-Timing", response.headers)
+        self.memory_exporter.clear()
+
+        set_global_response_propagator(TraceResponsePropagator())
+
+        response = await self.async_client.get("/span_name/1234/")
+        span = self.memory_exporter.get_finished_spans()[0]
+
+        self.assertIn("traceresponse", response.headers)
+        self.assertEqual(
+            response.headers["Access-Control-Expose-Headers"], "traceresponse",
+        )
+        self.assertEqual(
+            response.headers["traceresponse"],
+            "00-{0}-{1}-01".format(
+                format_trace_id(span.get_span_context().trace_id),
+                format_span_id(span.get_span_context().span_id),
+            ),
+        )
+        self.memory_exporter.clear()
+
+
+class TestMiddlewareAsgiWithTracerProvider(SimpleTestCase, TestBase):
+    @classmethod
+    def setUpClass(cls):
+        conf.settings.configure(ROOT_URLCONF=modules[__name__])
+        super().setUpClass()
+
+    def setUp(self):
+        super().setUp()
+        setup_test_environment()
+        resource = resources.Resource.create(
+            {"resource-key": "resource-value"}
+        )
+        result = self.create_tracer_provider(resource=resource)
+        tracer_provider, exporter = result
+        self.exporter = exporter
+        _django_instrumentor.instrument(tracer_provider=tracer_provider)
+
+    def tearDown(self):
+        super().tearDown()
+        teardown_test_environment()
+        _django_instrumentor.uninstrument()
+
+    @classmethod
+    def tearDownClass(cls):
+        super().tearDownClass()
+        conf.settings = conf.LazySettings()
+
+    async def test_tracer_provider_traced(self):
+        await self.async_client.post("/traced/")
+
+        spans = self.exporter.get_finished_spans()
+        self.assertEqual(len(spans), 1)
+
+        span = spans[0]
+
+        self.assertEqual(
+            span.resource.attributes["resource-key"], "resource-value"
+        )
diff --git a/instrumentation/opentelemetry-instrumentation-django/tests/views.py b/instrumentation/opentelemetry-instrumentation-django/tests/views.py
--- a/instrumentation/opentelemetry-instrumentation-django/tests/views.py
+++ b/instrumentation/opentelemetry-instrumentation-django/tests/views.py
@@ -29,3 +29,35 @@ def route_span_name(
     request, *args, **kwargs
 ):  # pylint: disable=unused-argument
     return HttpResponse()
+
+
+async def async_traced(request):  # pylint: disable=unused-argument
+    return HttpResponse()
+
+
+async def async_traced_template(
+    request, year
+):  # pylint: disable=unused-argument
+    return HttpResponse()
+
+
+async def async_error(request):  # pylint: disable=unused-argument
+    raise ValueError("error")
+
+
+async def async_excluded(request):  # pylint: disable=unused-argument
+    return HttpResponse()
+
+
+async def async_excluded_noarg(request):  # pylint: disable=unused-argument
+    return HttpResponse()
+
+
+async def async_excluded_noarg2(request):  # pylint: disable=unused-argument
+    return HttpResponse()
+
+
+async def async_route_span_name(
+    request, *args, **kwargs
+):  # pylint: disable=unused-argument
+    return HttpResponse()