Skip to content

Commit

Permalink
Add Django ASGI support (#391)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamantike authored Oct 12, 2021
1 parent 36275f3 commit 5105820
Show file tree
Hide file tree
Showing 8 changed files with 515 additions and 28 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#706](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/706))
- `opentelemetry-instrumentation-requests` added exclude urls functionality
([#714](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/714))
- `opentelemetry-instrumentation-django` Add ASGI support
([#391](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/391))

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

### Changed

- `opentelemetry-instrumentation-fastapi` Allow instrumentation of newer FastAPI versions.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ install_requires =
opentelemetry-semantic-conventions == 0.24b0

[options.extras_require]
asgi =
opentelemetry-instrumentation-asgi == 0.24b0
test =
opentelemetry-test == 0.24b0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 = [
Expand All @@ -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"""

Expand Down Expand Up @@ -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),
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -57,22 +56,22 @@
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()


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()
Expand Down Expand Up @@ -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/")

Expand Down Expand Up @@ -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):
Expand All @@ -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/")

Expand Down
Loading

0 comments on commit 5105820

Please sign in to comment.