about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django')
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/__init__.py447
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/environment_variables.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/otel_middleware.py476
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/middleware/sqlcommenter_middleware.py123
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/package.py17
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/django/version.py15
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"