diff --git a/CHANGELOG.md b/CHANGELOG.md index 05d3b39006..5478699c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +- `opentelemetry-instrumentation-django`: Add `_DjangoMiddleware._get_span` for easier customization ([#3652](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3652)) + ### Fixed - `opentelemetry-instrumentation`: Fix dependency conflict detection when instrumented packages are not installed by moving check back to before instrumentors are loaded. Add "instruments-any" feature for instrumentations that target multiple packages. diff --git a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py index f607046959..7a18e9c15a 100644 --- a/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py +++ b/instrumentation/opentelemetry-instrumentation-django/src/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -13,15 +13,17 @@ # limitations under the License. import types +from contextvars import Token from logging import getLogger from time import time from timeit import default_timer -from typing import Callable +from typing import Callable, ContextManager, Union from django import VERSION as django_version from django.http import HttpRequest, HttpResponse from opentelemetry.context import detach +from opentelemetry.context.context import Context from opentelemetry.instrumentation._semconv import ( _filter_semconv_active_request_count_attr, _filter_semconv_duration_attrs, @@ -207,26 +209,17 @@ def process_request(self, request): 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, + span, token, activation = self._get_span( + request, attributes, is_asgi_request ) active_requests_count_attrs = _parse_active_request_count_attrs( @@ -282,8 +275,6 @@ def process_request(self, request): 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 @@ -301,6 +292,37 @@ def process_request(self, request): # would not be called. Log the exception instead. _logger.exception("Exception raised by request_hook") + def _get_span( + self, + request: Union[HttpRequest, ASGIRequest], + attributes: dict, + is_asgi_request: bool, + ) -> tuple[Span, Token[Context], ContextManager]: + request_meta = request.META + + if is_asgi_request: + carrier = request.scope + carrier_getter = asgi_getter + else: + carrier = request_meta + carrier_getter = wsgi_getter + + 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, + ) + + activation = use_span(span, end_on_exit=True) + activation.__enter__() # pylint: disable=E1101 + + return span, token, activation + # 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 diff --git a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py index d5bf5db7fd..41c3506cc3 100644 --- a/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py +++ b/opentelemetry-instrumentation/src/opentelemetry/instrumentation/utils.py @@ -16,6 +16,7 @@ import urllib.parse from contextlib import contextmanager +from contextvars import Token from importlib import import_module from re import escape, sub from typing import Any, Dict, Generator, Sequence @@ -30,10 +31,11 @@ _SUPPRESS_HTTP_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY, ) +from opentelemetry.context.context import Context # pylint: disable=E0611 from opentelemetry.propagate import extract -from opentelemetry.trace import StatusCode +from opentelemetry.trace import Span, StatusCode from opentelemetry.trace.propagation.tracecontext import ( TraceContextTextMapPropagator, ) @@ -119,7 +121,7 @@ def _start_internal_or_server_span( context_carrier, context_getter, attributes=None, -): +) -> tuple[Span, Token[Context]]: """Returns internal or server span along with the token which can be used by caller to reset context @@ -136,13 +138,13 @@ def _start_internal_or_server_span( from carrier. """ - token = ctx = span_kind = None if trace.get_current_span() is trace.INVALID_SPAN: ctx = extract(context_carrier, getter=context_getter) token = context.attach(ctx) span_kind = trace.SpanKind.SERVER else: ctx = context.get_current() + token = None span_kind = trace.SpanKind.INTERNAL span = tracer.start_span( name=span_name,