diff options
author | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
---|---|---|
committer | S. Solomon Darnell | 2025-03-28 21:52:21 -0500 |
commit | 4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch) | |
tree | ee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django | |
parent | cc961e04ba734dd72309fb548a2f97d67d578813 (diff) | |
download | gn-ai-master.tar.gz |
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django')
7 files changed, 1093 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/__init__.py new file mode 100644 index 00000000..3b9af412 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/__init__.py @@ -0,0 +1,447 @@ +# 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. +""" + +Instrument `django`_ to trace Django applications. + +.. _django: https://pypi.org/project/django/ + +SQLCOMMENTER +***************************************** +You can optionally configure Django instrumentation to enable sqlcommenter which enriches +the query with contextual information. + +Usage +----- + +.. code:: python + + from opentelemetry.instrumentation.django import DjangoInstrumentor + + DjangoInstrumentor().instrument(is_sql_commentor_enabled=True) + + +For example, +:: + + Invoking Users().objects.all() will lead to sql query "select * from auth_users" but when SQLCommenter is enabled + the query will get appended with some configurable tags like "select * from auth_users /*metrics=value*/;" + + +SQLCommenter Configurations +*************************** +We can configure the tags to be appended to the sqlquery log by adding below variables to the settings.py + +SQLCOMMENTER_WITH_FRAMEWORK = True(Default) or False + +For example, +:: +Enabling this flag will add django framework and it's version which is /*framework='django%3A2.2.3*/ + +SQLCOMMENTER_WITH_CONTROLLER = True(Default) or False + +For example, +:: +Enabling this flag will add controller name that handles the request /*controller='index'*/ + +SQLCOMMENTER_WITH_ROUTE = True(Default) or False + +For example, +:: +Enabling this flag will add url path that handles the request /*route='polls/'*/ + +SQLCOMMENTER_WITH_APP_NAME = True(Default) or False + +For example, +:: +Enabling this flag will add app name that handles the request /*app_name='polls'*/ + +SQLCOMMENTER_WITH_OPENTELEMETRY = True(Default) or False + +For example, +:: +Enabling this flag will add opentelemetry traceparent /*traceparent='00-fd720cffceba94bbf75940ff3caaf3cc-4fd1a2bdacf56388-01'*/ + +SQLCOMMENTER_WITH_DB_DRIVER = True(Default) or False + +For example, +:: +Enabling this flag will add name of the db driver /*db_driver='django.db.backends.postgresql'*/ + +Usage +----- + +.. code:: python + + from opentelemetry.instrumentation.django import DjangoInstrumentor + + DjangoInstrumentor().instrument() + + +Configuration +------------- + +Exclude lists +************* +To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_DJANGO_EXCLUDED_URLS`` +(or ``OTEL_PYTHON_EXCLUDED_URLS`` to cover all instrumentations) to a string of comma delimited regexes that match the +URLs. + +For example, + +:: + + export OTEL_PYTHON_DJANGO_EXCLUDED_URLS="client/.*/info,healthcheck" + +will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``. + +Request attributes +******************** +To extract attributes from Django's request object and use them as span attributes, set the environment variable +``OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS`` to a comma delimited list of request attribute names. + +For example, + +:: + + export OTEL_PYTHON_DJANGO_TRACED_REQUEST_ATTRS='path_info,content_type' + +will extract the ``path_info`` and ``content_type`` attributes from every traced request and add them as span attributes. + +Django Request object reference: https://docs.djangoproject.com/en/3.1/ref/request-response/#attributes + +Request and Response hooks +*************************** +This instrumentation supports request and response hooks. These are functions that get called +right after a span is created for a request and right before the span is finished for the response. +The hooks can be configured as follows: + +.. code:: python + + from opentelemetry.instrumentation.django import DjangoInstrumentor + + def request_hook(span, request): + pass + + def response_hook(span, request, response): + pass + + DjangoInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook) + +Django Request object: https://docs.djangoproject.com/en/3.1/ref/request-response/#httprequest-objects +Django Response object: https://docs.djangoproject.com/en/3.1/ref/request-response/#httpresponse-objects + +Capture HTTP request and response headers +***************************************** +You can configure the agent to capture specified HTTP headers as span attributes, according to the +`semantic convention <https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers>`_. + +Request headers +*************** +To capture HTTP request headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to a comma delimited list of HTTP header names. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="content-type,custom_request_header" + +will extract ``content-type`` and ``custom_request_header`` from the request headers and add them as span attributes. + +Request header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST="Accept.*,X-.*" + +Would match all request headers that start with ``Accept`` and ``X-``. + +To capture all request headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST=".*" + +The name of the added span attribute will follow the format ``http.request.header.<header_name>`` where ``<header_name>`` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: +``http.request.header.custom_request_header = ["<value1>,<value2>"]`` + +Response headers +**************** +To capture HTTP response headers as span attributes, set the environment variable +``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to a comma delimited list of HTTP header names. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="content-type,custom_response_header" + +will extract ``content-type`` and ``custom_response_header`` from the response headers and add them as span attributes. + +Response header names in Django are case-insensitive. So, giving the header name as ``CUStom-Header`` in the environment +variable will capture the header named ``custom-header``. + +Regular expressions may also be used to match multiple headers that correspond to the given pattern. For example: +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE="Content.*,X-.*" + +Would match all response headers that start with ``Content`` and ``X-``. + +To capture all response headers, set ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE`` to ``".*"``. +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE=".*" + +The name of the added span attribute will follow the format ``http.response.header.<header_name>`` where ``<header_name>`` +is the normalized HTTP header name (lowercase, with ``-`` replaced by ``_``). The value of the attribute will be a +single item list containing all the header values. + +For example: +``http.response.header.custom_response_header = ["<value1>,<value2>"]`` + +Sanitizing headers +****************** +In order to prevent storing sensitive data such as personally identifiable information (PII), session keys, passwords, +etc, set the environment variable ``OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS`` +to a comma delimited list of HTTP header names to be sanitized. Regexes may be used, and all header names will be +matched in a case-insensitive manner. + +For example, +:: + + export OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS=".*session.*,set-cookie" + +will replace the value of headers such as ``session-id`` and ``set-cookie`` with ``[REDACTED]`` in the span. + +Note: + The environment variable names used to capture HTTP headers are still experimental, and thus are subject to change. + +API +--- + +""" + +from logging import getLogger +from os import environ +from typing import Collection + +from django import VERSION as django_version +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + +from opentelemetry.instrumentation._semconv import ( + _get_schema_url, + _OpenTelemetrySemanticConventionStability, + _OpenTelemetryStabilitySignalType, + _report_new, + _report_old, +) +from opentelemetry.instrumentation.django.environment_variables import ( + OTEL_PYTHON_DJANGO_INSTRUMENT, +) +from opentelemetry.instrumentation.django.middleware.otel_middleware import ( + _DjangoMiddleware, +) +from opentelemetry.instrumentation.django.package import _instruments +from opentelemetry.instrumentation.django.version import __version__ +from opentelemetry.instrumentation.instrumentor import BaseInstrumentor +from opentelemetry.metrics import get_meter +from opentelemetry.semconv._incubating.metrics.http_metrics import ( + create_http_server_active_requests, +) +from opentelemetry.semconv.metrics import MetricInstruments +from opentelemetry.semconv.metrics.http_metrics import ( + HTTP_SERVER_REQUEST_DURATION, +) +from opentelemetry.trace import get_tracer +from opentelemetry.util.http import get_excluded_urls, parse_excluded_urls + +DJANGO_2_0 = django_version >= (2, 0) + +_excluded_urls_from_env = get_excluded_urls("DJANGO") +_logger = getLogger(__name__) + + +def _get_django_middleware_setting() -> str: + # In Django versions 1.x, setting MIDDLEWARE_CLASSES can be used as a legacy + # alternative to MIDDLEWARE. This is the case when `settings.MIDDLEWARE` has + # its default value (`None`). + if not DJANGO_2_0 and getattr(settings, "MIDDLEWARE", None) is None: + return "MIDDLEWARE_CLASSES" + return "MIDDLEWARE" + + +def _get_django_otel_middleware_position( + middleware_length, default_middleware_position=0 +): + otel_position = environ.get("OTEL_PYTHON_DJANGO_MIDDLEWARE_POSITION") + try: + middleware_position = int(otel_position) + except (ValueError, TypeError): + _logger.debug( + "Invalid OTEL_PYTHON_DJANGO_MIDDLEWARE_POSITION value: (%s). Using default position: %d.", + otel_position, + default_middleware_position, + ) + middleware_position = default_middleware_position + + if middleware_position < 0 or middleware_position > middleware_length: + _logger.debug( + "Middleware position %d is out of range (0-%d). Using 0 as the position", + middleware_position, + middleware_length, + ) + middleware_position = 0 + return middleware_position + + +class DjangoInstrumentor(BaseInstrumentor): + """An instrumentor for Django + + See `BaseInstrumentor` + """ + + _opentelemetry_middleware = ".".join( + [_DjangoMiddleware.__module__, _DjangoMiddleware.__qualname__] + ) + + _sql_commenter_middleware = "opentelemetry.instrumentation.django.middleware.sqlcommenter_middleware.SqlCommenter" + + def instrumentation_dependencies(self) -> Collection[str]: + return _instruments + + def _instrument(self, **kwargs): + # FIXME this is probably a pattern that will show up in the rest of the + # ext. Find a better way of implementing this. + if environ.get(OTEL_PYTHON_DJANGO_INSTRUMENT) == "False": + return + + # initialize semantic conventions opt-in if needed + _OpenTelemetrySemanticConventionStability._initialize() + sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP, + ) + + tracer_provider = kwargs.get("tracer_provider") + meter_provider = kwargs.get("meter_provider") + _excluded_urls = kwargs.get("excluded_urls") + tracer = get_tracer( + __name__, + __version__, + tracer_provider=tracer_provider, + schema_url=_get_schema_url(sem_conv_opt_in_mode), + ) + meter = get_meter( + __name__, + __version__, + meter_provider=meter_provider, + schema_url=_get_schema_url(sem_conv_opt_in_mode), + ) + _DjangoMiddleware._sem_conv_opt_in_mode = sem_conv_opt_in_mode + _DjangoMiddleware._tracer = tracer + _DjangoMiddleware._meter = meter + _DjangoMiddleware._excluded_urls = ( + _excluded_urls_from_env + if _excluded_urls is None + else parse_excluded_urls(_excluded_urls) + ) + _DjangoMiddleware._otel_request_hook = kwargs.pop("request_hook", None) + _DjangoMiddleware._otel_response_hook = kwargs.pop( + "response_hook", None + ) + _DjangoMiddleware._duration_histogram_old = None + if _report_old(sem_conv_opt_in_mode): + _DjangoMiddleware._duration_histogram_old = meter.create_histogram( + name=MetricInstruments.HTTP_SERVER_DURATION, + unit="ms", + description="Measures the duration of inbound HTTP requests.", + ) + _DjangoMiddleware._duration_histogram_new = None + if _report_new(sem_conv_opt_in_mode): + _DjangoMiddleware._duration_histogram_new = meter.create_histogram( + name=HTTP_SERVER_REQUEST_DURATION, + description="Duration of HTTP server requests.", + unit="s", + ) + _DjangoMiddleware._active_request_counter = ( + create_http_server_active_requests(meter) + ) + # This can not be solved, but is an inherent problem of this approach: + # the order of middleware entries matters, and here you have no control + # on that: + # https://docs.djangoproject.com/en/3.0/topics/http/middleware/#activating-middleware + # https://docs.djangoproject.com/en/3.0/ref/middleware/#middleware-ordering + + _middleware_setting = _get_django_middleware_setting() + settings_middleware = [] + try: + settings_middleware = getattr(settings, _middleware_setting, []) + except ImproperlyConfigured as exception: + _logger.debug( + "DJANGO_SETTINGS_MODULE environment variable not configured. Defaulting to empty settings: %s", + exception, + ) + settings.configure() + settings_middleware = getattr(settings, _middleware_setting, []) + except ModuleNotFoundError as exception: + _logger.debug( + "DJANGO_SETTINGS_MODULE points to a non-existent module. Defaulting to empty settings: %s", + exception, + ) + settings.configure() + settings_middleware = getattr(settings, _middleware_setting, []) + + # Django allows to specify middlewares as a tuple, so we convert this tuple to a + # list, otherwise we wouldn't be able to call append/remove + if isinstance(settings_middleware, tuple): + settings_middleware = list(settings_middleware) + + is_sql_commentor_enabled = kwargs.pop("is_sql_commentor_enabled", None) + + middleware_position = _get_django_otel_middleware_position( + len(settings_middleware), kwargs.pop("middleware_position", 0) + ) + + if is_sql_commentor_enabled: + settings_middleware.insert( + middleware_position, self._sql_commenter_middleware + ) + + settings_middleware.insert( + middleware_position, self._opentelemetry_middleware + ) + + setattr(settings, _middleware_setting, settings_middleware) + + def _uninstrument(self, **kwargs): + _middleware_setting = _get_django_middleware_setting() + settings_middleware = getattr(settings, _middleware_setting, None) + + # FIXME This is starting to smell like trouble. We have 2 mechanisms + # that may make this condition be True, one implemented in + # BaseInstrumentor and another one implemented in _instrument. Both + # stop _instrument from running and thus, settings_middleware not being + # set. + if settings_middleware is None or ( + self._opentelemetry_middleware not in settings_middleware + ): + return + + settings_middleware.remove(self._opentelemetry_middleware) + setattr(settings, _middleware_setting, settings_middleware) diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/environment_variables.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/environment_variables.py new file mode 100644 index 00000000..4972a62e --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/environment_variables.py @@ -0,0 +1,15 @@ +# 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. + +OTEL_PYTHON_DJANGO_INSTRUMENT = "OTEL_PYTHON_DJANGO_INSTRUMENT" diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/__init__.py diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/otel_middleware.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/otel_middleware.py new file mode 100644 index 00000000..f6070469 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/otel_middleware.py @@ -0,0 +1,476 @@ +# 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, + ) diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py new file mode 100644 index 00000000..ef53d5dc --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py @@ -0,0 +1,123 @@ +# 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 contextlib import ExitStack +from logging import getLogger +from typing import Any, Type, TypeVar + +# pylint: disable=no-name-in-module +from django import conf, get_version +from django.db import connections +from django.db.backends.utils import CursorDebugWrapper + +from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment +from opentelemetry.instrumentation.utils import _get_opentelemetry_values +from opentelemetry.trace.propagation.tracecontext import ( + TraceContextTextMapPropagator, +) + +_propagator = TraceContextTextMapPropagator() + +_django_version = get_version() +_logger = getLogger(__name__) + +T = TypeVar("T") # pylint: disable-msg=invalid-name + + +class SqlCommenter: + """ + Middleware to append a comment to each database query with details about + the framework and the execution context. + """ + + def __init__(self, get_response) -> None: + self.get_response = get_response + + def __call__(self, request) -> Any: + with ExitStack() as stack: + for db_alias in connections: + stack.enter_context( + connections[db_alias].execute_wrapper( + _QueryWrapper(request) + ) + ) + return self.get_response(request) + + +class _QueryWrapper: + def __init__(self, request) -> None: + self.request = request + + def __call__(self, execute: Type[T], sql, params, many, context) -> T: + # pylint: disable-msg=too-many-locals + with_framework = getattr( + conf.settings, "SQLCOMMENTER_WITH_FRAMEWORK", True + ) + with_controller = getattr( + conf.settings, "SQLCOMMENTER_WITH_CONTROLLER", True + ) + with_route = getattr(conf.settings, "SQLCOMMENTER_WITH_ROUTE", True) + with_app_name = getattr( + conf.settings, "SQLCOMMENTER_WITH_APP_NAME", True + ) + with_opentelemetry = getattr( + conf.settings, "SQLCOMMENTER_WITH_OPENTELEMETRY", True + ) + with_db_driver = getattr( + conf.settings, "SQLCOMMENTER_WITH_DB_DRIVER", True + ) + + db_driver = context["connection"].settings_dict.get("ENGINE", "") + resolver_match = self.request.resolver_match + + sql = _add_sql_comment( + sql, + # Information about the controller. + controller=( + resolver_match.view_name + if resolver_match and with_controller + else None + ), + # route is the pattern that matched a request with a controller i.e. the regex + # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.route + # getattr() because the attribute doesn't exist in Django < 2.2. + route=( + getattr(resolver_match, "route", None) + if resolver_match and with_route + else None + ), + # app_name is the application namespace for the URL pattern that matches the URL. + # See https://docs.djangoproject.com/en/stable/ref/urlresolvers/#django.urls.ResolverMatch.app_name + app_name=( + (resolver_match.app_name or None) + if resolver_match and with_app_name + else None + ), + # Framework centric information. + framework=f"django:{_django_version}" if with_framework else None, + # Information about the database and driver. + db_driver=db_driver if with_db_driver else None, + **_get_opentelemetry_values() if with_opentelemetry else {}, + ) + + # TODO: MySQL truncates logs > 1024B so prepend comments + # instead of statements, if the engine is MySQL. + # See: + # * https://github.com/basecamp/marginalia/issues/61 + # * https://github.com/basecamp/marginalia/pull/80 + + # Add the query to the query log if debugging. + if isinstance(context["cursor"], CursorDebugWrapper): + context["connection"].queries_log.append(sql) + + return execute(sql, params, many, context) diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/package.py new file mode 100644 index 00000000..290061a3 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/package.py @@ -0,0 +1,17 @@ +# 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. + + +_instruments = ("django >= 1.10",) +_supports_metrics = True diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/version.py new file mode 100644 index 00000000..7fb5b98b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/version.py @@ -0,0 +1,15 @@ +# 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. + +__version__ = "0.52b1" |