# 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. import types from logging import getLogger from time import time from timeit import default_timer from typing import Callable from django import VERSION as django_version from django.http import HttpRequest, HttpResponse from opentelemetry.context import detach from opentelemetry.instrumentation._semconv import ( _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, _report_new, _report_old, _server_active_requests_count_attrs_new, _server_active_requests_count_attrs_old, _server_duration_attrs_new, _server_duration_attrs_old, _StabilityMode, ) from opentelemetry.instrumentation.propagators import ( get_global_response_propagator, ) from opentelemetry.instrumentation.utils import ( _start_internal_or_server_span, extract_attributes_from_object, ) from opentelemetry.instrumentation.wsgi import ( add_response_attributes, wsgi_getter, ) from opentelemetry.instrumentation.wsgi import ( collect_custom_request_headers_attributes as wsgi_collect_custom_request_headers_attributes, ) from opentelemetry.instrumentation.wsgi import ( collect_custom_response_headers_attributes as wsgi_collect_custom_response_headers_attributes, ) from opentelemetry.instrumentation.wsgi import ( collect_request_attributes as wsgi_collect_request_attributes, ) from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE from opentelemetry.semconv.trace import SpanAttributes from opentelemetry.trace import Span, SpanKind, use_span from opentelemetry.util.http import ( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST, OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE, SanitizeValue, get_custom_headers, get_excluded_urls, get_traced_request_attrs, normalise_request_header_name, normalise_response_header_name, sanitize_method, ) try: from django.core.urlresolvers import ( # pylint: disable=no-name-in-module Resolver404, resolve, ) except ImportError: 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 # middlewares can be used. class MiddlewareMixin: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): self.process_request(request) response = self.get_response(request) return self.process_response(request, response) else: # Django versions 1.x can use `settings.MIDDLEWARE_CLASSES` and expect # old-style middlewares, which are created by inheriting from # `deprecation.MiddlewareMixin` since its creation in Django 1.10 and 1.11, # or from `object` for older versions. try: from django.utils.deprecation import MiddlewareMixin 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, asgi_setter, set_status_code, ) from opentelemetry.instrumentation.asgi import ( collect_custom_headers_attributes as asgi_collect_custom_headers_attributes, ) from opentelemetry.instrumentation.asgi import ( collect_request_attributes as asgi_collect_request_attributes, ) _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__) def _is_asgi_request(request: HttpRequest) -> bool: return ASGIRequest is not None and isinstance(request, ASGIRequest) class _DjangoMiddleware(MiddlewareMixin): """Django Middleware for OpenTelemetry""" _environ_activation_key = ( "opentelemetry-instrumentor-django.activation_key" ) _environ_token = "opentelemetry-instrumentor-django.token" _environ_span_key = "opentelemetry-instrumentor-django.span_key" _environ_exception_key = "opentelemetry-instrumentor-django.exception_key" _environ_active_request_attr_key = ( "opentelemetry-instrumentor-django.active_request_attr_key" ) _environ_duration_attr_key = ( "opentelemetry-instrumentor-django.duration_attr_key" ) _environ_timer_key = "opentelemetry-instrumentor-django.timer_key" _traced_request_attrs = get_traced_request_attrs("DJANGO") _excluded_urls = get_excluded_urls("DJANGO") _tracer = None _meter = None _duration_histogram_old = None _duration_histogram_new = None _active_request_counter = None _sem_conv_opt_in_mode = _StabilityMode.DEFAULT _otel_request_hook: Callable[[Span, HttpRequest], None] = None _otel_response_hook: Callable[[Span, HttpRequest, HttpResponse], None] = ( None ) @staticmethod def _get_span_name(request): method = sanitize_method(request.method.strip()) if method == "_OTHER": return "HTTP" try: if getattr(request, "resolver_match"): match = request.resolver_match else: match = resolve(request.path) if hasattr(match, "route") and match.route: return f"{method} {match.route}" if hasattr(match, "url_name") and match.url_name: return f"{method} {match.url_name}" return request.method except Resolver404: return request.method # pylint: disable=too-many-locals # pylint: disable=too-many-branches def process_request(self, request): # request.META is a dictionary containing all available HTTP headers # Read more about request.META here: # https://docs.djangoproject.com/en/3.0/ref/request-response/#django.http.HttpRequest.META 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 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 attributes = collect_request_attributes( carrier, self._sem_conv_opt_in_mode, ) span, token = _start_internal_or_server_span( tracer=self._tracer, span_name=self._get_span_name(request), start_time=request_meta.get( "opentelemetry-instrumentor-django.starttime_key" ), context_carrier=carrier, context_getter=carrier_getter, attributes=attributes, ) active_requests_count_attrs = _parse_active_request_count_attrs( attributes, self._sem_conv_opt_in_mode, ) request.META[self._environ_active_request_attr_key] = ( active_requests_count_attrs ) # Pass all of attributes to duration key because we will filter during response request.META[self._environ_duration_attr_key] = attributes self._active_request_counter.add(1, active_requests_count_attrs) 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, ) if span.is_recording() and span.kind == SpanKind.SERVER: attributes.update( asgi_collect_custom_headers_attributes( carrier, SanitizeValue( get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ), get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST ), normalise_request_header_name, ) ) else: if span.is_recording() and span.kind == SpanKind.SERVER: custom_attributes = ( wsgi_collect_custom_request_headers_attributes(carrier) ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) for key, value in attributes.items(): span.set_attribute(key, value) activation = use_span(span, end_on_exit=True) activation.__enter__() # pylint: disable=E1101 request_start_time = default_timer() request.META[self._environ_timer_key] = request_start_time request.META[self._environ_activation_key] = activation request.META[self._environ_span_key] = span if token: request.META[self._environ_token] = token if _DjangoMiddleware._otel_request_hook: try: _DjangoMiddleware._otel_request_hook( # pylint: disable=not-callable span, request ) except Exception: # pylint: disable=broad-exception-caught # Raising an exception here would leak the request span since process_response # would not be called. Log the exception instead. _logger.exception("Exception raised by request_hook") # pylint: disable=unused-argument def process_view(self, request, view_func, *args, **kwargs): # Process view is executed before the view function, here we get the # route template from request.resolver_match. It is not set yet in process_request if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return if ( self._environ_activation_key in request.META.keys() and self._environ_span_key in request.META.keys() ): span = request.META[self._environ_span_key] match = getattr(request, "resolver_match", None) if match: route = getattr(match, "route", None) if route: if span.is_recording(): # http.route is present for both old and new semconv span.set_attribute(SpanAttributes.HTTP_ROUTE, route) duration_attrs = request.META[ self._environ_duration_attr_key ] if _report_old(self._sem_conv_opt_in_mode): duration_attrs[SpanAttributes.HTTP_TARGET] = route if _report_new(self._sem_conv_opt_in_mode): duration_attrs[HTTP_ROUTE] = route def process_exception(self, request, exception): if self._excluded_urls.url_disabled(request.build_absolute_uri("?")): return if self._environ_activation_key in request.META.keys(): request.META[self._environ_exception_key] = exception # pylint: disable=too-many-branches # pylint: disable=too-many-locals # pylint: disable=too-many-statements 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) active_requests_count_attrs = request.META.pop( self._environ_active_request_attr_key, None ) duration_attrs = request.META.pop( self._environ_duration_attr_key, None ) request_start_time = request.META.pop(self._environ_timer_key, None) if activation and span: if is_asgi_request: set_status_code( span, response.status_code, metric_attributes=duration_attrs, sem_conv_opt_in_mode=self._sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == SpanKind.SERVER: custom_headers = {} for key, value in response.items(): asgi_setter.set(custom_headers, key, value) custom_res_attributes = asgi_collect_custom_headers_attributes( custom_headers, SanitizeValue( get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS ) ), get_custom_headers( OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE ), normalise_response_header_name, ) for key, value in custom_res_attributes.items(): span.set_attribute(key, value) else: add_response_attributes( span, f"{response.status_code} {response.reason_phrase}", response.items(), duration_attrs=duration_attrs, sem_conv_opt_in_mode=self._sem_conv_opt_in_mode, ) if span.is_recording() and span.kind == SpanKind.SERVER: custom_attributes = ( wsgi_collect_custom_response_headers_attributes( response.items() ) ) if len(custom_attributes) > 0: span.set_attributes(custom_attributes) propagator = get_global_response_propagator() if propagator: propagator.inject(response) # record any exceptions raised while processing the request exception = request.META.pop(self._environ_exception_key, None) if _DjangoMiddleware._otel_response_hook: try: _DjangoMiddleware._otel_response_hook( # pylint: disable=not-callable span, request, response ) except Exception: # pylint: disable=broad-exception-caught _logger.exception("Exception raised by response_hook") if exception: activation.__exit__( type(exception), exception, getattr(exception, "__traceback__", None), ) else: activation.__exit__(None, None, None) if request_start_time is not None: duration_s = default_timer() - request_start_time if self._duration_histogram_old: duration_attrs_old = _parse_duration_attrs( duration_attrs, _StabilityMode.DEFAULT ) # http.target to be included in old semantic conventions target = duration_attrs.get(SpanAttributes.HTTP_TARGET) if target: duration_attrs_old[SpanAttributes.HTTP_TARGET] = target self._duration_histogram_old.record( max(round(duration_s * 1000), 0), duration_attrs_old ) if self._duration_histogram_new: duration_attrs_new = _parse_duration_attrs( duration_attrs, _StabilityMode.HTTP ) self._duration_histogram_new.record( max(duration_s, 0), duration_attrs_new ) self._active_request_counter.add(-1, active_requests_count_attrs) if request.META.get(self._environ_token, None) is not None: detach(request.META.get(self._environ_token)) request.META.pop(self._environ_token) return response def _parse_duration_attrs( req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT ): return _filter_semconv_duration_attrs( req_attrs, _server_duration_attrs_old, _server_duration_attrs_new, sem_conv_opt_in_mode, ) def _parse_active_request_count_attrs( req_attrs, sem_conv_opt_in_mode=_StabilityMode.DEFAULT ): return _filter_semconv_active_request_count_attr( req_attrs, _server_active_requests_count_attrs_old, _server_active_requests_count_attrs_new, sem_conv_opt_in_mode, )