about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/opentelemetry/instrumentation
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/instrumentation')
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/_semconv.py435
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/__init__.py991
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/package.py20
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/types.py49
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/__init__.py135
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/_load.py164
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py17
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap.py186
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap_gen.py220
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/__init__.py631
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/package.py16
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/version.py17
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dependencies.py86
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/distro.py70
-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
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/environment_variables.py28
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/__init__.py456
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/package.py20
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py776
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py20
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/instrumentor.py139
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/propagators.py125
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/__init__.py336
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/package.py22
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/__init__.py469
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/package.py20
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/sqlcommenter_utils.py66
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py477
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/package.py21
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/__init__.py599
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/package.py20
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/utils.py226
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/version.py15
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/__init__.py747
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/package.py21
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/py.typed0
-rw-r--r--.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/version.py15
54 files changed, 8853 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/_semconv.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/_semconv.py
new file mode 100644
index 00000000..091c8765
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/_semconv.py
@@ -0,0 +1,435 @@
+# 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 os
+import threading
+from enum import Enum
+
+from opentelemetry.instrumentation.utils import http_status_to_status_code
+from opentelemetry.semconv.attributes.client_attributes import (
+    CLIENT_ADDRESS,
+    CLIENT_PORT,
+)
+from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
+from opentelemetry.semconv.attributes.http_attributes import (
+    HTTP_REQUEST_METHOD,
+    HTTP_REQUEST_METHOD_ORIGINAL,
+    HTTP_RESPONSE_STATUS_CODE,
+    HTTP_ROUTE,
+)
+from opentelemetry.semconv.attributes.network_attributes import (
+    NETWORK_PROTOCOL_VERSION,
+)
+from opentelemetry.semconv.attributes.server_attributes import (
+    SERVER_ADDRESS,
+    SERVER_PORT,
+)
+from opentelemetry.semconv.attributes.url_attributes import (
+    URL_FULL,
+    URL_PATH,
+    URL_QUERY,
+    URL_SCHEME,
+)
+from opentelemetry.semconv.attributes.user_agent_attributes import (
+    USER_AGENT_ORIGINAL,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace.status import Status, StatusCode
+
+# These lists represent attributes for metrics that are currently supported
+
+_client_duration_attrs_old = [
+    SpanAttributes.HTTP_STATUS_CODE,
+    SpanAttributes.HTTP_HOST,
+    SpanAttributes.NET_PEER_PORT,
+    SpanAttributes.NET_PEER_NAME,
+    SpanAttributes.HTTP_METHOD,
+    SpanAttributes.HTTP_FLAVOR,
+    SpanAttributes.HTTP_SCHEME,
+]
+
+_client_duration_attrs_new = [
+    ERROR_TYPE,
+    HTTP_REQUEST_METHOD,
+    HTTP_RESPONSE_STATUS_CODE,
+    NETWORK_PROTOCOL_VERSION,
+    SERVER_ADDRESS,
+    SERVER_PORT,
+    # TODO: Support opt-in for scheme in new semconv
+    # URL_SCHEME,
+]
+
+_server_duration_attrs_old = [
+    SpanAttributes.HTTP_METHOD,
+    SpanAttributes.HTTP_HOST,
+    SpanAttributes.HTTP_SCHEME,
+    SpanAttributes.HTTP_STATUS_CODE,
+    SpanAttributes.HTTP_FLAVOR,
+    SpanAttributes.HTTP_SERVER_NAME,
+    SpanAttributes.NET_HOST_NAME,
+    SpanAttributes.NET_HOST_PORT,
+]
+
+_server_duration_attrs_new = [
+    ERROR_TYPE,
+    HTTP_REQUEST_METHOD,
+    HTTP_RESPONSE_STATUS_CODE,
+    HTTP_ROUTE,
+    NETWORK_PROTOCOL_VERSION,
+    URL_SCHEME,
+]
+
+_server_active_requests_count_attrs_old = [
+    SpanAttributes.HTTP_METHOD,
+    SpanAttributes.HTTP_HOST,
+    SpanAttributes.HTTP_SCHEME,
+    SpanAttributes.HTTP_FLAVOR,
+    SpanAttributes.HTTP_SERVER_NAME,
+]
+
+_server_active_requests_count_attrs_new = [
+    HTTP_REQUEST_METHOD,
+    URL_SCHEME,
+    # TODO: Support SERVER_ADDRESS AND SERVER_PORT
+]
+
+OTEL_SEMCONV_STABILITY_OPT_IN = "OTEL_SEMCONV_STABILITY_OPT_IN"
+
+
+class _OpenTelemetryStabilitySignalType:
+    HTTP = "http"
+    DATABASE = "database"
+
+
+class _StabilityMode(Enum):
+    DEFAULT = "default"
+    HTTP = "http"
+    HTTP_DUP = "http/dup"
+    DATABASE = "database"
+    DATABASE_DUP = "database/dup"
+
+
+def _report_new(mode: _StabilityMode):
+    return mode != _StabilityMode.DEFAULT
+
+
+def _report_old(mode: _StabilityMode):
+    return mode not in (_StabilityMode.HTTP, _StabilityMode.DATABASE)
+
+
+class _OpenTelemetrySemanticConventionStability:
+    _initialized = False
+    _lock = threading.Lock()
+    _OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {}
+
+    @classmethod
+    def _initialize(cls):
+        with cls._lock:
+            if cls._initialized:
+                return
+
+            # Users can pass in comma delimited string for opt-in options
+            # Only values for http and database stability are supported for now
+            opt_in = os.environ.get(OTEL_SEMCONV_STABILITY_OPT_IN)
+
+            if not opt_in:
+                # early return in case of default
+                cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING = {
+                    _OpenTelemetryStabilitySignalType.HTTP: _StabilityMode.DEFAULT,
+                    _OpenTelemetryStabilitySignalType.DATABASE: _StabilityMode.DEFAULT,
+                }
+                cls._initialized = True
+                return
+
+            opt_in_list = [s.strip() for s in opt_in.split(",")]
+
+            cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
+                _OpenTelemetryStabilitySignalType.HTTP
+            ] = cls._filter_mode(
+                opt_in_list, _StabilityMode.HTTP, _StabilityMode.HTTP_DUP
+            )
+
+            cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING[
+                _OpenTelemetryStabilitySignalType.DATABASE
+            ] = cls._filter_mode(
+                opt_in_list,
+                _StabilityMode.DATABASE,
+                _StabilityMode.DATABASE_DUP,
+            )
+
+            cls._initialized = True
+
+    @staticmethod
+    def _filter_mode(opt_in_list, stable_mode, dup_mode):
+        # Process semconv stability opt-in
+        # http/dup,database/dup has higher precedence over http,database
+        if dup_mode.value in opt_in_list:
+            return dup_mode
+
+        return (
+            stable_mode
+            if stable_mode.value in opt_in_list
+            else _StabilityMode.DEFAULT
+        )
+
+    @classmethod
+    def _get_opentelemetry_stability_opt_in_mode(
+        cls, signal_type: _OpenTelemetryStabilitySignalType
+    ) -> _StabilityMode:
+        # Get OpenTelemetry opt-in mode based off of signal type (http, messaging, etc.)
+        return cls._OTEL_SEMCONV_STABILITY_SIGNAL_MAPPING.get(
+            signal_type, _StabilityMode.DEFAULT
+        )
+
+
+def _filter_semconv_duration_attrs(
+    attrs,
+    old_attrs,
+    new_attrs,
+    sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+):
+    filtered_attrs = {}
+    # duration is two different metrics depending on sem_conv_opt_in_mode, so no DUP attributes
+    allowed_attributes = (
+        new_attrs if sem_conv_opt_in_mode == _StabilityMode.HTTP else old_attrs
+    )
+    for key, val in attrs.items():
+        if key in allowed_attributes:
+            filtered_attrs[key] = val
+    return filtered_attrs
+
+
+def _filter_semconv_active_request_count_attr(
+    attrs,
+    old_attrs,
+    new_attrs,
+    sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+):
+    filtered_attrs = {}
+    if _report_old(sem_conv_opt_in_mode):
+        for key, val in attrs.items():
+            if key in old_attrs:
+                filtered_attrs[key] = val
+    if _report_new(sem_conv_opt_in_mode):
+        for key, val in attrs.items():
+            if key in new_attrs:
+                filtered_attrs[key] = val
+    return filtered_attrs
+
+
+def set_string_attribute(result, key, value):
+    if value:
+        result[key] = value
+
+
+def set_int_attribute(result, key, value):
+    if value:
+        try:
+            result[key] = int(value)
+        except ValueError:
+            return
+
+
+def _set_http_method(result, original, normalized, sem_conv_opt_in_mode):
+    original = original.strip()
+    normalized = normalized.strip()
+    # See https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#common-attributes
+    # Method is case sensitive. "http.request.method_original" should not be sanitized or automatically capitalized.
+    if original != normalized and _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, HTTP_REQUEST_METHOD_ORIGINAL, original)
+
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_METHOD, normalized)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, HTTP_REQUEST_METHOD, normalized)
+
+
+def _set_http_status_code(result, code, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_int_attribute(result, SpanAttributes.HTTP_STATUS_CODE, code)
+    if _report_new(sem_conv_opt_in_mode):
+        set_int_attribute(result, HTTP_RESPONSE_STATUS_CODE, code)
+
+
+def _set_http_url(result, url, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_URL, url)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, URL_FULL, url)
+
+
+def _set_http_scheme(result, scheme, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_SCHEME, scheme)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, URL_SCHEME, scheme)
+
+
+def _set_http_flavor_version(result, version, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version)
+
+
+def _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(
+            result, SpanAttributes.HTTP_USER_AGENT, user_agent
+        )
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, USER_AGENT_ORIGINAL, user_agent)
+
+
+# Client
+
+
+def _set_http_host_client(result, host, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_HOST, host)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, SERVER_ADDRESS, host)
+
+
+def _set_http_net_peer_name_client(result, peer_name, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.NET_PEER_NAME, peer_name)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, SERVER_ADDRESS, peer_name)
+
+
+def _set_http_peer_port_client(result, port, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port)
+    if _report_new(sem_conv_opt_in_mode):
+        set_int_attribute(result, SERVER_PORT, port)
+
+
+def _set_http_network_protocol_version(result, version, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_FLAVOR, version)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, NETWORK_PROTOCOL_VERSION, version)
+
+
+# Server
+
+
+def _set_http_net_host(result, host, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.NET_HOST_NAME, host)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, SERVER_ADDRESS, host)
+
+
+def _set_http_net_host_port(result, port, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_int_attribute(result, SpanAttributes.NET_HOST_PORT, port)
+    if _report_new(sem_conv_opt_in_mode):
+        set_int_attribute(result, SERVER_PORT, port)
+
+
+def _set_http_target(result, target, path, query, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_TARGET, target)
+    if _report_new(sem_conv_opt_in_mode):
+        if path:
+            set_string_attribute(result, URL_PATH, path)
+        if query:
+            set_string_attribute(result, URL_QUERY, query)
+
+
+def _set_http_host_server(result, host, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.HTTP_HOST, host)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, CLIENT_ADDRESS, host)
+
+
+# net.peer.ip -> net.sock.peer.addr
+# https://github.com/open-telemetry/semantic-conventions/blob/40db676ca0e735aa84f242b5a0fb14e49438b69b/schemas/1.15.0#L18
+# net.sock.peer.addr -> client.socket.address for server spans (TODO) AND client.address if missing
+# https://github.com/open-telemetry/semantic-conventions/blob/v1.21.0/CHANGELOG.md#v1210-2023-07-13
+# https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md#common-attributes-across-http-client-and-server-spans
+def _set_http_peer_ip_server(result, ip, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.NET_PEER_IP, ip)
+    if _report_new(sem_conv_opt_in_mode):
+        # Only populate if not already populated
+        if not result.get(CLIENT_ADDRESS):
+            set_string_attribute(result, CLIENT_ADDRESS, ip)
+
+
+def _set_http_peer_port_server(result, port, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_int_attribute(result, SpanAttributes.NET_PEER_PORT, port)
+    if _report_new(sem_conv_opt_in_mode):
+        set_int_attribute(result, CLIENT_PORT, port)
+
+
+def _set_http_net_peer_name_server(result, name, sem_conv_opt_in_mode):
+    if _report_old(sem_conv_opt_in_mode):
+        set_string_attribute(result, SpanAttributes.NET_PEER_NAME, name)
+    if _report_new(sem_conv_opt_in_mode):
+        set_string_attribute(result, CLIENT_ADDRESS, name)
+
+
+def _set_status(
+    span,
+    metrics_attributes: dict,
+    status_code: int,
+    status_code_str: str,
+    server_span: bool = True,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    if status_code < 0:
+        if _report_new(sem_conv_opt_in_mode):
+            metrics_attributes[ERROR_TYPE] = status_code_str
+        if span.is_recording():
+            if _report_new(sem_conv_opt_in_mode):
+                span.set_attribute(ERROR_TYPE, status_code_str)
+            span.set_status(
+                Status(
+                    StatusCode.ERROR,
+                    "Non-integer HTTP status: " + status_code_str,
+                )
+            )
+    else:
+        status = http_status_to_status_code(
+            status_code, server_span=server_span
+        )
+
+        if _report_old(sem_conv_opt_in_mode):
+            if span.is_recording():
+                span.set_attribute(
+                    SpanAttributes.HTTP_STATUS_CODE, status_code
+                )
+            metrics_attributes[SpanAttributes.HTTP_STATUS_CODE] = status_code
+        if _report_new(sem_conv_opt_in_mode):
+            if span.is_recording():
+                span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code)
+            metrics_attributes[HTTP_RESPONSE_STATUS_CODE] = status_code
+            if status == StatusCode.ERROR:
+                if span.is_recording():
+                    span.set_attribute(ERROR_TYPE, status_code_str)
+                metrics_attributes[ERROR_TYPE] = status_code_str
+        if span.is_recording():
+            span.set_status(Status(status))
+
+
+# Get schema version based off of opt-in mode
+def _get_schema_url(mode: _StabilityMode) -> str:
+    if mode is _StabilityMode.DEFAULT:
+        return "https://opentelemetry.io/schemas/1.11.0"
+    return SpanAttributes.SCHEMA_URL
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/__init__.py
new file mode 100644
index 00000000..b0600951
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/__init__.py
@@ -0,0 +1,991 @@
+# 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.
+# pylint: disable=too-many-locals
+
+"""
+The opentelemetry-instrumentation-asgi package provides an ASGI middleware that can be used
+on any ASGI framework (such as Django-channels / Quart) to track request timing through OpenTelemetry.
+
+Usage (Quart)
+-------------
+
+.. code-block:: python
+
+    from quart import Quart
+    from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
+
+    app = Quart(__name__)
+    app.asgi_app = OpenTelemetryMiddleware(app.asgi_app)
+
+    @app.route("/")
+    async def hello():
+        return "Hello!"
+
+    if __name__ == "__main__":
+        app.run(debug=True)
+
+
+Usage (Django 3.0)
+------------------
+
+Modify the application's ``asgi.py`` file as shown below.
+
+.. code-block:: python
+
+    import os
+    from django.core.asgi import get_asgi_application
+    from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
+
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'asgi_example.settings')
+
+    application = get_asgi_application()
+    application = OpenTelemetryMiddleware(application)
+
+
+Usage (Raw ASGI)
+----------------
+
+.. code-block:: python
+
+    from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
+
+    app = ...  # An ASGI application.
+    app = OpenTelemetryMiddleware(app)
+
+
+Configuration
+-------------
+
+Request/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 server request hook is passed a server span and ASGI scope object for every incoming request.
+- The client request hook is called with the internal span and an ASGI scope when the method ``receive`` is called.
+- The client response hook is called with the internal span and an ASGI event when the method ``send`` is called.
+
+For example,
+
+.. code-block:: python
+
+    def server_request_hook(span: Span, scope: dict[str, Any]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
+
+    def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
+
+    def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
+
+   OpenTelemetryMiddleware().(application, server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook)
+
+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 ASGI 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
+list containing 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 ASGI 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
+list containing 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 __future__ import annotations
+
+import typing
+import urllib
+from collections import defaultdict
+from functools import wraps
+from timeit import default_timer
+from typing import Any, Awaitable, Callable, DefaultDict, Tuple
+
+from asgiref.compatibility import guarantee_single_callable
+
+from opentelemetry import context, trace
+from opentelemetry.instrumentation._semconv import (
+    _filter_semconv_active_request_count_attr,
+    _filter_semconv_duration_attrs,
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _report_new,
+    _report_old,
+    _server_active_requests_count_attrs_new,
+    _server_active_requests_count_attrs_old,
+    _server_duration_attrs_new,
+    _server_duration_attrs_old,
+    _set_http_flavor_version,
+    _set_http_host_server,
+    _set_http_method,
+    _set_http_net_host_port,
+    _set_http_peer_ip_server,
+    _set_http_peer_port_server,
+    _set_http_scheme,
+    _set_http_target,
+    _set_http_url,
+    _set_http_user_agent,
+    _set_status,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.asgi.types import (
+    ClientRequestHook,
+    ClientResponseHook,
+    ServerRequestHook,
+)
+from opentelemetry.instrumentation.asgi.version import __version__  # noqa
+from opentelemetry.instrumentation.propagators import (
+    get_global_response_propagator,
+)
+from opentelemetry.instrumentation.utils import _start_internal_or_server_span
+from opentelemetry.metrics import get_meter
+from opentelemetry.propagators.textmap import Getter, Setter
+from opentelemetry.semconv._incubating.metrics.http_metrics import (
+    create_http_server_active_requests,
+    create_http_server_request_body_size,
+    create_http_server_response_body_size,
+)
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+    HTTP_SERVER_REQUEST_DURATION,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import set_span_in_context
+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,
+    _parse_url_query,
+    get_custom_headers,
+    normalise_request_header_name,
+    normalise_response_header_name,
+    remove_url_credentials,
+    sanitize_method,
+)
+
+
+class ASGIGetter(Getter[dict]):
+    def get(
+        self, carrier: dict, key: str
+    ) -> typing.Optional[typing.List[str]]:
+        """Getter implementation to retrieve a HTTP header value from the ASGI
+        scope.
+
+        Args:
+            carrier: ASGI scope object
+            key: header name in scope
+        Returns:
+            A list with a single string with the header value if it exists,
+                else None.
+        """
+        headers = carrier.get("headers")
+        if not headers:
+            return None
+
+        # ASGI header keys are in lower case
+        key = key.lower()
+        decoded = [
+            _decode_header_item(_value)
+            for (_key, _value) in headers
+            if _decode_header_item(_key).lower() == key
+        ]
+        if not decoded:
+            return None
+        return decoded
+
+    def keys(self, carrier: dict) -> typing.List[str]:
+        headers = carrier.get("headers") or []
+        return [_decode_header_item(_key) for (_key, _value) in headers]
+
+
+asgi_getter = ASGIGetter()
+
+
+class ASGISetter(Setter[dict]):
+    def set(self, carrier: dict, key: str, value: str) -> None:  # pylint: disable=no-self-use
+        """Sets response header values on an ASGI scope according to `the spec <https://asgi.readthedocs.io/en/latest/specs/www.html#response-start-send-event>`_.
+
+        Args:
+            carrier: ASGI scope object
+            key: response header name to set
+            value: response header value
+        Returns:
+            None
+        """
+        headers = carrier.get("headers")
+        if not headers:
+            headers = []
+            carrier["headers"] = headers
+
+        headers.append([key.lower().encode(), value.encode()])
+
+
+asgi_setter = ASGISetter()
+
+
+# pylint: disable=too-many-branches
+def collect_request_attributes(
+    scope, sem_conv_opt_in_mode=_StabilityMode.DEFAULT
+):
+    """Collects HTTP request attributes from the ASGI scope and returns a
+    dictionary to be used as span creation attributes."""
+    server_host, port, http_url = get_host_port_url_tuple(scope)
+    query_string = scope.get("query_string")
+    if query_string and http_url:
+        if isinstance(query_string, bytes):
+            query_string = query_string.decode("utf8")
+        http_url += "?" + urllib.parse.unquote(query_string)
+    result = {}
+
+    scheme = scope.get("scheme")
+    if scheme:
+        _set_http_scheme(result, scheme, sem_conv_opt_in_mode)
+    if server_host:
+        _set_http_host_server(result, server_host, sem_conv_opt_in_mode)
+    if port:
+        _set_http_net_host_port(result, port, sem_conv_opt_in_mode)
+    flavor = scope.get("http_version")
+    if flavor:
+        _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode)
+    path = scope.get("path")
+    if path:
+        _set_http_target(
+            result, path, path, query_string, sem_conv_opt_in_mode
+        )
+    if http_url:
+        if _report_old(sem_conv_opt_in_mode):
+            _set_http_url(
+                result,
+                remove_url_credentials(http_url),
+                _StabilityMode.DEFAULT,
+            )
+    http_method = scope.get("method", "")
+    if http_method:
+        _set_http_method(
+            result,
+            http_method,
+            sanitize_method(http_method),
+            sem_conv_opt_in_mode,
+        )
+
+    http_host_value_list = asgi_getter.get(scope, "host")
+    if http_host_value_list:
+        if _report_old(sem_conv_opt_in_mode):
+            result[SpanAttributes.HTTP_SERVER_NAME] = ",".join(
+                http_host_value_list
+            )
+    http_user_agent = asgi_getter.get(scope, "user-agent")
+    if http_user_agent:
+        _set_http_user_agent(result, http_user_agent[0], sem_conv_opt_in_mode)
+
+    if "client" in scope and scope["client"] is not None:
+        _set_http_peer_ip_server(
+            result, scope.get("client")[0], sem_conv_opt_in_mode
+        )
+        _set_http_peer_port_server(
+            result, scope.get("client")[1], sem_conv_opt_in_mode
+        )
+
+    # remove None values
+    result = {k: v for k, v in result.items() if v is not None}
+
+    return result
+
+
+def collect_custom_headers_attributes(
+    scope_or_response_message: dict[str, Any],
+    sanitize: SanitizeValue,
+    header_regexes: list[str],
+    normalize_names: Callable[[str], str],
+) -> dict[str, list[str]]:
+    """
+    Returns custom HTTP request or response headers to be added into SERVER span as span attributes.
+
+    Refer specifications:
+     - https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
+    """
+    headers: DefaultDict[str, list[str]] = defaultdict(list)
+    raw_headers = scope_or_response_message.get("headers")
+    if raw_headers:
+        for key, value in raw_headers:
+            # Decode headers before processing.
+            headers[_decode_header_item(key)].append(
+                _decode_header_item(value)
+            )
+
+    return sanitize.sanitize_header_values(
+        headers,
+        header_regexes,
+        normalize_names,
+    )
+
+
+def get_host_port_url_tuple(scope):
+    """Returns (host, port, full_url) tuple."""
+    server = scope.get("server") or ["0.0.0.0", 80]
+    port = server[1]
+    server_host = server[0] + (":" + str(port) if str(port) != "80" else "")
+    # using the scope path is enough, see:
+    # - https://asgi.readthedocs.io/en/latest/specs/www.html#http-connection-scope (see: root_path and path)
+    # - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-compatibility (see: PATH_INFO)
+    #       PATH_INFO can be derived by stripping root_path from path
+    #       -> that means that the path should contain the root_path already, so prefixing it again is not necessary
+    # - https://wsgi.readthedocs.io/en/latest/definitions.html#envvar-PATH_INFO
+    full_path = scope.get("path", "")
+    http_url = scope.get("scheme", "http") + "://" + server_host + full_path
+    return server_host, port, http_url
+
+
+def set_status_code(
+    span,
+    status_code,
+    metric_attributes=None,
+    sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+):
+    """Adds HTTP response attributes to span using the status_code argument."""
+    status_code_str = str(status_code)
+
+    try:
+        status_code = int(status_code)
+    except ValueError:
+        status_code = -1
+    if metric_attributes is None:
+        metric_attributes = {}
+    _set_status(
+        span,
+        metric_attributes,
+        status_code,
+        status_code_str,
+        server_span=True,
+        sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+    )
+
+
+def get_default_span_details(scope: dict) -> Tuple[str, dict]:
+    """
+    Default span name is the HTTP method and URL path, or just the method.
+    https://github.com/open-telemetry/opentelemetry-specification/pull/3165
+    https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
+
+    Args:
+        scope: the ASGI scope dictionary
+    Returns:
+        a tuple of the span name, and any attributes to attach to the span.
+    """
+    path = scope.get("path", "").strip()
+    method = sanitize_method(scope.get("method", "").strip())
+    if method == "_OTHER":
+        method = "HTTP"
+    if method and path:  # http
+        return f"{method} {path}", {}
+    if path:  # websocket
+        return path, {}
+    return method, {}  # http with no path
+
+
+def _collect_target_attribute(
+    scope: typing.Dict[str, typing.Any],
+) -> typing.Optional[str]:
+    """
+    Returns the target path as defined by the Semantic Conventions.
+
+    This value is suitable to use in metrics as it should replace concrete
+    values with a parameterized name. Example: /api/users/{user_id}
+
+    Refer to the specification
+    https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#parameterized-attributes
+
+    Note: this function requires specific code for each framework, as there's no
+    standard attribute to use.
+    """
+    # FastAPI
+    root_path = scope.get("root_path", "")
+
+    route = scope.get("route")
+    path_format = getattr(route, "path_format", None)
+    if path_format:
+        return f"{root_path}{path_format}"
+
+    return None
+
+
+class OpenTelemetryMiddleware:
+    """The ASGI application middleware.
+
+    This class is an ASGI middleware that starts and annotates spans for any
+    requests it is invoked with.
+
+    Args:
+        app: The ASGI application callable to forward requests to.
+        default_span_details: Callback which should return a string and a tuple, representing the desired default span name and a
+                      dictionary with any additional span attributes to set.
+                      Optional: Defaults to get_default_span_details.
+        server_request_hook: Optional callback which is called with the server span and ASGI
+                      scope object for every incoming request.
+        client_request_hook: Optional callback which is called with the internal span, and ASGI
+                      scope and event which are sent as dictionaries for when the method receive is called.
+        client_response_hook: Optional callback which is called with the internal span, and ASGI
+                      scope and event which are sent as dictionaries for when the method send is called.
+        tracer_provider: The optional tracer provider to use. If omitted
+            the current globally configured one is used.
+        meter_provider: The optional meter provider to use. If omitted
+            the current globally configured one is used.
+        exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace.
+    """
+
+    # pylint: disable=too-many-branches
+    def __init__(
+        self,
+        app,
+        excluded_urls=None,
+        default_span_details=None,
+        server_request_hook: ServerRequestHook = None,
+        client_request_hook: ClientRequestHook = None,
+        client_response_hook: ClientResponseHook = None,
+        tracer_provider=None,
+        meter_provider=None,
+        tracer=None,
+        meter=None,
+        http_capture_headers_server_request: list[str] | None = None,
+        http_capture_headers_server_response: list[str] | None = None,
+        http_capture_headers_sanitize_fields: list[str] | None = None,
+        exclude_spans: list[typing.Literal["receive", "send"]] | None = None,
+    ):
+        # initialize semantic conventions opt-in if needed
+        _OpenTelemetrySemanticConventionStability._initialize()
+        sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+            _OpenTelemetryStabilitySignalType.HTTP,
+        )
+        self.app = guarantee_single_callable(app)
+        self.tracer = (
+            trace.get_tracer(
+                __name__,
+                __version__,
+                tracer_provider,
+                schema_url=_get_schema_url(sem_conv_opt_in_mode),
+            )
+            if tracer is None
+            else tracer
+        )
+        self.meter = (
+            get_meter(
+                __name__,
+                __version__,
+                meter_provider,
+                schema_url=_get_schema_url(sem_conv_opt_in_mode),
+            )
+            if meter is None
+            else meter
+        )
+        self.duration_histogram_old = None
+        if _report_old(sem_conv_opt_in_mode):
+            self.duration_histogram_old = self.meter.create_histogram(
+                name=MetricInstruments.HTTP_SERVER_DURATION,
+                unit="ms",
+                description="Measures the duration of inbound HTTP requests.",
+            )
+        self.duration_histogram_new = None
+        if _report_new(sem_conv_opt_in_mode):
+            self.duration_histogram_new = self.meter.create_histogram(
+                name=HTTP_SERVER_REQUEST_DURATION,
+                description="Duration of HTTP server requests.",
+                unit="s",
+            )
+        self.server_response_size_histogram = None
+        if _report_old(sem_conv_opt_in_mode):
+            self.server_response_size_histogram = self.meter.create_histogram(
+                name=MetricInstruments.HTTP_SERVER_RESPONSE_SIZE,
+                unit="By",
+                description="measures the size of HTTP response messages (compressed).",
+            )
+        self.server_response_body_size_histogram = None
+        if _report_new(sem_conv_opt_in_mode):
+            self.server_response_body_size_histogram = (
+                create_http_server_response_body_size(self.meter)
+            )
+        self.server_request_size_histogram = None
+        if _report_old(sem_conv_opt_in_mode):
+            self.server_request_size_histogram = self.meter.create_histogram(
+                name=MetricInstruments.HTTP_SERVER_REQUEST_SIZE,
+                unit="By",
+                description="Measures the size of HTTP request messages (compressed).",
+            )
+        self.server_request_body_size_histogram = None
+        if _report_new(sem_conv_opt_in_mode):
+            self.server_request_body_size_histogram = (
+                create_http_server_request_body_size(self.meter)
+            )
+        self.active_requests_counter = create_http_server_active_requests(
+            self.meter
+        )
+        self.excluded_urls = excluded_urls
+        self.default_span_details = (
+            default_span_details or get_default_span_details
+        )
+        self.server_request_hook = server_request_hook
+        self.client_request_hook = client_request_hook
+        self.client_response_hook = client_response_hook
+        self.content_length_header = None
+        self._sem_conv_opt_in_mode = sem_conv_opt_in_mode
+
+        # Environment variables as constructor parameters
+        self.http_capture_headers_server_request = (
+            http_capture_headers_server_request
+            or (
+                get_custom_headers(
+                    OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
+                )
+            )
+            or None
+        )
+        self.http_capture_headers_server_response = (
+            http_capture_headers_server_response
+            or (
+                get_custom_headers(
+                    OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
+                )
+            )
+            or None
+        )
+        self.http_capture_headers_sanitize_fields = SanitizeValue(
+            http_capture_headers_sanitize_fields
+            or (
+                get_custom_headers(
+                    OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
+                )
+            )
+            or []
+        )
+        self.exclude_receive_span = (
+            "receive" in exclude_spans if exclude_spans else False
+        )
+        self.exclude_send_span = (
+            "send" in exclude_spans if exclude_spans else False
+        )
+
+    # pylint: disable=too-many-statements
+    async def __call__(
+        self,
+        scope: dict[str, Any],
+        receive: Callable[[], Awaitable[dict[str, Any]]],
+        send: Callable[[dict[str, Any]], Awaitable[None]],
+    ) -> None:
+        """The ASGI application
+
+        Args:
+            scope: An ASGI environment.
+            receive: An awaitable callable yielding dictionaries
+            send: An awaitable callable taking a single dictionary as argument.
+        """
+        start = default_timer()
+        if scope["type"] not in ("http", "websocket"):
+            return await self.app(scope, receive, send)
+
+        _, _, url = get_host_port_url_tuple(scope)
+        if self.excluded_urls and self.excluded_urls.url_disabled(url):
+            return await self.app(scope, receive, send)
+
+        span_name, additional_attributes = self.default_span_details(scope)
+
+        attributes = collect_request_attributes(
+            scope, self._sem_conv_opt_in_mode
+        )
+        attributes.update(additional_attributes)
+        span, token = _start_internal_or_server_span(
+            tracer=self.tracer,
+            span_name=span_name,
+            start_time=None,
+            context_carrier=scope,
+            context_getter=asgi_getter,
+            attributes=attributes,
+        )
+        active_requests_count_attrs = _parse_active_request_count_attrs(
+            attributes,
+            self._sem_conv_opt_in_mode,
+        )
+
+        if scope["type"] == "http":
+            self.active_requests_counter.add(1, active_requests_count_attrs)
+        try:
+            with trace.use_span(span, end_on_exit=False) as current_span:
+                if current_span.is_recording():
+                    for key, value in attributes.items():
+                        current_span.set_attribute(key, value)
+
+                    if current_span.kind == trace.SpanKind.SERVER:
+                        custom_attributes = (
+                            collect_custom_headers_attributes(
+                                scope,
+                                self.http_capture_headers_sanitize_fields,
+                                self.http_capture_headers_server_request,
+                                normalise_request_header_name,
+                            )
+                            if self.http_capture_headers_server_request
+                            else {}
+                        )
+                        if len(custom_attributes) > 0:
+                            current_span.set_attributes(custom_attributes)
+
+                if callable(self.server_request_hook):
+                    self.server_request_hook(current_span, scope)
+
+                otel_receive = self._get_otel_receive(
+                    span_name, scope, receive
+                )
+
+                otel_send = self._get_otel_send(
+                    current_span,
+                    span_name,
+                    scope,
+                    send,
+                    attributes,
+                )
+
+                await self.app(scope, otel_receive, otel_send)
+        finally:
+            if scope["type"] == "http":
+                target = _collect_target_attribute(scope)
+                if target:
+                    path, query = _parse_url_query(target)
+                    _set_http_target(
+                        attributes,
+                        target,
+                        path,
+                        query,
+                        self._sem_conv_opt_in_mode,
+                    )
+                duration_s = default_timer() - start
+                duration_attrs_old = _parse_duration_attrs(
+                    attributes, _StabilityMode.DEFAULT
+                )
+                if target:
+                    duration_attrs_old[SpanAttributes.HTTP_TARGET] = target
+                duration_attrs_new = _parse_duration_attrs(
+                    attributes, _StabilityMode.HTTP
+                )
+                if self.duration_histogram_old:
+                    self.duration_histogram_old.record(
+                        max(round(duration_s * 1000), 0), duration_attrs_old
+                    )
+                if self.duration_histogram_new:
+                    self.duration_histogram_new.record(
+                        max(duration_s, 0), duration_attrs_new
+                    )
+                self.active_requests_counter.add(
+                    -1, active_requests_count_attrs
+                )
+                if self.content_length_header:
+                    if self.server_response_size_histogram:
+                        self.server_response_size_histogram.record(
+                            self.content_length_header, duration_attrs_old
+                        )
+                    if self.server_response_body_size_histogram:
+                        self.server_response_body_size_histogram.record(
+                            self.content_length_header, duration_attrs_new
+                        )
+
+                request_size = asgi_getter.get(scope, "content-length")
+                if request_size:
+                    try:
+                        request_size_amount = int(request_size[0])
+                    except ValueError:
+                        pass
+                    else:
+                        if self.server_request_size_histogram:
+                            self.server_request_size_histogram.record(
+                                request_size_amount, duration_attrs_old
+                            )
+                        if self.server_request_body_size_histogram:
+                            self.server_request_body_size_histogram.record(
+                                request_size_amount, duration_attrs_new
+                            )
+            if token:
+                context.detach(token)
+            if span.is_recording():
+                span.end()
+
+    # pylint: enable=too-many-branches
+    def _get_otel_receive(self, server_span_name, scope, receive):
+        if self.exclude_receive_span:
+            return receive
+
+        @wraps(receive)
+        async def otel_receive():
+            with self.tracer.start_as_current_span(
+                " ".join((server_span_name, scope["type"], "receive"))
+            ) as receive_span:
+                message = await receive()
+                if callable(self.client_request_hook):
+                    self.client_request_hook(receive_span, scope, message)
+                if receive_span.is_recording():
+                    if message["type"] == "websocket.receive":
+                        set_status_code(
+                            receive_span,
+                            200,
+                            None,
+                            self._sem_conv_opt_in_mode,
+                        )
+                    receive_span.set_attribute(
+                        "asgi.event.type", message["type"]
+                    )
+            return message
+
+        return otel_receive
+
+    def _set_send_span(
+        self,
+        server_span_name,
+        scope,
+        send,
+        message,
+        status_code,
+        expecting_trailers,
+    ):
+        """Set send span attributes and status code."""
+        with self.tracer.start_as_current_span(
+            " ".join((server_span_name, scope["type"], "send"))
+        ) as send_span:
+            if callable(self.client_response_hook):
+                self.client_response_hook(send_span, scope, message)
+
+            if send_span.is_recording():
+                if message["type"] == "http.response.start":
+                    expecting_trailers = message.get("trailers", False)
+                send_span.set_attribute("asgi.event.type", message["type"])
+
+            if status_code:
+                set_status_code(
+                    send_span,
+                    status_code,
+                    None,
+                    self._sem_conv_opt_in_mode,
+                )
+        return expecting_trailers
+
+    def _set_server_span(
+        self, server_span, message, status_code, duration_attrs
+    ):
+        """Set server span attributes and status code."""
+        if (
+            server_span.is_recording()
+            and server_span.kind == trace.SpanKind.SERVER
+            and "headers" in message
+        ):
+            custom_response_attributes = (
+                collect_custom_headers_attributes(
+                    message,
+                    self.http_capture_headers_sanitize_fields,
+                    self.http_capture_headers_server_response,
+                    normalise_response_header_name,
+                )
+                if self.http_capture_headers_server_response
+                else {}
+            )
+            if len(custom_response_attributes) > 0:
+                server_span.set_attributes(custom_response_attributes)
+
+        if status_code:
+            set_status_code(
+                server_span,
+                status_code,
+                duration_attrs,
+                self._sem_conv_opt_in_mode,
+            )
+
+    def _get_otel_send(
+        self,
+        server_span,
+        server_span_name,
+        scope,
+        send,
+        duration_attrs,
+    ):
+        expecting_trailers = False
+
+        @wraps(send)
+        async def otel_send(message: dict[str, Any]):
+            nonlocal expecting_trailers
+
+            status_code = None
+            if message["type"] == "http.response.start":
+                status_code = message["status"]
+            elif message["type"] == "websocket.send":
+                status_code = 200
+
+            if not self.exclude_send_span:
+                expecting_trailers = self._set_send_span(
+                    server_span_name,
+                    scope,
+                    send,
+                    message,
+                    status_code,
+                    expecting_trailers,
+                )
+
+            self._set_server_span(
+                server_span, message, status_code, duration_attrs
+            )
+
+            propagator = get_global_response_propagator()
+            if propagator:
+                propagator.inject(
+                    message,
+                    context=set_span_in_context(
+                        server_span, trace.context_api.Context()
+                    ),
+                    setter=asgi_setter,
+                )
+
+            content_length = asgi_getter.get(message, "content-length")
+            if content_length:
+                try:
+                    self.content_length_header = int(content_length[0])
+                except ValueError:
+                    pass
+
+            await send(message)
+
+            # pylint: disable=too-many-boolean-expressions
+            if (
+                not expecting_trailers
+                and message["type"] == "http.response.body"
+                and not message.get("more_body", False)
+            ) or (
+                expecting_trailers
+                and message["type"] == "http.response.trailers"
+                and not message.get("more_trailers", False)
+            ):
+                server_span.end()
+
+        return otel_send
+
+
+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,
+    )
+
+
+def _decode_header_item(value):
+    try:
+        return value.decode("utf-8")
+    except ValueError:
+        # ASGI header encoding specs, see:
+        # - https://asgi.readthedocs.io/en/latest/specs/www.html#wsgi-encoding-differences (see: WSGI encoding differences)
+        # - https://docs.python.org/3/library/codecs.html#text-encodings (see: Text Encodings)
+        return value.decode("unicode_escape")
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/package.py
new file mode 100644
index 00000000..cd35b1f7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/package.py
@@ -0,0 +1,20 @@
+# 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 = ("asgiref ~= 3.0",)
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/types.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/types.py
new file mode 100644
index 00000000..bc0c11af
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/types.py
@@ -0,0 +1,49 @@
+# 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 typing import Any, Callable, Dict, Optional
+
+from opentelemetry.trace import Span
+
+_Scope = Dict[str, Any]
+_Message = Dict[str, Any]
+
+ServerRequestHook = Optional[Callable[[Span, _Scope], None]]
+"""
+Incoming request callback type.
+
+Args:
+    - Server span
+    - ASGI scope as a mapping
+"""
+
+ClientRequestHook = Optional[Callable[[Span, _Scope, _Message], None]]
+"""
+Receive callback type.
+
+Args:
+    - Internal span
+    - ASGI scope as a mapping
+    - ASGI event as a mapping
+"""
+
+ClientResponseHook = Optional[Callable[[Span, _Scope, _Message], None]]
+"""
+Send callback type.
+
+Args:
+    - Internal span
+    - ASGI scope as a mapping
+    - ASGI event as a mapping
+"""
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/asgi/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/__init__.py
new file mode 100644
index 00000000..69af0b4c
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/__init__.py
@@ -0,0 +1,135 @@
+# 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 argparse import REMAINDER, ArgumentParser
+from logging import getLogger
+from os import environ, execl, getcwd
+from os.path import abspath, dirname, pathsep
+from re import sub
+from shutil import which
+
+from opentelemetry.instrumentation.auto_instrumentation._load import (
+    _load_configurators,
+    _load_distro,
+    _load_instrumentors,
+)
+from opentelemetry.instrumentation.utils import _python_path_without_directory
+from opentelemetry.instrumentation.version import __version__
+from opentelemetry.util._importlib_metadata import entry_points
+
+_logger = getLogger(__name__)
+
+
+def run() -> None:
+    parser = ArgumentParser(
+        description="""
+        opentelemetry-instrument automatically instruments a Python
+        program and its dependencies and then runs the program.
+        """,
+        epilog="""
+        Optional arguments (except for --help and --version) for opentelemetry-instrument
+        directly correspond with OpenTelemetry environment variables. The
+        corresponding optional argument is formed by removing the OTEL_ or
+        OTEL_PYTHON_ prefix from the environment variable and lower casing the
+        rest. For example, the optional argument --attribute_value_length_limit
+        corresponds with the environment variable
+        OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT.
+
+        These optional arguments will override the current value of the
+        corresponding environment variable during the execution of the command.
+        """,
+    )
+
+    argument_otel_environment_variable = {}
+
+    for entry_point in entry_points(
+        group="opentelemetry_environment_variables"
+    ):
+        environment_variable_module = entry_point.load()
+
+        for attribute in dir(environment_variable_module):
+            if attribute.startswith("OTEL_"):
+                argument = sub(r"OTEL_(PYTHON_)?", "", attribute).lower()
+
+                parser.add_argument(
+                    f"--{argument}",
+                    required=False,
+                )
+                argument_otel_environment_variable[argument] = attribute
+
+    parser.add_argument(
+        "--version",
+        help="print version information",
+        action="version",
+        version="%(prog)s " + __version__,
+    )
+    parser.add_argument("command", help="Your Python application.")
+    parser.add_argument(
+        "command_args",
+        help="Arguments for your application.",
+        nargs=REMAINDER,
+    )
+
+    args = parser.parse_args()
+
+    for argument, otel_environment_variable in (
+        argument_otel_environment_variable
+    ).items():
+        value = getattr(args, argument)
+        if value is not None:
+            environ[otel_environment_variable] = value
+
+    python_path = environ.get("PYTHONPATH")
+
+    if not python_path:
+        python_path = []
+
+    else:
+        python_path = python_path.split(pathsep)
+
+    cwd_path = getcwd()
+
+    # This is being added to support applications that are being run from their
+    # own executable, like Django.
+    # FIXME investigate if there is another way to achieve this
+    if cwd_path not in python_path:
+        python_path.insert(0, cwd_path)
+
+    filedir_path = dirname(abspath(__file__))
+
+    python_path = [path for path in python_path if path != filedir_path]
+
+    python_path.insert(0, filedir_path)
+
+    environ["PYTHONPATH"] = pathsep.join(python_path)
+
+    executable = which(args.command)
+    execl(executable, executable, *args.command_args)
+
+
+def initialize():
+    """Setup auto-instrumentation, called by the sitecustomize module"""
+    # prevents auto-instrumentation of subprocesses if code execs another python process
+    if "PYTHONPATH" in environ:
+        environ["PYTHONPATH"] = _python_path_without_directory(
+            environ["PYTHONPATH"], dirname(abspath(__file__)), pathsep
+        )
+
+    try:
+        distro = _load_distro()
+        distro.configure()
+        _load_configurators()
+        _load_instrumentors(distro)
+    except Exception:  # pylint: disable=broad-except
+        _logger.exception("Failed to auto initialize OpenTelemetry")
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/_load.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/_load.py
new file mode 100644
index 00000000..3d602b2a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/_load.py
@@ -0,0 +1,164 @@
+# 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 functools import cached_property
+from logging import getLogger
+from os import environ
+
+from opentelemetry.instrumentation.dependencies import (
+    get_dist_dependency_conflicts,
+)
+from opentelemetry.instrumentation.distro import BaseDistro, DefaultDistro
+from opentelemetry.instrumentation.environment_variables import (
+    OTEL_PYTHON_CONFIGURATOR,
+    OTEL_PYTHON_DISABLED_INSTRUMENTATIONS,
+    OTEL_PYTHON_DISTRO,
+)
+from opentelemetry.instrumentation.version import __version__
+from opentelemetry.util._importlib_metadata import (
+    EntryPoint,
+    distributions,
+    entry_points,
+)
+
+_logger = getLogger(__name__)
+
+
+class _EntryPointDistFinder:
+    @cached_property
+    def _mapping(self):
+        return {
+            self._key_for(ep): dist
+            for dist in distributions()
+            for ep in dist.entry_points
+        }
+
+    def dist_for(self, entry_point: EntryPoint):
+        dist = getattr(entry_point, "dist", None)
+        if dist:
+            return dist
+
+        return self._mapping.get(self._key_for(entry_point))
+
+    @staticmethod
+    def _key_for(entry_point: EntryPoint):
+        return f"{entry_point.group}:{entry_point.name}:{entry_point.value}"
+
+
+def _load_distro() -> BaseDistro:
+    distro_name = environ.get(OTEL_PYTHON_DISTRO, None)
+    for entry_point in entry_points(group="opentelemetry_distro"):
+        try:
+            # If no distro is specified, use first to come up.
+            if distro_name is None or distro_name == entry_point.name:
+                distro = entry_point.load()()
+                if not isinstance(distro, BaseDistro):
+                    _logger.debug(
+                        "%s is not an OpenTelemetry Distro. Skipping",
+                        entry_point.name,
+                    )
+                    continue
+                _logger.debug(
+                    "Distribution %s will be configured", entry_point.name
+                )
+                return distro
+        except Exception as exc:  # pylint: disable=broad-except
+            _logger.exception(
+                "Distribution %s configuration failed", entry_point.name
+            )
+            raise exc
+    return DefaultDistro()
+
+
+def _load_instrumentors(distro):
+    package_to_exclude = environ.get(OTEL_PYTHON_DISABLED_INSTRUMENTATIONS, [])
+    entry_point_finder = _EntryPointDistFinder()
+    if isinstance(package_to_exclude, str):
+        package_to_exclude = package_to_exclude.split(",")
+        # to handle users entering "requests , flask" or "requests, flask" with spaces
+        package_to_exclude = [x.strip() for x in package_to_exclude]
+
+    for entry_point in entry_points(group="opentelemetry_pre_instrument"):
+        entry_point.load()()
+
+    for entry_point in entry_points(group="opentelemetry_instrumentor"):
+        if entry_point.name in package_to_exclude:
+            _logger.debug(
+                "Instrumentation skipped for library %s", entry_point.name
+            )
+            continue
+
+        try:
+            entry_point_dist = entry_point_finder.dist_for(entry_point)
+            conflict = get_dist_dependency_conflicts(entry_point_dist)
+            if conflict:
+                _logger.debug(
+                    "Skipping instrumentation %s: %s",
+                    entry_point.name,
+                    conflict,
+                )
+                continue
+
+            # tell instrumentation to not run dep checks again as we already did it above
+            distro.load_instrumentor(entry_point, skip_dep_check=True)
+            _logger.debug("Instrumented %s", entry_point.name)
+        except ImportError:
+            # in scenarios using the kubernetes operator to do autoinstrumentation some
+            # instrumentors (usually requiring binary extensions) may fail to load
+            # because the injected autoinstrumentation code does not match the application
+            # environment regarding python version, libc, etc... In this case it's better
+            # to skip the single instrumentation rather than failing to load everything
+            # so treat differently ImportError than the rest of exceptions
+            _logger.exception(
+                "Importing of %s failed, skipping it", entry_point.name
+            )
+            continue
+        except Exception as exc:  # pylint: disable=broad-except
+            _logger.exception("Instrumenting of %s failed", entry_point.name)
+            raise exc
+
+    for entry_point in entry_points(group="opentelemetry_post_instrument"):
+        entry_point.load()()
+
+
+def _load_configurators():
+    configurator_name = environ.get(OTEL_PYTHON_CONFIGURATOR, None)
+    configured = None
+    for entry_point in entry_points(group="opentelemetry_configurator"):
+        if configured is not None:
+            _logger.warning(
+                "Configuration of %s not loaded, %s already loaded",
+                entry_point.name,
+                configured,
+            )
+            continue
+        try:
+            if (
+                configurator_name is None
+                or configurator_name == entry_point.name
+            ):
+                entry_point.load()().configure(
+                    auto_instrumentation_version=__version__
+                )  # type: ignore
+                configured = entry_point.name
+            else:
+                _logger.warning(
+                    "Configuration of %s not loaded because %s is set by %s",
+                    entry_point.name,
+                    configurator_name,
+                    OTEL_PYTHON_CONFIGURATOR,
+                )
+        except Exception as exc:  # pylint: disable=broad-except
+            _logger.exception("Configuration of %s failed", entry_point.name)
+            raise exc
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.py
new file mode 100644
index 00000000..c126b873
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/auto_instrumentation/sitecustomize.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.
+
+from opentelemetry.instrumentation.auto_instrumentation import initialize
+
+initialize()
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap.py
new file mode 100644
index 00000000..cc0ac68f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap.py
@@ -0,0 +1,186 @@
+# 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 argparse
+import logging
+import sys
+from subprocess import (
+    PIPE,
+    CalledProcessError,
+    Popen,
+    SubprocessError,
+    check_call,
+)
+from typing import Optional
+
+from packaging.requirements import Requirement
+
+from opentelemetry.instrumentation.bootstrap_gen import (
+    default_instrumentations as gen_default_instrumentations,
+)
+from opentelemetry.instrumentation.bootstrap_gen import (
+    libraries as gen_libraries,
+)
+from opentelemetry.instrumentation.version import __version__
+from opentelemetry.util._importlib_metadata import (
+    PackageNotFoundError,
+    version,
+)
+
+logger = logging.getLogger(__name__)
+
+
+def _syscall(func):
+    def wrapper(package=None):
+        try:
+            if package:
+                return func(package)
+            return func()
+        except SubprocessError as exp:
+            cmd = getattr(exp, "cmd", None)
+            if cmd:
+                msg = f'Error calling system command "{" ".join(cmd)}"'
+            if package:
+                msg = f'{msg} for package "{package}"'
+            raise RuntimeError(msg)
+
+    return wrapper
+
+
+@_syscall
+def _sys_pip_install(package):
+    # explicit upgrade strategy to override potential pip config
+    try:
+        check_call(
+            [
+                sys.executable,
+                "-m",
+                "pip",
+                "install",
+                "-U",
+                "--upgrade-strategy",
+                "only-if-needed",
+                package,
+            ]
+        )
+    except CalledProcessError as error:
+        print(error)
+
+
+def _pip_check(libraries):
+    """Ensures none of the instrumentations have dependency conflicts.
+    Clean check reported as:
+    'No broken requirements found.'
+    Dependency conflicts are reported as:
+    'opentelemetry-instrumentation-flask 1.0.1 has requirement opentelemetry-sdk<2.0,>=1.0, but you have opentelemetry-sdk 0.5.'
+    To not be too restrictive, we'll only check for relevant packages.
+    """
+    with Popen(
+        [sys.executable, "-m", "pip", "check"], stdout=PIPE
+    ) as check_pipe:
+        pip_check = check_pipe.communicate()[0].decode()
+        pip_check_lower = pip_check.lower()
+    for package_tup in libraries:
+        for package in package_tup:
+            if package.lower() in pip_check_lower:
+                raise RuntimeError(f"Dependency conflict found: {pip_check}")
+
+
+def _is_installed(req):
+    req = Requirement(req)
+
+    try:
+        dist_version = version(req.name)
+    except PackageNotFoundError:
+        return False
+
+    if not req.specifier.filter(dist_version):
+        logger.warning(
+            "instrumentation for package %s is available"
+            " but version %s is installed. Skipping.",
+            req,
+            dist_version,
+        )
+        return False
+    return True
+
+
+def _find_installed_libraries(default_instrumentations, libraries):
+    for lib in default_instrumentations:
+        yield lib
+
+    for lib in libraries:
+        if _is_installed(lib["library"]):
+            yield lib["instrumentation"]
+
+
+def _run_requirements(default_instrumentations, libraries):
+    logger.setLevel(logging.ERROR)
+    print(
+        "\n".join(
+            _find_installed_libraries(default_instrumentations, libraries)
+        )
+    )
+
+
+def _run_install(default_instrumentations, libraries):
+    for lib in _find_installed_libraries(default_instrumentations, libraries):
+        _sys_pip_install(lib)
+    _pip_check(libraries)
+
+
+def run(
+    default_instrumentations: Optional[list] = None,
+    libraries: Optional[list] = None,
+) -> None:
+    action_install = "install"
+    action_requirements = "requirements"
+
+    parser = argparse.ArgumentParser(
+        description="""
+        opentelemetry-bootstrap detects installed libraries and automatically
+        installs the relevant instrumentation packages for them.
+        """
+    )
+    parser.add_argument(
+        "--version",
+        help="print version information",
+        action="version",
+        version="%(prog)s " + __version__,
+    )
+    parser.add_argument(
+        "-a",
+        "--action",
+        choices=[action_install, action_requirements],
+        default=action_requirements,
+        help="""
+        install - uses pip to install the new requirements using to the
+                  currently active site-package.
+        requirements - prints out the new requirements to stdout. Action can
+                       be piped and appended to a requirements.txt file.
+        """,
+    )
+    args = parser.parse_args()
+
+    if libraries is None:
+        libraries = gen_libraries
+
+    if default_instrumentations is None:
+        default_instrumentations = gen_default_instrumentations
+
+    cmd = {
+        action_install: _run_install,
+        action_requirements: _run_requirements,
+    }[args.action]
+    cmd(default_instrumentations, libraries)
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap_gen.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap_gen.py
new file mode 100644
index 00000000..a6b45788
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/bootstrap_gen.py
@@ -0,0 +1,220 @@
+# 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.
+
+# DO NOT EDIT. THIS FILE WAS AUTOGENERATED FROM INSTRUMENTATION PACKAGES.
+# RUN `python scripts/generate_instrumentation_bootstrap.py` TO REGENERATE.
+
+libraries = [
+    {
+        "library": "openai >= 1.26.0",
+        "instrumentation": "opentelemetry-instrumentation-openai-v2",
+    },
+    {
+        "library": "google-cloud-aiplatform >= 1.64",
+        "instrumentation": "opentelemetry-instrumentation-vertexai>=2.0b0",
+    },
+    {
+        "library": "aio_pika >= 7.2.0, < 10.0.0",
+        "instrumentation": "opentelemetry-instrumentation-aio-pika==0.52b1",
+    },
+    {
+        "library": "aiohttp ~= 3.0",
+        "instrumentation": "opentelemetry-instrumentation-aiohttp-client==0.52b1",
+    },
+    {
+        "library": "aiohttp ~= 3.0",
+        "instrumentation": "opentelemetry-instrumentation-aiohttp-server==0.52b1",
+    },
+    {
+        "library": "aiokafka >= 0.8, < 1.0",
+        "instrumentation": "opentelemetry-instrumentation-aiokafka==0.52b1",
+    },
+    {
+        "library": "aiopg >= 0.13.0, < 2.0.0",
+        "instrumentation": "opentelemetry-instrumentation-aiopg==0.52b1",
+    },
+    {
+        "library": "asgiref ~= 3.0",
+        "instrumentation": "opentelemetry-instrumentation-asgi==0.52b1",
+    },
+    {
+        "library": "asyncpg >= 0.12.0",
+        "instrumentation": "opentelemetry-instrumentation-asyncpg==0.52b1",
+    },
+    {
+        "library": "boto~=2.0",
+        "instrumentation": "opentelemetry-instrumentation-boto==0.52b1",
+    },
+    {
+        "library": "boto3 ~= 1.0",
+        "instrumentation": "opentelemetry-instrumentation-boto3sqs==0.52b1",
+    },
+    {
+        "library": "botocore ~= 1.0",
+        "instrumentation": "opentelemetry-instrumentation-botocore==0.52b1",
+    },
+    {
+        "library": "cassandra-driver ~= 3.25",
+        "instrumentation": "opentelemetry-instrumentation-cassandra==0.52b1",
+    },
+    {
+        "library": "scylla-driver ~= 3.25",
+        "instrumentation": "opentelemetry-instrumentation-cassandra==0.52b1",
+    },
+    {
+        "library": "celery >= 4.0, < 6.0",
+        "instrumentation": "opentelemetry-instrumentation-celery==0.52b1",
+    },
+    {
+        "library": "click >= 8.1.3, < 9.0.0",
+        "instrumentation": "opentelemetry-instrumentation-click==0.52b1",
+    },
+    {
+        "library": "confluent-kafka >= 1.8.2, <= 2.7.0",
+        "instrumentation": "opentelemetry-instrumentation-confluent-kafka==0.52b1",
+    },
+    {
+        "library": "django >= 1.10",
+        "instrumentation": "opentelemetry-instrumentation-django==0.52b1",
+    },
+    {
+        "library": "elasticsearch >= 6.0",
+        "instrumentation": "opentelemetry-instrumentation-elasticsearch==0.52b1",
+    },
+    {
+        "library": "falcon >= 1.4.1, < 5.0.0",
+        "instrumentation": "opentelemetry-instrumentation-falcon==0.52b1",
+    },
+    {
+        "library": "fastapi ~= 0.58",
+        "instrumentation": "opentelemetry-instrumentation-fastapi==0.52b1",
+    },
+    {
+        "library": "flask >= 1.0",
+        "instrumentation": "opentelemetry-instrumentation-flask==0.52b1",
+    },
+    {
+        "library": "grpcio >= 1.42.0",
+        "instrumentation": "opentelemetry-instrumentation-grpc==0.52b1",
+    },
+    {
+        "library": "httpx >= 0.18.0",
+        "instrumentation": "opentelemetry-instrumentation-httpx==0.52b1",
+    },
+    {
+        "library": "jinja2 >= 2.7, < 4.0",
+        "instrumentation": "opentelemetry-instrumentation-jinja2==0.52b1",
+    },
+    {
+        "library": "kafka-python >= 2.0, < 3.0",
+        "instrumentation": "opentelemetry-instrumentation-kafka-python==0.52b1",
+    },
+    {
+        "library": "kafka-python-ng >= 2.0, < 3.0",
+        "instrumentation": "opentelemetry-instrumentation-kafka-python==0.52b1",
+    },
+    {
+        "library": "mysql-connector-python >= 8.0, < 10.0",
+        "instrumentation": "opentelemetry-instrumentation-mysql==0.52b1",
+    },
+    {
+        "library": "mysqlclient < 3",
+        "instrumentation": "opentelemetry-instrumentation-mysqlclient==0.52b1",
+    },
+    {
+        "library": "pika >= 0.12.0",
+        "instrumentation": "opentelemetry-instrumentation-pika==0.52b1",
+    },
+    {
+        "library": "psycopg >= 3.1.0",
+        "instrumentation": "opentelemetry-instrumentation-psycopg==0.52b1",
+    },
+    {
+        "library": "psycopg2 >= 2.7.3.1",
+        "instrumentation": "opentelemetry-instrumentation-psycopg2==0.52b1",
+    },
+    {
+        "library": "psycopg2-binary >= 2.7.3.1",
+        "instrumentation": "opentelemetry-instrumentation-psycopg2==0.52b1",
+    },
+    {
+        "library": "pymemcache >= 1.3.5, < 5",
+        "instrumentation": "opentelemetry-instrumentation-pymemcache==0.52b1",
+    },
+    {
+        "library": "pymongo >= 3.1, < 5.0",
+        "instrumentation": "opentelemetry-instrumentation-pymongo==0.52b1",
+    },
+    {
+        "library": "pymssql >= 2.1.5, < 3",
+        "instrumentation": "opentelemetry-instrumentation-pymssql==0.52b1",
+    },
+    {
+        "library": "PyMySQL < 2",
+        "instrumentation": "opentelemetry-instrumentation-pymysql==0.52b1",
+    },
+    {
+        "library": "pyramid >= 1.7",
+        "instrumentation": "opentelemetry-instrumentation-pyramid==0.52b1",
+    },
+    {
+        "library": "redis >= 2.6",
+        "instrumentation": "opentelemetry-instrumentation-redis==0.52b1",
+    },
+    {
+        "library": "remoulade >= 0.50",
+        "instrumentation": "opentelemetry-instrumentation-remoulade==0.52b1",
+    },
+    {
+        "library": "requests ~= 2.0",
+        "instrumentation": "opentelemetry-instrumentation-requests==0.52b1",
+    },
+    {
+        "library": "sqlalchemy >= 1.0.0, < 2.1.0",
+        "instrumentation": "opentelemetry-instrumentation-sqlalchemy==0.52b1",
+    },
+    {
+        "library": "starlette >= 0.13, <0.15",
+        "instrumentation": "opentelemetry-instrumentation-starlette==0.52b1",
+    },
+    {
+        "library": "psutil >= 5",
+        "instrumentation": "opentelemetry-instrumentation-system-metrics==0.52b1",
+    },
+    {
+        "library": "tornado >= 5.1.1",
+        "instrumentation": "opentelemetry-instrumentation-tornado==0.52b1",
+    },
+    {
+        "library": "tortoise-orm >= 0.17.0",
+        "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.52b1",
+    },
+    {
+        "library": "pydantic >= 1.10.2",
+        "instrumentation": "opentelemetry-instrumentation-tortoiseorm==0.52b1",
+    },
+    {
+        "library": "urllib3 >= 1.0.0, < 3.0.0",
+        "instrumentation": "opentelemetry-instrumentation-urllib3==0.52b1",
+    },
+]
+default_instrumentations = [
+    "opentelemetry-instrumentation-asyncio==0.52b1",
+    "opentelemetry-instrumentation-dbapi==0.52b1",
+    "opentelemetry-instrumentation-logging==0.52b1",
+    "opentelemetry-instrumentation-sqlite3==0.52b1",
+    "opentelemetry-instrumentation-threading==0.52b1",
+    "opentelemetry-instrumentation-urllib==0.52b1",
+    "opentelemetry-instrumentation-wsgi==0.52b1",
+]
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/__init__.py
new file mode 100644
index 00000000..c7b1dee3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/__init__.py
@@ -0,0 +1,631 @@
+# 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.
+
+"""
+The trace integration with Database API supports libraries that follow the
+Python Database API Specification v2.0.
+`<https://www.python.org/dev/peps/pep-0249/>`_
+
+Usage
+-----
+
+.. code-block:: python
+
+    import mysql.connector
+    import pyodbc
+
+    from opentelemetry.instrumentation.dbapi import trace_integration
+
+
+    # Ex: mysql.connector
+    trace_integration(mysql.connector, "connect", "mysql")
+    # Ex: pyodbc
+    trace_integration(pyodbc, "Connection", "odbc")
+
+API
+---
+"""
+
+from __future__ import annotations
+
+import functools
+import logging
+import re
+from typing import Any, Callable, Generic, TypeVar
+
+import wrapt
+from wrapt import wrap_function_wrapper
+
+from opentelemetry import trace as trace_api
+from opentelemetry.instrumentation.dbapi.version import __version__
+from opentelemetry.instrumentation.sqlcommenter_utils import _add_sql_comment
+from opentelemetry.instrumentation.utils import (
+    _get_opentelemetry_values,
+    unwrap,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import SpanKind, TracerProvider, get_tracer
+from opentelemetry.util._importlib_metadata import version as util_version
+
+_DB_DRIVER_ALIASES = {
+    "MySQLdb": "mysqlclient",
+}
+
+_logger = logging.getLogger(__name__)
+
+ConnectionT = TypeVar("ConnectionT")
+CursorT = TypeVar("CursorT")
+
+
+def trace_integration(
+    connect_module: Callable[..., Any],
+    connect_method_name: str,
+    database_system: str,
+    connection_attributes: dict[str, Any] | None = None,
+    tracer_provider: TracerProvider | None = None,
+    capture_parameters: bool = False,
+    enable_commenter: bool = False,
+    db_api_integration_factory: type[DatabaseApiIntegration] | None = None,
+    enable_attribute_commenter: bool = False,
+):
+    """Integrate with DB API library.
+    https://www.python.org/dev/peps/pep-0249/
+
+    Args:
+        connect_module: Module name where connect method is available.
+        connect_method_name: The connect method name.
+        database_system: An identifier for the database management system (DBMS)
+            product being used.
+        connection_attributes: Attribute names for database, port, host and
+            user in Connection object.
+        tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to
+            use. If omitted the current configured one is used.
+        capture_parameters: Configure if db.statement.parameters should be captured.
+        enable_commenter: Flag to enable/disable sqlcommenter.
+        db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
+            default one is used.
+        enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
+    """
+    wrap_connect(
+        __name__,
+        connect_module,
+        connect_method_name,
+        database_system,
+        connection_attributes,
+        version=__version__,
+        tracer_provider=tracer_provider,
+        capture_parameters=capture_parameters,
+        enable_commenter=enable_commenter,
+        db_api_integration_factory=db_api_integration_factory,
+        enable_attribute_commenter=enable_attribute_commenter,
+    )
+
+
+def wrap_connect(
+    name: str,
+    connect_module: Callable[..., Any],
+    connect_method_name: str,
+    database_system: str,
+    connection_attributes: dict[str, Any] | None = None,
+    version: str = "",
+    tracer_provider: TracerProvider | None = None,
+    capture_parameters: bool = False,
+    enable_commenter: bool = False,
+    db_api_integration_factory: type[DatabaseApiIntegration] | None = None,
+    commenter_options: dict[str, Any] | None = None,
+    enable_attribute_commenter: bool = False,
+):
+    """Integrate with DB API library.
+    https://www.python.org/dev/peps/pep-0249/
+
+    Args:
+        connect_module: Module name where connect method is available.
+        connect_method_name: The connect method name.
+        database_system: An identifier for the database management system (DBMS)
+            product being used.
+        connection_attributes: Attribute names for database, port, host and
+            user in Connection object.
+        tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to
+            use. If omitted the current configured one is used.
+        capture_parameters: Configure if db.statement.parameters should be captured.
+        enable_commenter: Flag to enable/disable sqlcommenter.
+        db_api_integration_factory: The `DatabaseApiIntegration` to use. If none is passed the
+            default one is used.
+        commenter_options: Configurations for tags to be appended at the sql query.
+        enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
+
+    """
+    db_api_integration_factory = (
+        db_api_integration_factory or DatabaseApiIntegration
+    )
+
+    # pylint: disable=unused-argument
+    def wrap_connect_(
+        wrapped: Callable[..., Any],
+        instance: Any,
+        args: tuple[Any, Any],
+        kwargs: dict[Any, Any],
+    ):
+        db_integration = db_api_integration_factory(
+            name,
+            database_system,
+            connection_attributes=connection_attributes,
+            version=version,
+            tracer_provider=tracer_provider,
+            capture_parameters=capture_parameters,
+            enable_commenter=enable_commenter,
+            commenter_options=commenter_options,
+            connect_module=connect_module,
+            enable_attribute_commenter=enable_attribute_commenter,
+        )
+        return db_integration.wrapped_connection(wrapped, args, kwargs)
+
+    try:
+        wrap_function_wrapper(
+            connect_module, connect_method_name, wrap_connect_
+        )
+    except Exception as ex:  # pylint: disable=broad-except
+        _logger.warning("Failed to integrate with DB API. %s", str(ex))
+
+
+def unwrap_connect(
+    connect_module: Callable[..., Any], connect_method_name: str
+):
+    """Disable integration with DB API library.
+    https://www.python.org/dev/peps/pep-0249/
+
+    Args:
+        connect_module: Module name where the connect method is available.
+        connect_method_name: The connect method name.
+    """
+    unwrap(connect_module, connect_method_name)
+
+
+def instrument_connection(
+    name: str,
+    connection: ConnectionT | TracedConnectionProxy[ConnectionT],
+    database_system: str,
+    connection_attributes: dict[str, Any] | None = None,
+    version: str = "",
+    tracer_provider: TracerProvider | None = None,
+    capture_parameters: bool = False,
+    enable_commenter: bool = False,
+    commenter_options: dict[str, Any] | None = None,
+    connect_module: Callable[..., Any] | None = None,
+    enable_attribute_commenter: bool = False,
+    db_api_integration_factory: type[DatabaseApiIntegration] | None = None,
+) -> TracedConnectionProxy[ConnectionT]:
+    """Enable instrumentation in a database connection.
+
+    Args:
+        name: The instrumentation module name.
+        connection: The connection to instrument.
+        database_system: An identifier for the database management system (DBMS)
+            product being used.
+        connection_attributes: Attribute names for database, port, host and
+            user in a connection object.
+        tracer_provider: The :class:`opentelemetry.trace.TracerProvider` to
+            use. If omitted the current configured one is used.
+        capture_parameters: Configure if db.statement.parameters should be captured.
+        enable_commenter: Flag to enable/disable sqlcommenter.
+        commenter_options: Configurations for tags to be appended at the sql query.
+        connect_module: Module name where connect method is available.
+        enable_attribute_commenter: Flag to enable/disable sqlcomment inclusion in `db.statement` span attribute. Only available if enable_commenter=True.
+        db_api_integration_factory: A class or factory function to use as a
+            replacement for :class:`DatabaseApiIntegration`. Can be used to
+            obtain connection attributes from the connect method instead of
+            from the connection itself (as done by the pymssql intrumentor).
+
+    Returns:
+        An instrumented connection.
+    """
+    if isinstance(connection, wrapt.ObjectProxy):
+        _logger.warning("Connection already instrumented")
+        return connection
+
+    db_api_integration_factory = (
+        db_api_integration_factory or DatabaseApiIntegration
+    )
+
+    db_integration = db_api_integration_factory(
+        name,
+        database_system,
+        connection_attributes=connection_attributes,
+        version=version,
+        tracer_provider=tracer_provider,
+        capture_parameters=capture_parameters,
+        enable_commenter=enable_commenter,
+        commenter_options=commenter_options,
+        connect_module=connect_module,
+        enable_attribute_commenter=enable_attribute_commenter,
+    )
+    db_integration.get_connection_attributes(connection)
+    return get_traced_connection_proxy(connection, db_integration)
+
+
+def uninstrument_connection(
+    connection: ConnectionT | TracedConnectionProxy[ConnectionT],
+) -> ConnectionT:
+    """Disable instrumentation in a database connection.
+
+    Args:
+        connection: The connection to uninstrument.
+
+    Returns:
+        An uninstrumented connection.
+    """
+    if isinstance(connection, wrapt.ObjectProxy):
+        return connection.__wrapped__
+
+    _logger.warning("Connection is not instrumented")
+    return connection
+
+
+class DatabaseApiIntegration:
+    def __init__(
+        self,
+        name: str,
+        database_system: str,
+        connection_attributes: dict[str, Any] | None = None,
+        version: str = "",
+        tracer_provider: TracerProvider | None = None,
+        capture_parameters: bool = False,
+        enable_commenter: bool = False,
+        commenter_options: dict[str, Any] | None = None,
+        connect_module: Callable[..., Any] | None = None,
+        enable_attribute_commenter: bool = False,
+    ):
+        if connection_attributes is None:
+            self.connection_attributes = {
+                "database": "database",
+                "port": "port",
+                "host": "host",
+                "user": "user",
+            }
+        else:
+            self.connection_attributes = connection_attributes
+        self._name = name
+        self._version = version
+        self._tracer = get_tracer(
+            self._name,
+            instrumenting_library_version=self._version,
+            tracer_provider=tracer_provider,
+            schema_url="https://opentelemetry.io/schemas/1.11.0",
+        )
+        self.capture_parameters = capture_parameters
+        self.enable_commenter = enable_commenter
+        self.commenter_options = commenter_options
+        self.enable_attribute_commenter = enable_attribute_commenter
+        self.database_system = database_system
+        self.connection_props: dict[str, Any] = {}
+        self.span_attributes: dict[str, Any] = {}
+        self.name = ""
+        self.database = ""
+        self.connect_module = connect_module
+        self.commenter_data = self.calculate_commenter_data()
+
+    def _get_db_version(self, db_driver: str) -> str:
+        if db_driver in _DB_DRIVER_ALIASES:
+            return util_version(_DB_DRIVER_ALIASES[db_driver])
+        db_version = ""
+        try:
+            db_version = self.connect_module.__version__
+        except AttributeError:
+            db_version = "unknown"
+        return db_version
+
+    def calculate_commenter_data(self) -> dict[str, Any]:
+        commenter_data: dict[str, Any] = {}
+        if not self.enable_commenter:
+            return commenter_data
+
+        db_driver = getattr(self.connect_module, "__name__", "unknown")
+        db_version = self._get_db_version(db_driver)
+
+        commenter_data = {
+            "db_driver": f"{db_driver}:{db_version.split(' ')[0]}",
+            # PEP 249-compliant drivers should have the following attributes.
+            # We can assume apilevel "1.0" if not given.
+            # We use "unknown" for others to prevent uncaught AttributeError.
+            # https://peps.python.org/pep-0249/#globals
+            "dbapi_threadsafety": getattr(
+                self.connect_module, "threadsafety", "unknown"
+            ),
+            "dbapi_level": getattr(self.connect_module, "apilevel", "1.0"),
+            "driver_paramstyle": getattr(
+                self.connect_module, "paramstyle", "unknown"
+            ),
+        }
+
+        if self.database_system == "postgresql":
+            if hasattr(self.connect_module, "__libpq_version__"):
+                libpq_version = self.connect_module.__libpq_version__
+            else:
+                libpq_version = self.connect_module.pq.__build_version__
+            commenter_data.update({"libpq_version": libpq_version})
+        elif self.database_system == "mysql":
+            mysqlc_version = ""
+            if db_driver == "MySQLdb":
+                mysqlc_version = self.connect_module._mysql.get_client_info()
+            elif db_driver == "pymysql":
+                mysqlc_version = self.connect_module.get_client_info()
+
+            commenter_data.update({"mysql_client_version": mysqlc_version})
+
+        return commenter_data
+
+    def wrapped_connection(
+        self,
+        connect_method: Callable[..., ConnectionT],
+        args: tuple[Any, ...],
+        kwargs: dict[Any, Any],
+    ) -> TracedConnectionProxy[ConnectionT]:
+        """Add object proxy to connection object."""
+        connection = connect_method(*args, **kwargs)
+        self.get_connection_attributes(connection)
+        return get_traced_connection_proxy(connection, self)
+
+    def get_connection_attributes(self, connection: object) -> None:
+        # Populate span fields using connection
+        for key, value in self.connection_attributes.items():
+            # Allow attributes nested in connection object
+            attribute = functools.reduce(
+                lambda attribute, attribute_value: getattr(
+                    attribute, attribute_value, None
+                ),
+                value.split("."),
+                connection,
+            )
+            if attribute:
+                self.connection_props[key] = attribute
+        self.name = self.database_system
+        self.database = self.connection_props.get("database", "")
+        if self.database:
+            # PyMySQL encodes names with utf-8
+            if hasattr(self.database, "decode"):
+                self.database = self.database.decode(errors="ignore")
+            self.name += "." + self.database
+        user = self.connection_props.get("user")
+        # PyMySQL encodes this data
+        if user and isinstance(user, bytes):
+            user = user.decode()
+        if user is not None:
+            self.span_attributes[SpanAttributes.DB_USER] = str(user)
+        host = self.connection_props.get("host")
+        if host is not None:
+            self.span_attributes[SpanAttributes.NET_PEER_NAME] = host
+        port = self.connection_props.get("port")
+        if port is not None:
+            self.span_attributes[SpanAttributes.NET_PEER_PORT] = port
+
+
+# pylint: disable=abstract-method
+class TracedConnectionProxy(wrapt.ObjectProxy, Generic[ConnectionT]):
+    # pylint: disable=unused-argument
+    def __init__(
+        self,
+        connection: ConnectionT,
+        db_api_integration: DatabaseApiIntegration | None = None,
+    ):
+        wrapt.ObjectProxy.__init__(self, connection)
+        self._self_db_api_integration = db_api_integration
+
+    def __getattribute__(self, name: str):
+        if object.__getattribute__(self, name):
+            return object.__getattribute__(self, name)
+
+        return object.__getattribute__(
+            object.__getattribute__(self, "_connection"), name
+        )
+
+    def cursor(self, *args: Any, **kwargs: Any):
+        return get_traced_cursor_proxy(
+            self.__wrapped__.cursor(*args, **kwargs),
+            self._self_db_api_integration,
+        )
+
+    def __enter__(self):
+        self.__wrapped__.__enter__()
+        return self
+
+    def __exit__(self, *args: Any, **kwargs: Any):
+        self.__wrapped__.__exit__(*args, **kwargs)
+
+
+def get_traced_connection_proxy(
+    connection: ConnectionT,
+    db_api_integration: DatabaseApiIntegration | None,
+    *args: Any,
+    **kwargs: Any,
+) -> TracedConnectionProxy[ConnectionT]:
+    return TracedConnectionProxy(connection, db_api_integration)
+
+
+class CursorTracer(Generic[CursorT]):
+    def __init__(self, db_api_integration: DatabaseApiIntegration) -> None:
+        self._db_api_integration = db_api_integration
+        self._commenter_enabled = self._db_api_integration.enable_commenter
+        self._commenter_options = (
+            self._db_api_integration.commenter_options
+            if self._db_api_integration.commenter_options
+            else {}
+        )
+        self._enable_attribute_commenter = (
+            self._db_api_integration.enable_attribute_commenter
+        )
+        self._connect_module = self._db_api_integration.connect_module
+        self._leading_comment_remover = re.compile(r"^/\*.*?\*/")
+
+    def _capture_mysql_version(self, cursor) -> None:
+        """Lazy capture of mysql-connector client version using cursor, if applicable"""
+        if (
+            self._db_api_integration.database_system == "mysql"
+            and self._db_api_integration.connect_module.__name__
+            == "mysql.connector"
+            and not self._db_api_integration.commenter_data[
+                "mysql_client_version"
+            ]
+        ):
+            self._db_api_integration.commenter_data["mysql_client_version"] = (
+                cursor._cnx._cmysql.get_client_info()
+            )
+
+    def _get_commenter_data(self) -> dict:
+        """Uses DB-API integration to return commenter data for sqlcomment"""
+        commenter_data = dict(self._db_api_integration.commenter_data)
+        if self._commenter_options.get("opentelemetry_values", True):
+            commenter_data.update(**_get_opentelemetry_values())
+        return {
+            k: v
+            for k, v in commenter_data.items()
+            if self._commenter_options.get(k, True)
+        }
+
+    def _update_args_with_added_sql_comment(self, args, cursor) -> tuple:
+        """Updates args with cursor info and adds sqlcomment to query statement"""
+        try:
+            args_list = list(args)
+            self._capture_mysql_version(cursor)
+            commenter_data = self._get_commenter_data()
+            statement = _add_sql_comment(args_list[0], **commenter_data)
+            args_list[0] = statement
+            args = tuple(args_list)
+        except Exception as exc:  # pylint: disable=broad-except
+            _logger.exception(
+                "Exception while generating sql comment: %s", exc
+            )
+        return args
+
+    def _populate_span(
+        self,
+        span: trace_api.Span,
+        cursor: CursorT,
+        *args: tuple[Any, ...],
+    ):
+        if not span.is_recording():
+            return
+        statement = self.get_statement(cursor, args)
+        span.set_attribute(
+            SpanAttributes.DB_SYSTEM, self._db_api_integration.database_system
+        )
+        span.set_attribute(
+            SpanAttributes.DB_NAME, self._db_api_integration.database
+        )
+        span.set_attribute(SpanAttributes.DB_STATEMENT, statement)
+
+        for (
+            attribute_key,
+            attribute_value,
+        ) in self._db_api_integration.span_attributes.items():
+            span.set_attribute(attribute_key, attribute_value)
+
+        if self._db_api_integration.capture_parameters and len(args) > 1:
+            span.set_attribute("db.statement.parameters", str(args[1]))
+
+    def get_operation_name(
+        self, cursor: CursorT, args: tuple[Any, ...]
+    ) -> str:  # pylint: disable=no-self-use
+        if args and isinstance(args[0], str):
+            # Strip leading comments so we get the operation name.
+            return self._leading_comment_remover.sub("", args[0]).split()[0]
+        return ""
+
+    def get_statement(self, cursor: CursorT, args: tuple[Any, ...]):  # pylint: disable=no-self-use
+        if not args:
+            return ""
+        statement = args[0]
+        if isinstance(statement, bytes):
+            return statement.decode("utf8", "replace")
+        return statement
+
+    def traced_execution(
+        self,
+        cursor: CursorT,
+        query_method: Callable[..., Any],
+        *args: tuple[Any, ...],
+        **kwargs: dict[Any, Any],
+    ):
+        name = self.get_operation_name(cursor, args)
+        if not name:
+            name = (
+                self._db_api_integration.database
+                if self._db_api_integration.database
+                else self._db_api_integration.name
+            )
+
+        with self._db_api_integration._tracer.start_as_current_span(
+            name, kind=SpanKind.CLIENT
+        ) as span:
+            if span.is_recording():
+                if args and self._commenter_enabled:
+                    if self._enable_attribute_commenter:
+                        # sqlcomment is added to executed query and db.statement span attribute
+                        args = self._update_args_with_added_sql_comment(
+                            args, cursor
+                        )
+                        self._populate_span(span, cursor, *args)
+                    else:
+                        # sqlcomment is only added to executed query
+                        # so db.statement is set before add_sql_comment
+                        self._populate_span(span, cursor, *args)
+                        args = self._update_args_with_added_sql_comment(
+                            args, cursor
+                        )
+                else:
+                    # no sqlcomment anywhere
+                    self._populate_span(span, cursor, *args)
+            return query_method(*args, **kwargs)
+
+
+# pylint: disable=abstract-method
+class TracedCursorProxy(wrapt.ObjectProxy, Generic[CursorT]):
+    # pylint: disable=unused-argument
+    def __init__(
+        self,
+        cursor: CursorT,
+        db_api_integration: DatabaseApiIntegration,
+    ):
+        wrapt.ObjectProxy.__init__(self, cursor)
+        self._self_cursor_tracer = CursorTracer[CursorT](db_api_integration)
+
+    def execute(self, *args: Any, **kwargs: Any):
+        return self._self_cursor_tracer.traced_execution(
+            self.__wrapped__, self.__wrapped__.execute, *args, **kwargs
+        )
+
+    def executemany(self, *args: Any, **kwargs: Any):
+        return self._self_cursor_tracer.traced_execution(
+            self.__wrapped__, self.__wrapped__.executemany, *args, **kwargs
+        )
+
+    def callproc(self, *args: Any, **kwargs: Any):
+        return self._self_cursor_tracer.traced_execution(
+            self.__wrapped__, self.__wrapped__.callproc, *args, **kwargs
+        )
+
+    def __enter__(self):
+        self.__wrapped__.__enter__()
+        return self
+
+    def __exit__(self, *args, **kwargs):
+        self.__wrapped__.__exit__(*args, **kwargs)
+
+
+def get_traced_cursor_proxy(
+    cursor: CursorT,
+    db_api_integration: DatabaseApiIntegration,
+    *args: Any,
+    **kwargs: Any,
+) -> TracedCursorProxy[CursorT]:
+    return TracedCursorProxy(cursor, db_api_integration)
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/package.py
new file mode 100644
index 00000000..7a66a17a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/package.py
@@ -0,0 +1,16 @@
+# 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 = tuple()
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/py.typed b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/py.typed
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/version.py
new file mode 100644
index 00000000..bc1d59fd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dbapi/version.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.
+
+__version__ = "0.52b1"
+
+_instruments = tuple()
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dependencies.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dependencies.py
new file mode 100644
index 00000000..b7e4cff4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/dependencies.py
@@ -0,0 +1,86 @@
+# 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 __future__ import annotations
+
+from logging import getLogger
+from typing import Collection
+
+from packaging.requirements import InvalidRequirement, Requirement
+
+from opentelemetry.util._importlib_metadata import (
+    Distribution,
+    PackageNotFoundError,
+    version,
+)
+
+logger = getLogger(__name__)
+
+
+class DependencyConflict:
+    required: str | None = None
+    found: str | None = None
+
+    def __init__(self, required: str | None, found: str | None = None):
+        self.required = required
+        self.found = found
+
+    def __str__(self):
+        return f'DependencyConflict: requested: "{self.required}" but found: "{self.found}"'
+
+
+def get_dist_dependency_conflicts(
+    dist: Distribution,
+) -> DependencyConflict | None:
+    instrumentation_deps = []
+    extra = "extra"
+    instruments = "instruments"
+    instruments_marker = {extra: instruments}
+    if dist.requires:
+        for dep in dist.requires:
+            if extra not in dep or instruments not in dep:
+                continue
+
+            req = Requirement(dep)
+            if req.marker.evaluate(instruments_marker):
+                instrumentation_deps.append(req)
+
+    return get_dependency_conflicts(instrumentation_deps)
+
+
+def get_dependency_conflicts(
+    deps: Collection[str | Requirement],
+) -> DependencyConflict | None:
+    for dep in deps:
+        if isinstance(dep, Requirement):
+            req = dep
+        else:
+            try:
+                req = Requirement(dep)
+            except InvalidRequirement as exc:
+                logger.warning(
+                    'error parsing dependency, reporting as a conflict: "%s" - %s',
+                    dep,
+                    exc,
+                )
+                return DependencyConflict(dep)
+
+        try:
+            dist_version = version(req.name)
+        except PackageNotFoundError:
+            return DependencyConflict(dep)
+
+        if not req.specifier.contains(dist_version):
+            return DependencyConflict(dep, f"{req.name} {dist_version}")
+    return None
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/distro.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/distro.py
new file mode 100644
index 00000000..1b450f25
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/distro.py
@@ -0,0 +1,70 @@
+# 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.
+# type: ignore
+
+"""
+OpenTelemetry Base Distribution (Distro)
+"""
+
+from abc import ABC, abstractmethod
+from logging import getLogger
+
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.util._importlib_metadata import EntryPoint
+
+_LOG = getLogger(__name__)
+
+
+class BaseDistro(ABC):
+    """An ABC for distro"""
+
+    _instance = None
+
+    def __new__(cls, *args, **kwargs):
+        if cls._instance is None:
+            cls._instance = object.__new__(cls, *args, **kwargs)
+
+        return cls._instance
+
+    @abstractmethod
+    def _configure(self, **kwargs):
+        """Configure the distribution"""
+
+    def configure(self, **kwargs):
+        """Configure the distribution"""
+        self._configure(**kwargs)
+
+    def load_instrumentor(  # pylint: disable=no-self-use
+        self, entry_point: EntryPoint, **kwargs
+    ):
+        """Takes an instrumentation entry point and activates it by instantiating
+        and calling instrument() on it.
+        This is called for each opentelemetry_instrumentor entry point by auto
+        instrumentation.
+
+        Distros can override this method to customize the behavior by
+        inspecting each entry point and configuring them in special ways,
+        passing additional arguments, load a replacement/fork instead,
+        skip loading entirely, etc.
+        """
+        instrumentor: BaseInstrumentor = entry_point.load()
+        instrumentor().instrument(**kwargs)
+
+
+class DefaultDistro(BaseDistro):
+    def _configure(self, **kwargs):
+        pass
+
+
+__all__ = ["BaseDistro", "DefaultDistro"]
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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/environment_variables.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/environment_variables.py
new file mode 100644
index 00000000..78867796
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/environment_variables.py
@@ -0,0 +1,28 @@
+# 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_DISABLED_INSTRUMENTATIONS = "OTEL_PYTHON_DISABLED_INSTRUMENTATIONS"
+"""
+.. envvar:: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS
+"""
+
+OTEL_PYTHON_DISTRO = "OTEL_PYTHON_DISTRO"
+"""
+.. envvar:: OTEL_PYTHON_DISTRO
+"""
+
+OTEL_PYTHON_CONFIGURATOR = "OTEL_PYTHON_CONFIGURATOR"
+"""
+.. envvar:: OTEL_PYTHON_CONFIGURATOR
+"""
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/__init__.py
new file mode 100644
index 00000000..a19480b2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/__init__.py
@@ -0,0 +1,456 @@
+# 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.
+
+"""
+Usage
+-----
+
+.. code-block:: python
+
+    import fastapi
+    from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
+
+    app = fastapi.FastAPI()
+
+    @app.get("/foobar")
+    async def foobar():
+        return {"message": "hello world"}
+
+    FastAPIInstrumentor.instrument_app(app)
+
+Configuration
+-------------
+
+Exclude lists
+*************
+To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FASTAPI_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_FASTAPI_EXCLUDED_URLS="client/.*/info,healthcheck"
+
+will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
+
+You can also pass comma delimited regexes directly to the ``instrument_app`` method:
+
+.. code-block:: python
+
+    FastAPIInstrumentor.instrument_app(app, excluded_urls="client/.*/info,healthcheck")
+
+Request/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 server request hook is passed a server span and ASGI scope object for every incoming request.
+- The client request hook is called with the internal span, and ASGI scope and event when the method ``receive`` is called.
+- The client response hook is called with the internal span, and ASGI scope and event when the method ``send`` is called.
+
+.. code-block:: python
+
+    def server_request_hook(span: Span, scope: dict[str, Any]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
+
+    def client_request_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_client_request_hook", "some-value")
+
+    def client_response_hook(span: Span, scope: dict[str, Any], message: dict[str, Any]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
+
+   FastAPIInstrumentor().instrument(server_request_hook=server_request_hook, client_request_hook=client_request_hook, client_response_hook=client_response_hook)
+
+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,
+or pass the ``http_capture_headers_server_request`` keyword argument to the ``instrument_app`` method.
+
+For example using the environment variable,
+::
+
+    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 FastAPI 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,
+or pass the ``http_capture_headers_server_response`` keyword argument to the ``instrument_app`` method.
+
+For example using the environment variable,
+::
+
+    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 FastAPI 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
+list containing 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, or pass the ``http_capture_headers_sanitize_fields``
+keyword argument to the ``instrument_app`` method.
+
+Regexes may be used, and all header names will be matched in a case-insensitive manner.
+
+For example using the environment variable,
+::
+
+    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 __future__ import annotations
+
+import logging
+from typing import Collection, Literal
+
+import fastapi
+from starlette.routing import Match
+
+from opentelemetry.instrumentation._semconv import (
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.asgi import OpenTelemetryMiddleware
+from opentelemetry.instrumentation.asgi.types import (
+    ClientRequestHook,
+    ClientResponseHook,
+    ServerRequestHook,
+)
+from opentelemetry.instrumentation.fastapi.package import _instruments
+from opentelemetry.instrumentation.fastapi.version import __version__
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.metrics import get_meter
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import get_tracer
+from opentelemetry.util.http import (
+    get_excluded_urls,
+    parse_excluded_urls,
+    sanitize_method,
+)
+
+_excluded_urls_from_env = get_excluded_urls("FASTAPI")
+_logger = logging.getLogger(__name__)
+
+
+class FastAPIInstrumentor(BaseInstrumentor):
+    """An instrumentor for FastAPI
+
+    See `BaseInstrumentor`
+    """
+
+    _original_fastapi = None
+
+    @staticmethod
+    def instrument_app(
+        app,
+        server_request_hook: ServerRequestHook = None,
+        client_request_hook: ClientRequestHook = None,
+        client_response_hook: ClientResponseHook = None,
+        tracer_provider=None,
+        meter_provider=None,
+        excluded_urls=None,
+        http_capture_headers_server_request: list[str] | None = None,
+        http_capture_headers_server_response: list[str] | None = None,
+        http_capture_headers_sanitize_fields: list[str] | None = None,
+        exclude_spans: list[Literal["receive", "send"]] | None = None,
+    ):
+        """Instrument an uninstrumented FastAPI application.
+
+        Args:
+            app: The fastapi ASGI application callable to forward requests to.
+            server_request_hook: Optional callback which is called with the server span and ASGI
+                          scope object for every incoming request.
+            client_request_hook: Optional callback which is called with the internal span, and ASGI
+                          scope and event which are sent as dictionaries for when the method receive is called.
+            client_response_hook: Optional callback which is called with the internal span, and ASGI
+                          scope and event which are sent as dictionaries for when the method send is called.
+            tracer_provider: The optional tracer provider to use. If omitted
+                the current globally configured one is used.
+            meter_provider: The optional meter provider to use. If omitted
+                the current globally configured one is used.
+            excluded_urls: Optional comma delimited string of regexes to match URLs that should not be traced.
+            http_capture_headers_server_request: Optional list of HTTP headers to capture from the request.
+            http_capture_headers_server_response: Optional list of HTTP headers to capture from the response.
+            http_capture_headers_sanitize_fields: Optional list of HTTP headers to sanitize.
+            exclude_spans: Optionally exclude HTTP `send` and/or `receive` spans from the trace.
+        """
+        if not hasattr(app, "_is_instrumented_by_opentelemetry"):
+            app._is_instrumented_by_opentelemetry = False
+
+        if not getattr(app, "_is_instrumented_by_opentelemetry", False):
+            # initialize semantic conventions opt-in if needed
+            _OpenTelemetrySemanticConventionStability._initialize()
+            sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+                _OpenTelemetryStabilitySignalType.HTTP,
+            )
+            if excluded_urls is None:
+                excluded_urls = _excluded_urls_from_env
+            else:
+                excluded_urls = parse_excluded_urls(excluded_urls)
+            tracer = get_tracer(
+                __name__,
+                __version__,
+                tracer_provider,
+                schema_url=_get_schema_url(sem_conv_opt_in_mode),
+            )
+            meter = get_meter(
+                __name__,
+                __version__,
+                meter_provider,
+                schema_url=_get_schema_url(sem_conv_opt_in_mode),
+            )
+
+            app.add_middleware(
+                OpenTelemetryMiddleware,
+                excluded_urls=excluded_urls,
+                default_span_details=_get_default_span_details,
+                server_request_hook=server_request_hook,
+                client_request_hook=client_request_hook,
+                client_response_hook=client_response_hook,
+                # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
+                tracer=tracer,
+                meter=meter,
+                http_capture_headers_server_request=http_capture_headers_server_request,
+                http_capture_headers_server_response=http_capture_headers_server_response,
+                http_capture_headers_sanitize_fields=http_capture_headers_sanitize_fields,
+                exclude_spans=exclude_spans,
+            )
+            app._is_instrumented_by_opentelemetry = True
+            if app not in _InstrumentedFastAPI._instrumented_fastapi_apps:
+                _InstrumentedFastAPI._instrumented_fastapi_apps.add(app)
+        else:
+            _logger.warning(
+                "Attempting to instrument FastAPI app while already instrumented"
+            )
+
+    @staticmethod
+    def uninstrument_app(app: fastapi.FastAPI):
+        app.user_middleware = [
+            x
+            for x in app.user_middleware
+            if x.cls is not OpenTelemetryMiddleware
+        ]
+        app.middleware_stack = app.build_middleware_stack()
+        app._is_instrumented_by_opentelemetry = False
+
+    def instrumentation_dependencies(self) -> Collection[str]:
+        return _instruments
+
+    def _instrument(self, **kwargs):
+        self._original_fastapi = fastapi.FastAPI
+        _InstrumentedFastAPI._tracer_provider = kwargs.get("tracer_provider")
+        _InstrumentedFastAPI._server_request_hook = kwargs.get(
+            "server_request_hook"
+        )
+        _InstrumentedFastAPI._client_request_hook = kwargs.get(
+            "client_request_hook"
+        )
+        _InstrumentedFastAPI._client_response_hook = kwargs.get(
+            "client_response_hook"
+        )
+        _InstrumentedFastAPI._http_capture_headers_server_request = kwargs.get(
+            "http_capture_headers_server_request"
+        )
+        _InstrumentedFastAPI._http_capture_headers_server_response = (
+            kwargs.get("http_capture_headers_server_response")
+        )
+        _InstrumentedFastAPI._http_capture_headers_sanitize_fields = (
+            kwargs.get("http_capture_headers_sanitize_fields")
+        )
+        _excluded_urls = kwargs.get("excluded_urls")
+        _InstrumentedFastAPI._excluded_urls = (
+            _excluded_urls_from_env
+            if _excluded_urls is None
+            else parse_excluded_urls(_excluded_urls)
+        )
+        _InstrumentedFastAPI._meter_provider = kwargs.get("meter_provider")
+        _InstrumentedFastAPI._exclude_spans = kwargs.get("exclude_spans")
+        fastapi.FastAPI = _InstrumentedFastAPI
+
+    def _uninstrument(self, **kwargs):
+        for instance in _InstrumentedFastAPI._instrumented_fastapi_apps:
+            self.uninstrument_app(instance)
+        _InstrumentedFastAPI._instrumented_fastapi_apps.clear()
+        fastapi.FastAPI = self._original_fastapi
+
+
+class _InstrumentedFastAPI(fastapi.FastAPI):
+    _tracer_provider = None
+    _meter_provider = None
+    _excluded_urls = None
+    _server_request_hook: ServerRequestHook = None
+    _client_request_hook: ClientRequestHook = None
+    _client_response_hook: ClientResponseHook = None
+    _instrumented_fastapi_apps = set()
+    _sem_conv_opt_in_mode = _StabilityMode.DEFAULT
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        tracer = get_tracer(
+            __name__,
+            __version__,
+            _InstrumentedFastAPI._tracer_provider,
+            schema_url=_get_schema_url(
+                _InstrumentedFastAPI._sem_conv_opt_in_mode
+            ),
+        )
+        meter = get_meter(
+            __name__,
+            __version__,
+            _InstrumentedFastAPI._meter_provider,
+            schema_url=_get_schema_url(
+                _InstrumentedFastAPI._sem_conv_opt_in_mode
+            ),
+        )
+        self.add_middleware(
+            OpenTelemetryMiddleware,
+            excluded_urls=_InstrumentedFastAPI._excluded_urls,
+            default_span_details=_get_default_span_details,
+            server_request_hook=_InstrumentedFastAPI._server_request_hook,
+            client_request_hook=_InstrumentedFastAPI._client_request_hook,
+            client_response_hook=_InstrumentedFastAPI._client_response_hook,
+            # Pass in tracer/meter to get __name__and __version__ of fastapi instrumentation
+            tracer=tracer,
+            meter=meter,
+            http_capture_headers_server_request=_InstrumentedFastAPI._http_capture_headers_server_request,
+            http_capture_headers_server_response=_InstrumentedFastAPI._http_capture_headers_server_response,
+            http_capture_headers_sanitize_fields=_InstrumentedFastAPI._http_capture_headers_sanitize_fields,
+            exclude_spans=_InstrumentedFastAPI._exclude_spans,
+        )
+        self._is_instrumented_by_opentelemetry = True
+        _InstrumentedFastAPI._instrumented_fastapi_apps.add(self)
+
+    def __del__(self):
+        if self in _InstrumentedFastAPI._instrumented_fastapi_apps:
+            _InstrumentedFastAPI._instrumented_fastapi_apps.remove(self)
+
+
+def _get_route_details(scope):
+    """
+    Function to retrieve Starlette route from scope.
+
+    TODO: there is currently no way to retrieve http.route from
+    a starlette application from scope.
+    See: https://github.com/encode/starlette/pull/804
+
+    Args:
+        scope: A Starlette scope
+    Returns:
+        A string containing the route or None
+    """
+    app = scope["app"]
+    route = None
+
+    for starlette_route in app.routes:
+        match, _ = starlette_route.matches(scope)
+        if match == Match.FULL:
+            route = starlette_route.path
+            break
+        if match == Match.PARTIAL:
+            route = starlette_route.path
+    return route
+
+
+def _get_default_span_details(scope):
+    """
+    Callback to retrieve span name and attributes from scope.
+
+    Args:
+        scope: A Starlette scope
+    Returns:
+        A tuple of span name and attributes
+    """
+    route = _get_route_details(scope)
+    method = sanitize_method(scope.get("method", "").strip())
+    attributes = {}
+    if method == "_OTHER":
+        method = "HTTP"
+    if route:
+        attributes[SpanAttributes.HTTP_ROUTE] = route
+    if method and route:  # http
+        span_name = f"{method} {route}"
+    elif route:  # websocket
+        span_name = route
+    else:  # fallback
+        span_name = method
+    return span_name, attributes
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/package.py
new file mode 100644
index 00000000..d95a2cf6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/package.py
@@ -0,0 +1,20 @@
+# 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 = ("fastapi ~= 0.58",)
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/fastapi/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py
new file mode 100644
index 00000000..9691f884
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/__init__.py
@@ -0,0 +1,776 @@
+# 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.
+
+# Note: This package is not named "flask" because of
+# https://github.com/PyCQA/pylint/issues/2648
+
+"""
+This library builds on the OpenTelemetry WSGI middleware to track web requests
+in Flask applications. In addition to opentelemetry-util-http, it
+supports Flask-specific features such as:
+
+* The Flask url rule pattern is used as the Span name.
+* The ``http.route`` Span attribute is set so that one can see which URL rule
+  matched a request.
+
+SQLCOMMENTER
+*****************************************
+You can optionally configure Flask instrumentation to enable sqlcommenter which enriches
+the query with contextual information.
+
+Usage
+-----
+
+.. code:: python
+
+    from opentelemetry.instrumentation.flask import FlaskInstrumentor
+
+    FlaskInstrumentor().instrument(enable_commenter=True, commenter_options={})
+
+For example, FlaskInstrumentor when used with SQLAlchemyInstrumentor or Psycopg2Instrumentor,
+invoking ``cursor.execute("select * from auth_users")`` will lead to sql query
+``select * from auth_users`` but when SQLCommenter is enabled the query will get appended with
+some configurable tags like:
+
+.. code::
+
+    select * from auth_users /*metrics=value*/;"
+
+Inorder for the commenter to append flask related tags to sql queries, the commenter needs
+to enabled on the respective SQLAlchemyInstrumentor or Psycopg2Instrumentor framework too.
+
+SQLCommenter Configurations
+***************************
+We can configure the tags to be appended to the sqlquery log by adding configuration
+inside ``commenter_options={}`` dict.
+
+For example, enabling this flag will add flask and it's version which
+is ``/*flask%%3A2.9.3*/`` to the SQL query as a comment (default is True):
+
+.. code:: python
+
+    framework = True
+
+For example, enabling this flag will add route uri ``/*route='/home'*/``
+to the SQL query as a comment (default is True):
+
+.. code:: python
+
+    route = True
+
+For example, enabling this flag will add controller name ``/*controller='home_view'*/``
+to the SQL query as a comment (default is True):
+
+.. code:: python
+
+    controller = True
+
+Usage
+-----
+
+.. code-block:: python
+
+    from flask import Flask
+    from opentelemetry.instrumentation.flask import FlaskInstrumentor
+
+    app = Flask(__name__)
+
+    FlaskInstrumentor().instrument_app(app)
+
+    @app.route("/")
+    def hello():
+        return "Hello!"
+
+    if __name__ == "__main__":
+        app.run(debug=True)
+
+Configuration
+-------------
+
+Exclude lists
+*************
+To exclude certain URLs from tracking, set the environment variable ``OTEL_PYTHON_FLASK_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_FLASK_EXCLUDED_URLS="client/.*/info,healthcheck"
+
+will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
+
+You can also pass comma delimited regexes directly to the ``instrument_app`` method:
+
+.. code-block:: python
+
+    FlaskInstrumentor().instrument_app(app, excluded_urls="client/.*/info,healthcheck")
+
+Request/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 client request hook is called with the internal span and an instance of WSGIEnvironment (flask.request.environ)
+  when the method ``receive`` is called.
+- The client response hook is called with the internal span, the status of the response and a list of key-value (tuples)
+  representing the response headers returned from the response when the method ``send`` is called.
+
+For example,
+
+.. code-block:: python
+
+    def request_hook(span: Span, environ: WSGIEnvironment):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
+
+    def response_hook(span: Span, status: str, response_headers: List):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
+
+    FlaskInstrumentor().instrument(request_hook=request_hook, response_hook=response_hook)
+
+Flask Request object reference: https://flask.palletsprojects.com/en/2.1.x/api/#flask.Request
+
+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 Flask are case-insensitive and ``-`` characters are replaced by ``_``. 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 Flask 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
+---
+"""
+
+import weakref
+from logging import getLogger
+from time import time_ns
+from timeit import default_timer
+from typing import Collection
+
+import flask
+from packaging import version as package_version
+
+import opentelemetry.instrumentation.wsgi as otel_wsgi
+from opentelemetry import context, trace
+from opentelemetry.instrumentation._semconv import (
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _report_new,
+    _report_old,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.flask.package import _instruments
+from opentelemetry.instrumentation.flask.version import __version__
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.propagators import (
+    get_global_response_propagator,
+)
+from opentelemetry.instrumentation.utils import _start_internal_or_server_span
+from opentelemetry.metrics import get_meter
+from opentelemetry.semconv.attributes.http_attributes import HTTP_ROUTE
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+    HTTP_SERVER_REQUEST_DURATION,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.util._importlib_metadata import version
+from opentelemetry.util.http import (
+    get_excluded_urls,
+    parse_excluded_urls,
+    sanitize_method,
+)
+
+_logger = getLogger(__name__)
+
+_ENVIRON_STARTTIME_KEY = "opentelemetry-flask.starttime_key"
+_ENVIRON_SPAN_KEY = "opentelemetry-flask.span_key"
+_ENVIRON_ACTIVATION_KEY = "opentelemetry-flask.activation_key"
+_ENVIRON_REQCTX_REF_KEY = "opentelemetry-flask.reqctx_ref_key"
+_ENVIRON_TOKEN = "opentelemetry-flask.token"
+
+_excluded_urls_from_env = get_excluded_urls("FLASK")
+
+flask_version = version("flask")
+
+if package_version.parse(flask_version) >= package_version.parse("2.2.0"):
+
+    def _request_ctx_ref() -> weakref.ReferenceType:
+        return weakref.ref(flask.globals.request_ctx._get_current_object())
+
+else:
+
+    def _request_ctx_ref() -> weakref.ReferenceType:
+        return weakref.ref(flask._request_ctx_stack.top)
+
+
+def get_default_span_name():
+    method = sanitize_method(
+        flask.request.environ.get("REQUEST_METHOD", "").strip()
+    )
+    if method == "_OTHER":
+        method = "HTTP"
+    try:
+        span_name = f"{method} {flask.request.url_rule.rule}"
+    except AttributeError:
+        span_name = otel_wsgi.get_default_span_name(flask.request.environ)
+    return span_name
+
+
+def _rewrapped_app(
+    wsgi_app,
+    active_requests_counter,
+    duration_histogram_old=None,
+    response_hook=None,
+    excluded_urls=None,
+    sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+    duration_histogram_new=None,
+):
+    def _wrapped_app(wrapped_app_environ, start_response):
+        # We want to measure the time for route matching, etc.
+        # In theory, we could start the span here and use
+        # update_name later but that API is "highly discouraged" so
+        # we better avoid it.
+        wrapped_app_environ[_ENVIRON_STARTTIME_KEY] = time_ns()
+        start = default_timer()
+        attributes = otel_wsgi.collect_request_attributes(
+            wrapped_app_environ, sem_conv_opt_in_mode
+        )
+        active_requests_count_attrs = (
+            otel_wsgi._parse_active_request_count_attrs(
+                attributes,
+                sem_conv_opt_in_mode,
+            )
+        )
+
+        active_requests_counter.add(1, active_requests_count_attrs)
+        request_route = None
+
+        def _start_response(status, response_headers, *args, **kwargs):
+            if flask.request and (
+                excluded_urls is None
+                or not excluded_urls.url_disabled(flask.request.url)
+            ):
+                nonlocal request_route
+                request_route = flask.request.url_rule
+
+                span = flask.request.environ.get(_ENVIRON_SPAN_KEY)
+
+                propagator = get_global_response_propagator()
+                if propagator:
+                    propagator.inject(
+                        response_headers,
+                        setter=otel_wsgi.default_response_propagation_setter,
+                    )
+
+                if span:
+                    otel_wsgi.add_response_attributes(
+                        span,
+                        status,
+                        response_headers,
+                        attributes,
+                        sem_conv_opt_in_mode,
+                    )
+                    if (
+                        span.is_recording()
+                        and span.kind == trace.SpanKind.SERVER
+                    ):
+                        custom_attributes = otel_wsgi.collect_custom_response_headers_attributes(
+                            response_headers
+                        )
+                        if len(custom_attributes) > 0:
+                            span.set_attributes(custom_attributes)
+                else:
+                    _logger.warning(
+                        "Flask environ's OpenTelemetry span "
+                        "missing at _start_response(%s)",
+                        status,
+                    )
+                if response_hook is not None:
+                    response_hook(span, status, response_headers)
+            return start_response(status, response_headers, *args, **kwargs)
+
+        result = wsgi_app(wrapped_app_environ, _start_response)
+        duration_s = default_timer() - start
+        if duration_histogram_old:
+            duration_attrs_old = otel_wsgi._parse_duration_attrs(
+                attributes, _StabilityMode.DEFAULT
+            )
+
+            if request_route:
+                # http.target to be included in old semantic conventions
+                duration_attrs_old[SpanAttributes.HTTP_TARGET] = str(
+                    request_route
+                )
+
+            duration_histogram_old.record(
+                max(round(duration_s * 1000), 0), duration_attrs_old
+            )
+        if duration_histogram_new:
+            duration_attrs_new = otel_wsgi._parse_duration_attrs(
+                attributes, _StabilityMode.HTTP
+            )
+
+            if request_route:
+                duration_attrs_new[HTTP_ROUTE] = str(request_route)
+
+            duration_histogram_new.record(
+                max(duration_s, 0), duration_attrs_new
+            )
+        active_requests_counter.add(-1, active_requests_count_attrs)
+        return result
+
+    return _wrapped_app
+
+
+def _wrapped_before_request(
+    request_hook=None,
+    tracer=None,
+    excluded_urls=None,
+    enable_commenter=True,
+    commenter_options=None,
+    sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+):
+    def _before_request():
+        if excluded_urls and excluded_urls.url_disabled(flask.request.url):
+            return
+        flask_request_environ = flask.request.environ
+        span_name = get_default_span_name()
+
+        attributes = otel_wsgi.collect_request_attributes(
+            flask_request_environ,
+            sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+        )
+        if flask.request.url_rule:
+            # For 404 that result from no route found, etc, we
+            # don't have a url_rule.
+            attributes[SpanAttributes.HTTP_ROUTE] = flask.request.url_rule.rule
+        span, token = _start_internal_or_server_span(
+            tracer=tracer,
+            span_name=span_name,
+            start_time=flask_request_environ.get(_ENVIRON_STARTTIME_KEY),
+            context_carrier=flask_request_environ,
+            context_getter=otel_wsgi.wsgi_getter,
+            attributes=attributes,
+        )
+
+        if request_hook:
+            request_hook(span, flask_request_environ)
+
+        if span.is_recording():
+            for key, value in attributes.items():
+                span.set_attribute(key, value)
+            if span.is_recording() and span.kind == trace.SpanKind.SERVER:
+                custom_attributes = (
+                    otel_wsgi.collect_custom_request_headers_attributes(
+                        flask_request_environ
+                    )
+                )
+                if len(custom_attributes) > 0:
+                    span.set_attributes(custom_attributes)
+
+        activation = trace.use_span(span, end_on_exit=True)
+        activation.__enter__()  # pylint: disable=E1101
+        flask_request_environ[_ENVIRON_ACTIVATION_KEY] = activation
+        flask_request_environ[_ENVIRON_REQCTX_REF_KEY] = _request_ctx_ref()
+        flask_request_environ[_ENVIRON_SPAN_KEY] = span
+        flask_request_environ[_ENVIRON_TOKEN] = token
+
+        if enable_commenter:
+            current_context = context.get_current()
+            flask_info = {}
+
+            # https://flask.palletsprojects.com/en/1.1.x/api/#flask.has_request_context
+            if flask and flask.request:
+                if commenter_options.get("framework", True):
+                    flask_info["framework"] = f"flask:{flask_version}"
+                if (
+                    commenter_options.get("controller", True)
+                    and flask.request.endpoint
+                ):
+                    flask_info["controller"] = flask.request.endpoint
+                if (
+                    commenter_options.get("route", True)
+                    and flask.request.url_rule
+                    and flask.request.url_rule.rule
+                ):
+                    flask_info["route"] = flask.request.url_rule.rule
+            sqlcommenter_context = context.set_value(
+                "SQLCOMMENTER_ORM_TAGS_AND_VALUES", flask_info, current_context
+            )
+            context.attach(sqlcommenter_context)
+
+    return _before_request
+
+
+def _wrapped_teardown_request(
+    excluded_urls=None,
+):
+    def _teardown_request(exc):
+        # pylint: disable=E1101
+        if excluded_urls and excluded_urls.url_disabled(flask.request.url):
+            return
+
+        activation = flask.request.environ.get(_ENVIRON_ACTIVATION_KEY)
+
+        original_reqctx_ref = flask.request.environ.get(
+            _ENVIRON_REQCTX_REF_KEY
+        )
+        current_reqctx_ref = _request_ctx_ref()
+        if not activation or original_reqctx_ref != current_reqctx_ref:
+            # This request didn't start a span, maybe because it was created in
+            # a way that doesn't run `before_request`, like when it is created
+            # with `app.test_request_context`.
+            #
+            # Similarly, check that the request_ctx that created the span
+            # matches the current request_ctx, and only tear down if they match.
+            # This situation can arise if the original request_ctx handling
+            # the request calls functions that push new request_ctx's,
+            # like any decorated with `flask.copy_current_request_context`.
+
+            return
+        if exc is None:
+            activation.__exit__(None, None, None)
+        else:
+            activation.__exit__(
+                type(exc), exc, getattr(exc, "__traceback__", None)
+            )
+
+        if flask.request.environ.get(_ENVIRON_TOKEN, None):
+            context.detach(flask.request.environ.get(_ENVIRON_TOKEN))
+
+    return _teardown_request
+
+
+class _InstrumentedFlask(flask.Flask):
+    _excluded_urls = None
+    _tracer_provider = None
+    _request_hook = None
+    _response_hook = None
+    _enable_commenter = True
+    _commenter_options = None
+    _meter_provider = None
+    _sem_conv_opt_in_mode = _StabilityMode.DEFAULT
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self._original_wsgi_app = self.wsgi_app
+        self._is_instrumented_by_opentelemetry = True
+
+        meter = get_meter(
+            __name__,
+            __version__,
+            _InstrumentedFlask._meter_provider,
+            schema_url=_get_schema_url(
+                _InstrumentedFlask._sem_conv_opt_in_mode
+            ),
+        )
+        duration_histogram_old = None
+        if _report_old(_InstrumentedFlask._sem_conv_opt_in_mode):
+            duration_histogram_old = meter.create_histogram(
+                name=MetricInstruments.HTTP_SERVER_DURATION,
+                unit="ms",
+                description="Measures the duration of inbound HTTP requests.",
+            )
+        duration_histogram_new = None
+        if _report_new(_InstrumentedFlask._sem_conv_opt_in_mode):
+            duration_histogram_new = meter.create_histogram(
+                name=HTTP_SERVER_REQUEST_DURATION,
+                unit="s",
+                description="Duration of HTTP server requests.",
+            )
+        active_requests_counter = meter.create_up_down_counter(
+            name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
+            unit="requests",
+            description="measures the number of concurrent HTTP requests that are currently in-flight",
+        )
+
+        self.wsgi_app = _rewrapped_app(
+            self.wsgi_app,
+            active_requests_counter,
+            duration_histogram_old,
+            _InstrumentedFlask._response_hook,
+            excluded_urls=_InstrumentedFlask._excluded_urls,
+            sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode,
+            duration_histogram_new=duration_histogram_new,
+        )
+
+        tracer = trace.get_tracer(
+            __name__,
+            __version__,
+            _InstrumentedFlask._tracer_provider,
+            schema_url=_get_schema_url(
+                _InstrumentedFlask._sem_conv_opt_in_mode
+            ),
+        )
+
+        _before_request = _wrapped_before_request(
+            _InstrumentedFlask._request_hook,
+            tracer,
+            excluded_urls=_InstrumentedFlask._excluded_urls,
+            enable_commenter=_InstrumentedFlask._enable_commenter,
+            commenter_options=_InstrumentedFlask._commenter_options,
+            sem_conv_opt_in_mode=_InstrumentedFlask._sem_conv_opt_in_mode,
+        )
+        self._before_request = _before_request
+        self.before_request(_before_request)
+
+        _teardown_request = _wrapped_teardown_request(
+            excluded_urls=_InstrumentedFlask._excluded_urls,
+        )
+        self.teardown_request(_teardown_request)
+
+
+class FlaskInstrumentor(BaseInstrumentor):
+    # pylint: disable=protected-access,attribute-defined-outside-init
+    """An instrumentor for flask.Flask
+
+    See `BaseInstrumentor`
+    """
+
+    def instrumentation_dependencies(self) -> Collection[str]:
+        return _instruments
+
+    def _instrument(self, **kwargs):
+        self._original_flask = flask.Flask
+        request_hook = kwargs.get("request_hook")
+        response_hook = kwargs.get("response_hook")
+        if callable(request_hook):
+            _InstrumentedFlask._request_hook = request_hook
+        if callable(response_hook):
+            _InstrumentedFlask._response_hook = response_hook
+        tracer_provider = kwargs.get("tracer_provider")
+        _InstrumentedFlask._tracer_provider = tracer_provider
+        excluded_urls = kwargs.get("excluded_urls")
+        _InstrumentedFlask._excluded_urls = (
+            _excluded_urls_from_env
+            if excluded_urls is None
+            else parse_excluded_urls(excluded_urls)
+        )
+        enable_commenter = kwargs.get("enable_commenter", True)
+        _InstrumentedFlask._enable_commenter = enable_commenter
+
+        commenter_options = kwargs.get("commenter_options", {})
+        _InstrumentedFlask._commenter_options = commenter_options
+        meter_provider = kwargs.get("meter_provider")
+        _InstrumentedFlask._meter_provider = meter_provider
+
+        sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+            _OpenTelemetryStabilitySignalType.HTTP,
+        )
+
+        _InstrumentedFlask._sem_conv_opt_in_mode = sem_conv_opt_in_mode
+
+        flask.Flask = _InstrumentedFlask
+
+    def _uninstrument(self, **kwargs):
+        flask.Flask = self._original_flask
+
+    # pylint: disable=too-many-locals
+    @staticmethod
+    def instrument_app(
+        app,
+        request_hook=None,
+        response_hook=None,
+        tracer_provider=None,
+        excluded_urls=None,
+        enable_commenter=True,
+        commenter_options=None,
+        meter_provider=None,
+    ):
+        if not hasattr(app, "_is_instrumented_by_opentelemetry"):
+            app._is_instrumented_by_opentelemetry = False
+
+        if not app._is_instrumented_by_opentelemetry:
+            # initialize semantic conventions opt-in if needed
+            _OpenTelemetrySemanticConventionStability._initialize()
+            sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+                _OpenTelemetryStabilitySignalType.HTTP,
+            )
+            excluded_urls = (
+                parse_excluded_urls(excluded_urls)
+                if excluded_urls is not None
+                else _excluded_urls_from_env
+            )
+            meter = get_meter(
+                __name__,
+                __version__,
+                meter_provider,
+                schema_url=_get_schema_url(sem_conv_opt_in_mode),
+            )
+            duration_histogram_old = None
+            if _report_old(sem_conv_opt_in_mode):
+                duration_histogram_old = meter.create_histogram(
+                    name=MetricInstruments.HTTP_SERVER_DURATION,
+                    unit="ms",
+                    description="Measures the duration of inbound HTTP requests.",
+                )
+            duration_histogram_new = None
+            if _report_new(sem_conv_opt_in_mode):
+                duration_histogram_new = meter.create_histogram(
+                    name=HTTP_SERVER_REQUEST_DURATION,
+                    unit="s",
+                    description="Duration of HTTP server requests.",
+                )
+            active_requests_counter = meter.create_up_down_counter(
+                name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
+                unit="{request}",
+                description="Number of active HTTP server requests.",
+            )
+
+            app._original_wsgi_app = app.wsgi_app
+            app.wsgi_app = _rewrapped_app(
+                app.wsgi_app,
+                active_requests_counter,
+                duration_histogram_old,
+                response_hook=response_hook,
+                excluded_urls=excluded_urls,
+                sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+                duration_histogram_new=duration_histogram_new,
+            )
+
+            tracer = trace.get_tracer(
+                __name__,
+                __version__,
+                tracer_provider,
+                schema_url=_get_schema_url(sem_conv_opt_in_mode),
+            )
+
+            _before_request = _wrapped_before_request(
+                request_hook,
+                tracer,
+                excluded_urls=excluded_urls,
+                enable_commenter=enable_commenter,
+                commenter_options=(
+                    commenter_options if commenter_options else {}
+                ),
+                sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+            )
+            app._before_request = _before_request
+            app.before_request(_before_request)
+
+            _teardown_request = _wrapped_teardown_request(
+                excluded_urls=excluded_urls,
+            )
+            app._teardown_request = _teardown_request
+            app.teardown_request(_teardown_request)
+            app._is_instrumented_by_opentelemetry = True
+        else:
+            _logger.warning(
+                "Attempting to instrument Flask app while already instrumented"
+            )
+
+    @staticmethod
+    def uninstrument_app(app):
+        if hasattr(app, "_original_wsgi_app"):
+            app.wsgi_app = app._original_wsgi_app
+
+            # FIXME add support for other Flask blueprints that are not None
+            app.before_request_funcs[None].remove(app._before_request)
+            app.teardown_request_funcs[None].remove(app._teardown_request)
+            del app._original_wsgi_app
+            app._is_instrumented_by_opentelemetry = False
+        else:
+            _logger.warning(
+                "Attempting to uninstrument Flask "
+                "app while already uninstrumented"
+            )
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py
new file mode 100644
index 00000000..150ca0ca
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/package.py
@@ -0,0 +1,20 @@
+# 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 = ("flask >= 1.0",)
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/flask/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/instrumentor.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/instrumentor.py
new file mode 100644
index 00000000..cf079dbf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/instrumentor.py
@@ -0,0 +1,139 @@
+# 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.
+# type: ignore
+
+"""
+OpenTelemetry Base Instrumentor
+"""
+
+from __future__ import annotations
+
+from abc import ABC, abstractmethod
+from logging import getLogger
+from typing import Any, Collection
+
+from opentelemetry.instrumentation._semconv import (
+    _OpenTelemetrySemanticConventionStability,
+)
+from opentelemetry.instrumentation.dependencies import (
+    DependencyConflict,
+    get_dependency_conflicts,
+)
+
+_LOG = getLogger(__name__)
+
+
+class BaseInstrumentor(ABC):
+    """An ABC for instrumentors.
+
+    Child classes of this ABC should instrument specific third
+    party libraries or frameworks either by using the
+    ``opentelemetry-instrument`` command or by calling their methods
+    directly.
+
+    Since every third party library or framework is different and has different
+    instrumentation needs, more methods can be added to the child classes as
+    needed to provide practical instrumentation to the end user.
+    """
+
+    _instance = None
+    _is_instrumented_by_opentelemetry = False
+
+    def __new__(cls, *args, **kwargs):
+        if cls._instance is None:
+            cls._instance = object.__new__(cls)
+
+        return cls._instance
+
+    @property
+    def is_instrumented_by_opentelemetry(self):
+        return self._is_instrumented_by_opentelemetry
+
+    @abstractmethod
+    def instrumentation_dependencies(self) -> Collection[str]:
+        """Return a list of python packages with versions that the will be instrumented.
+
+        The format should be the same as used in requirements.txt or pyproject.toml.
+
+        For example, if an instrumentation instruments requests 1.x, this method should look
+        like:
+
+            def instrumentation_dependencies(self) -> Collection[str]:
+                return ['requests ~= 1.0']
+
+        This will ensure that the instrumentation will only be used when the specified library
+        is present in the environment.
+        """
+
+    def _instrument(self, **kwargs: Any):
+        """Instrument the library"""
+
+    @abstractmethod
+    def _uninstrument(self, **kwargs: Any):
+        """Uninstrument the library"""
+
+    def _check_dependency_conflicts(self) -> DependencyConflict | None:
+        dependencies = self.instrumentation_dependencies()
+        return get_dependency_conflicts(dependencies)
+
+    def instrument(self, **kwargs: Any):
+        """Instrument the library
+
+        This method will be called without any optional arguments by the
+        ``opentelemetry-instrument`` command.
+
+        This means that calling this method directly without passing any
+        optional values should do the very same thing that the
+        ``opentelemetry-instrument`` command does.
+        """
+
+        if self._is_instrumented_by_opentelemetry:
+            _LOG.warning("Attempting to instrument while already instrumented")
+            return None
+
+        # check if instrumentor has any missing or conflicting dependencies
+        skip_dep_check = kwargs.pop("skip_dep_check", False)
+        if not skip_dep_check:
+            conflict = self._check_dependency_conflicts()
+            if conflict:
+                _LOG.error(conflict)
+                return None
+
+        # initialize semantic conventions opt-in if needed
+        _OpenTelemetrySemanticConventionStability._initialize()
+
+        result = self._instrument(  # pylint: disable=assignment-from-no-return
+            **kwargs
+        )
+        self._is_instrumented_by_opentelemetry = True
+        return result
+
+    def uninstrument(self, **kwargs: Any):
+        """Uninstrument the library
+
+        See ``BaseInstrumentor.instrument`` for more information regarding the
+        usage of ``kwargs``.
+        """
+
+        if self._is_instrumented_by_opentelemetry:
+            result = self._uninstrument(**kwargs)
+            self._is_instrumented_by_opentelemetry = False
+            return result
+
+        _LOG.warning("Attempting to uninstrument while already uninstrumented")
+
+        return None
+
+
+__all__ = ["BaseInstrumentor"]
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/propagators.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/propagators.py
new file mode 100644
index 00000000..01859599
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/propagators.py
@@ -0,0 +1,125 @@
+# 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.
+
+"""
+This module implements experimental propagators to inject trace context
+into response carriers. This is useful for server side frameworks that start traces
+when server requests and want to share the trace context with the client so the
+client can add its spans to the same trace.
+
+This is part of an upcoming W3C spec and will eventually make it to the Otel spec.
+
+https://w3c.github.io/trace-context/#trace-context-http-response-headers-format
+"""
+
+import typing
+from abc import ABC, abstractmethod
+
+from opentelemetry import trace
+from opentelemetry.context.context import Context
+from opentelemetry.propagators import textmap
+from opentelemetry.trace import format_span_id, format_trace_id
+
+_HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS = "Access-Control-Expose-Headers"
+_RESPONSE_PROPAGATOR = None
+
+
+def get_global_response_propagator():
+    return _RESPONSE_PROPAGATOR
+
+
+def set_global_response_propagator(propagator):
+    global _RESPONSE_PROPAGATOR  # pylint:disable=global-statement
+    _RESPONSE_PROPAGATOR = propagator
+
+
+class Setter(ABC):
+    @abstractmethod
+    def set(self, carrier, key, value):
+        """Inject the provided key value pair in carrier."""
+
+
+class DictHeaderSetter(Setter):
+    def set(self, carrier, key, value):  # pylint: disable=no-self-use
+        old_value = carrier.get(key, "")
+        if old_value:
+            value = f"{old_value}, {value}"
+        carrier[key] = value
+
+
+class FuncSetter(Setter):
+    """FuncSetter converts a function into a valid Setter. Any function that
+    can set values in a carrier can be converted into a Setter by using
+    FuncSetter. This is useful when injecting trace context into non-dict
+    objects such HTTP Response objects for different framework.
+
+    For example, it can be used to create a setter for Falcon response object
+    as:
+
+        setter = FuncSetter(falcon.api.Response.append_header)
+
+    and then used with the propagator as:
+
+        propagator.inject(falcon_response, setter=setter)
+
+    This would essentially make the propagator call `falcon_response.append_header(key, value)`
+    """
+
+    def __init__(self, func):
+        self._func = func
+
+    def set(self, carrier, key, value):
+        self._func(carrier, key, value)
+
+
+default_setter = DictHeaderSetter()
+
+
+class ResponsePropagator(ABC):
+    @abstractmethod
+    def inject(
+        self,
+        carrier: textmap.CarrierT,
+        context: typing.Optional[Context] = None,
+        setter: textmap.Setter = default_setter,
+    ) -> None:
+        """Injects SpanContext into the HTTP response carrier."""
+
+
+class TraceResponsePropagator(ResponsePropagator):
+    """Experimental propagator that injects tracecontext into HTTP responses."""
+
+    def inject(
+        self,
+        carrier: textmap.CarrierT,
+        context: typing.Optional[Context] = None,
+        setter: textmap.Setter = default_setter,
+    ) -> None:
+        """Injects SpanContext into the HTTP response carrier."""
+        span = trace.get_current_span(context)
+        span_context = span.get_span_context()
+        if span_context == trace.INVALID_SPAN_CONTEXT:
+            return
+
+        header_name = "traceresponse"
+        setter.set(
+            carrier,
+            header_name,
+            f"00-{format_trace_id(span_context.trace_id)}-{format_span_id(span_context.span_id)}-{span_context.trace_flags:02x}",
+        )
+        setter.set(
+            carrier,
+            _HTTP_HEADER_ACCESS_CONTROL_EXPOSE_HEADERS,
+            header_name,
+        )
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/__init__.py
new file mode 100644
index 00000000..022c59f0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/__init__.py
@@ -0,0 +1,336 @@
+# 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.
+
+"""
+The integration with PostgreSQL supports the `Psycopg`_ library, it can be enabled by
+using ``Psycopg2Instrumentor``.
+
+.. _Psycopg: http://initd.org/psycopg/
+
+SQLCOMMENTER
+*****************************************
+You can optionally configure Psycopg2 instrumentation to enable sqlcommenter which enriches
+the query with contextual information.
+
+Usage
+-----
+
+.. code:: python
+
+    from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
+
+    Psycopg2Instrumentor().instrument(enable_commenter=True, commenter_options={})
+
+
+For example,
+::
+
+   Invoking cursor.execute("select * from auth_users") 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 /*tag=value*/;"
+
+
+SQLCommenter Configurations
+***************************
+We can configure the tags to be appended to the sqlquery log by adding configuration inside commenter_options(default:{}) keyword
+
+db_driver = True(Default) or False
+
+For example,
+::
+Enabling this flag will add psycopg2 and it's version which is /*psycopg2%%3A2.9.3*/
+
+dbapi_threadsafety = True(Default) or False
+
+For example,
+::
+Enabling this flag will add threadsafety /*dbapi_threadsafety=2*/
+
+dbapi_level = True(Default) or False
+
+For example,
+::
+Enabling this flag will add dbapi_level /*dbapi_level='2.0'*/
+
+libpq_version = True(Default) or False
+
+For example,
+::
+Enabling this flag will add libpq_version /*libpq_version=140001*/
+
+driver_paramstyle = True(Default) or False
+
+For example,
+::
+Enabling this flag will add driver_paramstyle /*driver_paramstyle='pyformat'*/
+
+opentelemetry_values = True(Default) or False
+
+For example,
+::
+Enabling this flag will add traceparent values /*traceparent='00-03afa25236b8cd948fa853d67038ac79-405ff022e8247c46-01'*/
+
+SQLComment in span attribute
+****************************
+If sqlcommenter is enabled, you can optionally configure psycopg2 instrumentation to append sqlcomment to query span attribute for convenience of your platform.
+
+.. code:: python
+
+    from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
+
+    Psycopg2Instrumentor().instrument(
+        enable_commenter=True,
+        enable_attribute_commenter=True,
+    )
+
+
+For example,
+::
+
+    Invoking cursor.execute("select * from auth_users") will lead to postgresql query "select * from auth_users" but when SQLCommenter and attribute_commenter are enabled
+    the query will get appended with some configurable tags like "select * from auth_users /*tag=value*/;" for both server query and `db.statement` span attribute.
+
+Usage
+-----
+
+.. code-block:: python
+
+    import psycopg2
+    from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
+
+    # Call instrument() to wrap all database connections
+    Psycopg2Instrumentor().instrument()
+
+    cnx = psycopg2.connect(database='Database')
+
+    cursor = cnx.cursor()
+    cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
+    cursor.execute("INSERT INTO test (testField) VALUES (123)")
+    cursor.close()
+    cnx.close()
+
+.. code-block:: python
+
+    import psycopg2
+    from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
+
+    # Alternatively, use instrument_connection for an individual connection
+    cnx = psycopg2.connect(database='Database')
+    instrumented_cnx = Psycopg2Instrumentor().instrument_connection(cnx)
+    cursor = instrumented_cnx.cursor()
+    cursor.execute("CREATE TABLE IF NOT EXISTS test (testField INTEGER)")
+    cursor.execute("INSERT INTO test (testField) VALUES (123)")
+    cursor.close()
+    instrumented_cnx.close()
+
+API
+---
+"""
+
+import logging
+import typing
+from importlib.metadata import PackageNotFoundError, distribution
+from typing import Collection
+
+import psycopg2
+from psycopg2.extensions import (
+    cursor as pg_cursor,  # pylint: disable=no-name-in-module
+)
+from psycopg2.sql import Composed  # pylint: disable=no-name-in-module
+
+from opentelemetry.instrumentation import dbapi
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.psycopg2.package import (
+    _instruments,
+    _instruments_psycopg2,
+    _instruments_psycopg2_binary,
+)
+from opentelemetry.instrumentation.psycopg2.version import __version__
+
+_logger = logging.getLogger(__name__)
+_OTEL_CURSOR_FACTORY_KEY = "_otel_orig_cursor_factory"
+
+
+class Psycopg2Instrumentor(BaseInstrumentor):
+    _CONNECTION_ATTRIBUTES = {
+        "database": "info.dbname",
+        "port": "info.port",
+        "host": "info.host",
+        "user": "info.user",
+    }
+
+    _DATABASE_SYSTEM = "postgresql"
+
+    def instrumentation_dependencies(self) -> Collection[str]:
+        # Determine which package of psycopg2 is installed
+        # Right now there are two packages, psycopg2 and psycopg2-binary
+        # The latter is a binary wheel package that does not require a compiler
+        try:
+            distribution("psycopg2")
+            return (_instruments_psycopg2,)
+        except PackageNotFoundError:
+            pass
+
+        try:
+            distribution("psycopg2-binary")
+            return (_instruments_psycopg2_binary,)
+        except PackageNotFoundError:
+            pass
+
+        return _instruments
+
+    def _instrument(self, **kwargs):
+        """Integrate with PostgreSQL Psycopg library.
+        Psycopg: http://initd.org/psycopg/
+        """
+        tracer_provider = kwargs.get("tracer_provider")
+        enable_sqlcommenter = kwargs.get("enable_commenter", False)
+        commenter_options = kwargs.get("commenter_options", {})
+        enable_attribute_commenter = kwargs.get(
+            "enable_attribute_commenter", False
+        )
+        dbapi.wrap_connect(
+            __name__,
+            psycopg2,
+            "connect",
+            self._DATABASE_SYSTEM,
+            self._CONNECTION_ATTRIBUTES,
+            version=__version__,
+            tracer_provider=tracer_provider,
+            db_api_integration_factory=DatabaseApiIntegration,
+            enable_commenter=enable_sqlcommenter,
+            commenter_options=commenter_options,
+            enable_attribute_commenter=enable_attribute_commenter,
+        )
+
+    def _uninstrument(self, **kwargs):
+        """ "Disable Psycopg2 instrumentation"""
+        dbapi.unwrap_connect(psycopg2, "connect")
+
+    # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
+    @staticmethod
+    def instrument_connection(connection, tracer_provider=None):
+        """Enable instrumentation in a psycopg2 connection.
+
+        Args:
+            connection: psycopg2.extensions.connection
+                The psycopg2 connection object to be instrumented.
+            tracer_provider: opentelemetry.trace.TracerProvider, optional
+                The TracerProvider to use for instrumentation. If not specified,
+                the global TracerProvider will be used.
+
+        Returns:
+            An instrumented psycopg2 connection object.
+        """
+
+        if not hasattr(connection, "_is_instrumented_by_opentelemetry"):
+            connection._is_instrumented_by_opentelemetry = False
+
+        if not connection._is_instrumented_by_opentelemetry:
+            setattr(
+                connection, _OTEL_CURSOR_FACTORY_KEY, connection.cursor_factory
+            )
+            connection.cursor_factory = _new_cursor_factory(
+                tracer_provider=tracer_provider
+            )
+            connection._is_instrumented_by_opentelemetry = True
+        else:
+            _logger.warning(
+                "Attempting to instrument Psycopg connection while already instrumented"
+            )
+        return connection
+
+    # TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
+    @staticmethod
+    def uninstrument_connection(connection):
+        connection.cursor_factory = getattr(
+            connection, _OTEL_CURSOR_FACTORY_KEY, None
+        )
+
+        return connection
+
+
+# TODO(owais): check if core dbapi can do this for all dbapi implementations e.g, pymysql and mysql
+class DatabaseApiIntegration(dbapi.DatabaseApiIntegration):
+    def wrapped_connection(
+        self,
+        connect_method: typing.Callable[..., typing.Any],
+        args: typing.Tuple[typing.Any, typing.Any],
+        kwargs: typing.Dict[typing.Any, typing.Any],
+    ):
+        """Add object proxy to connection object."""
+        base_cursor_factory = kwargs.pop("cursor_factory", None)
+        new_factory_kwargs = {"db_api": self}
+        if base_cursor_factory:
+            new_factory_kwargs["base_factory"] = base_cursor_factory
+        kwargs["cursor_factory"] = _new_cursor_factory(**new_factory_kwargs)
+        connection = connect_method(*args, **kwargs)
+        self.get_connection_attributes(connection)
+        return connection
+
+
+class CursorTracer(dbapi.CursorTracer):
+    def get_operation_name(self, cursor, args):
+        if not args:
+            return ""
+
+        statement = args[0]
+        if isinstance(statement, Composed):
+            statement = statement.as_string(cursor)
+
+        if isinstance(statement, str):
+            # Strip leading comments so we get the operation name.
+            return self._leading_comment_remover.sub("", statement).split()[0]
+
+        return ""
+
+    def get_statement(self, cursor, args):
+        if not args:
+            return ""
+
+        statement = args[0]
+        if isinstance(statement, Composed):
+            statement = statement.as_string(cursor)
+        return statement
+
+
+def _new_cursor_factory(db_api=None, base_factory=None, tracer_provider=None):
+    if not db_api:
+        db_api = DatabaseApiIntegration(
+            __name__,
+            Psycopg2Instrumentor._DATABASE_SYSTEM,
+            connection_attributes=Psycopg2Instrumentor._CONNECTION_ATTRIBUTES,
+            version=__version__,
+            tracer_provider=tracer_provider,
+        )
+
+    base_factory = base_factory or pg_cursor
+    _cursor_tracer = CursorTracer(db_api)
+
+    class TracedCursorFactory(base_factory):
+        def execute(self, *args, **kwargs):
+            return _cursor_tracer.traced_execution(
+                self, super().execute, *args, **kwargs
+            )
+
+        def executemany(self, *args, **kwargs):
+            return _cursor_tracer.traced_execution(
+                self, super().executemany, *args, **kwargs
+            )
+
+        def callproc(self, *args, **kwargs):
+            return _cursor_tracer.traced_execution(
+                self, super().callproc, *args, **kwargs
+            )
+
+    return TracedCursorFactory
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/package.py
new file mode 100644
index 00000000..b1bf9290
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/package.py
@@ -0,0 +1,22 @@
+# 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_psycopg2 = "psycopg2 >= 2.7.3.1"
+_instruments_psycopg2_binary = "psycopg2-binary >= 2.7.3.1"
+
+_instruments = (
+    _instruments_psycopg2,
+    _instruments_psycopg2_binary,
+)
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/psycopg2/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/py.typed b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/py.typed
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/__init__.py
new file mode 100644
index 00000000..1940e2f6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/__init__.py
@@ -0,0 +1,469 @@
+# 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.
+
+"""
+This library allows tracing HTTP requests made by the
+`requests <https://requests.readthedocs.io/en/master/>`_ library.
+
+Usage
+-----
+
+.. code-block:: python
+
+    import requests
+    from opentelemetry.instrumentation.requests import RequestsInstrumentor
+
+    # You can optionally pass a custom TracerProvider to instrument().
+    RequestsInstrumentor().instrument()
+    response = requests.get(url="https://www.example.org/")
+
+Configuration
+-------------
+
+Request/Response hooks
+**********************
+
+The requests instrumentation supports extending tracing behavior with the help of
+request and response hooks. These are functions that are called back by the instrumentation
+right after a Span is created for a request and right before the span is finished processing a response respectively.
+The hooks can be configured as follows:
+
+.. code:: python
+
+    import requests
+    from opentelemetry.instrumentation.requests import RequestsInstrumentor
+
+    # `request_obj` is an instance of requests.PreparedRequest
+    def request_hook(span, request_obj):
+        pass
+
+    # `request_obj` is an instance of requests.PreparedRequest
+    # `response` is an instance of requests.Response
+    def response_hook(span, request_obj, response):
+        pass
+
+    RequestsInstrumentor().instrument(
+        request_hook=request_hook, response_hook=response_hook
+    )
+
+Exclude lists
+*************
+To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_REQUESTS_EXCLUDED_URLS``
+(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
+
+For example,
+
+::
+
+    export OTEL_PYTHON_REQUESTS_EXCLUDED_URLS="client/.*/info,healthcheck"
+
+will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
+
+API
+---
+"""
+
+from __future__ import annotations
+
+import functools
+import types
+from timeit import default_timer
+from typing import Any, Callable, Collection, Optional
+from urllib.parse import urlparse
+
+from requests.models import PreparedRequest, Response
+from requests.sessions import Session
+from requests.structures import CaseInsensitiveDict
+
+from opentelemetry.instrumentation._semconv import (
+    _client_duration_attrs_new,
+    _client_duration_attrs_old,
+    _filter_semconv_duration_attrs,
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _report_new,
+    _report_old,
+    _set_http_host_client,
+    _set_http_method,
+    _set_http_net_peer_name_client,
+    _set_http_network_protocol_version,
+    _set_http_peer_port_client,
+    _set_http_scheme,
+    _set_http_url,
+    _set_status,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.requests.package import _instruments
+from opentelemetry.instrumentation.requests.version import __version__
+from opentelemetry.instrumentation.utils import (
+    is_http_instrumentation_enabled,
+    suppress_http_instrumentation,
+)
+from opentelemetry.metrics import Histogram, get_meter
+from opentelemetry.propagate import inject
+from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
+from opentelemetry.semconv.attributes.network_attributes import (
+    NETWORK_PEER_ADDRESS,
+    NETWORK_PEER_PORT,
+)
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+    HTTP_CLIENT_REQUEST_DURATION,
+)
+from opentelemetry.trace import SpanKind, Tracer, get_tracer
+from opentelemetry.trace.span import Span
+from opentelemetry.util.http import (
+    ExcludeList,
+    get_excluded_urls,
+    parse_excluded_urls,
+    remove_url_credentials,
+    sanitize_method,
+)
+from opentelemetry.util.http.httplib import set_ip_on_next_http_connection
+
+_excluded_urls_from_env = get_excluded_urls("REQUESTS")
+
+_RequestHookT = Optional[Callable[[Span, PreparedRequest], None]]
+_ResponseHookT = Optional[Callable[[Span, PreparedRequest, Response], None]]
+
+
+def _set_http_status_code_attribute(
+    span,
+    status_code,
+    metric_attributes=None,
+    sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+):
+    status_code_str = str(status_code)
+    try:
+        status_code = int(status_code)
+    except ValueError:
+        status_code = -1
+    if metric_attributes is None:
+        metric_attributes = {}
+    # When we have durations we should set metrics only once
+    # Also the decision to include status code on a histogram should
+    # not be dependent on tracing decisions.
+    _set_status(
+        span,
+        metric_attributes,
+        status_code,
+        status_code_str,
+        server_span=False,
+        sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+    )
+
+
+# pylint: disable=unused-argument
+# pylint: disable=R0915
+def _instrument(
+    tracer: Tracer,
+    duration_histogram_old: Histogram,
+    duration_histogram_new: Histogram,
+    request_hook: _RequestHookT = None,
+    response_hook: _ResponseHookT = None,
+    excluded_urls: ExcludeList | None = None,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    """Enables tracing of all requests calls that go through
+    :code:`requests.session.Session.request` (this includes
+    :code:`requests.get`, etc.)."""
+
+    # Since
+    # https://github.com/psf/requests/commit/d72d1162142d1bf8b1b5711c664fbbd674f349d1
+    # (v0.7.0, Oct 23, 2011), get, post, etc are implemented via request which
+    # again, is implemented via Session.request (`Session` was named `session`
+    # before v1.0.0, Dec 17, 2012, see
+    # https://github.com/psf/requests/commit/4e5c4a6ab7bb0195dececdd19bb8505b872fe120)
+
+    wrapped_send = Session.send
+
+    # pylint: disable-msg=too-many-locals,too-many-branches
+    @functools.wraps(wrapped_send)
+    def instrumented_send(
+        self: Session, request: PreparedRequest, **kwargs: Any
+    ):
+        if excluded_urls and excluded_urls.url_disabled(request.url):
+            return wrapped_send(self, request, **kwargs)
+
+        def get_or_create_headers():
+            request.headers = (
+                request.headers
+                if request.headers is not None
+                else CaseInsensitiveDict()
+            )
+            return request.headers
+
+        if not is_http_instrumentation_enabled():
+            return wrapped_send(self, request, **kwargs)
+
+        # See
+        # https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/http-spans.md#http-client
+        method = request.method
+        span_name = get_default_span_name(method)
+
+        url = remove_url_credentials(request.url)
+
+        span_attributes = {}
+        _set_http_method(
+            span_attributes,
+            method,
+            sanitize_method(method),
+            sem_conv_opt_in_mode,
+        )
+        _set_http_url(span_attributes, url, sem_conv_opt_in_mode)
+
+        metric_labels = {}
+        _set_http_method(
+            metric_labels,
+            method,
+            sanitize_method(method),
+            sem_conv_opt_in_mode,
+        )
+
+        try:
+            parsed_url = urlparse(url)
+            if parsed_url.scheme:
+                if _report_old(sem_conv_opt_in_mode):
+                    # TODO: Support opt-in for url.scheme in new semconv
+                    _set_http_scheme(
+                        metric_labels, parsed_url.scheme, sem_conv_opt_in_mode
+                    )
+            if parsed_url.hostname:
+                _set_http_host_client(
+                    metric_labels, parsed_url.hostname, sem_conv_opt_in_mode
+                )
+                _set_http_net_peer_name_client(
+                    metric_labels, parsed_url.hostname, sem_conv_opt_in_mode
+                )
+                if _report_new(sem_conv_opt_in_mode):
+                    _set_http_host_client(
+                        span_attributes,
+                        parsed_url.hostname,
+                        sem_conv_opt_in_mode,
+                    )
+                    # Use semconv library when available
+                    span_attributes[NETWORK_PEER_ADDRESS] = parsed_url.hostname
+            if parsed_url.port:
+                _set_http_peer_port_client(
+                    metric_labels, parsed_url.port, sem_conv_opt_in_mode
+                )
+                if _report_new(sem_conv_opt_in_mode):
+                    _set_http_peer_port_client(
+                        span_attributes, parsed_url.port, sem_conv_opt_in_mode
+                    )
+                    # Use semconv library when available
+                    span_attributes[NETWORK_PEER_PORT] = parsed_url.port
+        except ValueError:
+            pass
+
+        with tracer.start_as_current_span(
+            span_name, kind=SpanKind.CLIENT, attributes=span_attributes
+        ) as span, set_ip_on_next_http_connection(span):
+            exception = None
+            if callable(request_hook):
+                request_hook(span, request)
+
+            headers = get_or_create_headers()
+            inject(headers)
+
+            with suppress_http_instrumentation():
+                start_time = default_timer()
+                try:
+                    result = wrapped_send(
+                        self, request, **kwargs
+                    )  # *** PROCEED
+                except Exception as exc:  # pylint: disable=W0703
+                    exception = exc
+                    result = getattr(exc, "response", None)
+                finally:
+                    elapsed_time = max(default_timer() - start_time, 0)
+
+            if isinstance(result, Response):
+                span_attributes = {}
+                _set_http_status_code_attribute(
+                    span,
+                    result.status_code,
+                    metric_labels,
+                    sem_conv_opt_in_mode,
+                )
+
+                if result.raw is not None:
+                    version = getattr(result.raw, "version", None)
+                    if version:
+                        # Only HTTP/1 is supported by requests
+                        version_text = "1.1" if version == 11 else "1.0"
+                        _set_http_network_protocol_version(
+                            metric_labels, version_text, sem_conv_opt_in_mode
+                        )
+                        if _report_new(sem_conv_opt_in_mode):
+                            _set_http_network_protocol_version(
+                                span_attributes,
+                                version_text,
+                                sem_conv_opt_in_mode,
+                            )
+                for key, val in span_attributes.items():
+                    span.set_attribute(key, val)
+
+                if callable(response_hook):
+                    response_hook(span, request, result)
+
+            if exception is not None and _report_new(sem_conv_opt_in_mode):
+                span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
+                metric_labels[ERROR_TYPE] = type(exception).__qualname__
+
+            if duration_histogram_old is not None:
+                duration_attrs_old = _filter_semconv_duration_attrs(
+                    metric_labels,
+                    _client_duration_attrs_old,
+                    _client_duration_attrs_new,
+                    _StabilityMode.DEFAULT,
+                )
+                duration_histogram_old.record(
+                    max(round(elapsed_time * 1000), 0),
+                    attributes=duration_attrs_old,
+                )
+            if duration_histogram_new is not None:
+                duration_attrs_new = _filter_semconv_duration_attrs(
+                    metric_labels,
+                    _client_duration_attrs_old,
+                    _client_duration_attrs_new,
+                    _StabilityMode.HTTP,
+                )
+                duration_histogram_new.record(
+                    elapsed_time, attributes=duration_attrs_new
+                )
+
+            if exception is not None:
+                raise exception.with_traceback(exception.__traceback__)
+
+        return result
+
+    instrumented_send.opentelemetry_instrumentation_requests_applied = True
+    Session.send = instrumented_send
+
+
+def _uninstrument():
+    """Disables instrumentation of :code:`requests` through this module.
+
+    Note that this only works if no other module also patches requests."""
+    _uninstrument_from(Session)
+
+
+def _uninstrument_from(instr_root, restore_as_bound_func: bool = False):
+    for instr_func_name in ("request", "send"):
+        instr_func = getattr(instr_root, instr_func_name)
+        if not getattr(
+            instr_func,
+            "opentelemetry_instrumentation_requests_applied",
+            False,
+        ):
+            continue
+
+        original = instr_func.__wrapped__  # pylint:disable=no-member
+        if restore_as_bound_func:
+            original = types.MethodType(original, instr_root)
+        setattr(instr_root, instr_func_name, original)
+
+
+def get_default_span_name(method: str) -> str:
+    """
+    Default implementation for name_callback, returns HTTP {method_name}.
+    https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
+
+    Args:
+        method: string representing HTTP method
+    Returns:
+        span name
+    """
+    method = sanitize_method(method.strip())
+    if method == "_OTHER":
+        return "HTTP"
+    return method
+
+
+class RequestsInstrumentor(BaseInstrumentor):
+    """An instrumentor for requests
+    See `BaseInstrumentor`
+    """
+
+    def instrumentation_dependencies(self) -> Collection[str]:
+        return _instruments
+
+    def _instrument(self, **kwargs: Any):
+        """Instruments requests module
+
+        Args:
+            **kwargs: Optional arguments
+                ``tracer_provider``: a TracerProvider, defaults to global
+                ``request_hook``: An optional callback that is invoked right after a span is created.
+                ``response_hook``: An optional callback which is invoked right before the span is finished processing a response.
+                ``excluded_urls``: A string containing a comma-delimited
+                    list of regexes used to exclude URLs from tracking
+        """
+        semconv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+            _OpenTelemetryStabilitySignalType.HTTP,
+        )
+        schema_url = _get_schema_url(semconv_opt_in_mode)
+        tracer_provider = kwargs.get("tracer_provider")
+        tracer = get_tracer(
+            __name__,
+            __version__,
+            tracer_provider,
+            schema_url=schema_url,
+        )
+        excluded_urls = kwargs.get("excluded_urls")
+        meter_provider = kwargs.get("meter_provider")
+        meter = get_meter(
+            __name__,
+            __version__,
+            meter_provider,
+            schema_url=schema_url,
+        )
+        duration_histogram_old = None
+        if _report_old(semconv_opt_in_mode):
+            duration_histogram_old = meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_DURATION,
+                unit="ms",
+                description="measures the duration of the outbound HTTP request",
+            )
+        duration_histogram_new = None
+        if _report_new(semconv_opt_in_mode):
+            duration_histogram_new = meter.create_histogram(
+                name=HTTP_CLIENT_REQUEST_DURATION,
+                unit="s",
+                description="Duration of HTTP client requests.",
+            )
+        _instrument(
+            tracer,
+            duration_histogram_old,
+            duration_histogram_new,
+            request_hook=kwargs.get("request_hook"),
+            response_hook=kwargs.get("response_hook"),
+            excluded_urls=(
+                _excluded_urls_from_env
+                if excluded_urls is None
+                else parse_excluded_urls(excluded_urls)
+            ),
+            sem_conv_opt_in_mode=semconv_opt_in_mode,
+        )
+
+    def _uninstrument(self, **kwargs: Any):
+        _uninstrument()
+
+    @staticmethod
+    def uninstrument_session(session: Session):
+        """Disables instrumentation on the session object."""
+        _uninstrument_from(session, restore_as_bound_func=True)
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/package.py
new file mode 100644
index 00000000..9cd93a91
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/package.py
@@ -0,0 +1,20 @@
+# 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 = ("requests ~= 2.0",)
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/py.typed b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/py.typed
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/requests/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/sqlcommenter_utils.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/sqlcommenter_utils.py
new file mode 100644
index 00000000..1eeefbf2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/sqlcommenter_utils.py
@@ -0,0 +1,66 @@
+# 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 opentelemetry import context
+from opentelemetry.instrumentation.utils import _url_quote
+
+
+def _add_sql_comment(sql, **meta) -> str:
+    """
+    Appends comments to the sql statement and returns it
+    """
+    meta.update(**_add_framework_tags())
+    comment = _generate_sql_comment(**meta)
+    sql = sql.rstrip()
+    if sql[-1] == ";":
+        sql = sql[:-1] + comment + ";"
+    else:
+        sql = sql + comment
+    return sql
+
+
+def _generate_sql_comment(**meta) -> str:
+    """
+    Return a SQL comment with comma delimited key=value pairs created from
+    **meta kwargs.
+    """
+    key_value_delimiter = ","
+
+    if not meta:  # No entries added.
+        return ""
+
+    # Sort the keywords to ensure that caching works and that testing is
+    # deterministic. It eases visual inspection as well.
+    return (
+        " /*"
+        + key_value_delimiter.join(
+            f"{_url_quote(key)}={_url_quote(value)!r}"
+            for key, value in sorted(meta.items())
+            if value is not None
+        )
+        + "*/"
+    )
+
+
+def _add_framework_tags() -> dict:
+    """
+    Returns orm related tags if any set by the context
+    """
+
+    sqlcommenter_framework_values = (
+        context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES")
+        if context.get_value("SQLCOMMENTER_ORM_TAGS_AND_VALUES")
+        else {}
+    )
+    return sqlcommenter_framework_values
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py
new file mode 100644
index 00000000..a80e6d07
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/__init__.py
@@ -0,0 +1,477 @@
+# 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.
+
+"""
+This library allows tracing HTTP requests made by the
+`urllib <https://docs.python.org/3/library/urllib>`_ library.
+
+Usage
+-----
+.. code-block:: python
+
+    from urllib import request
+    from opentelemetry.instrumentation.urllib import URLLibInstrumentor
+
+    # You can optionally pass a custom TracerProvider to
+    # URLLibInstrumentor().instrument()
+
+    URLLibInstrumentor().instrument()
+    req = request.Request('https://postman-echo.com/post', method="POST")
+    r = request.urlopen(req)
+
+Configuration
+-------------
+
+Request/Response hooks
+**********************
+
+The urllib instrumentation supports extending tracing behavior with the help of
+request and response hooks. These are functions that are called back by the instrumentation
+right after a Span is created for a request and right before the span is finished processing a response respectively.
+The hooks can be configured as follows:
+
+.. code:: python
+
+    from http.client import HTTPResponse
+    from urllib.request import Request
+
+    from opentelemetry.instrumentation.urllib import URLLibInstrumentor
+    from opentelemetry.trace import Span
+
+
+    def request_hook(span: Span, request: Request):
+        pass
+
+
+    def response_hook(span: Span, request: Request, response: HTTPResponse):
+        pass
+
+
+    URLLibInstrumentor().instrument(
+        request_hook=request_hook,
+        response_hook=response_hook
+    )
+
+Exclude lists
+*************
+
+To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB_EXCLUDED_URLS``
+(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
+
+For example,
+
+::
+
+    export OTEL_PYTHON_URLLIB_EXCLUDED_URLS="client/.*/info,healthcheck"
+
+will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
+
+API
+---
+"""
+
+from __future__ import annotations
+
+import functools
+import types
+import typing
+from http import client
+from timeit import default_timer
+from typing import Any, Collection
+from urllib.request import (  # pylint: disable=no-name-in-module,import-error
+    OpenerDirector,
+    Request,
+)
+
+from opentelemetry.instrumentation._semconv import (
+    _client_duration_attrs_new,
+    _client_duration_attrs_old,
+    _filter_semconv_duration_attrs,
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _report_new,
+    _report_old,
+    _set_http_method,
+    _set_http_network_protocol_version,
+    _set_http_url,
+    _set_status,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.urllib.package import _instruments
+from opentelemetry.instrumentation.urllib.version import __version__
+from opentelemetry.instrumentation.utils import (
+    is_http_instrumentation_enabled,
+    suppress_http_instrumentation,
+)
+from opentelemetry.metrics import Histogram, Meter, get_meter
+from opentelemetry.propagate import inject
+from opentelemetry.semconv._incubating.metrics.http_metrics import (
+    HTTP_CLIENT_REQUEST_BODY_SIZE,
+    HTTP_CLIENT_RESPONSE_BODY_SIZE,
+    create_http_client_request_body_size,
+    create_http_client_response_body_size,
+)
+from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+    HTTP_CLIENT_REQUEST_DURATION,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer
+from opentelemetry.util.http import (
+    ExcludeList,
+    get_excluded_urls,
+    parse_excluded_urls,
+    remove_url_credentials,
+    sanitize_method,
+)
+from opentelemetry.util.types import Attributes
+
+_excluded_urls_from_env = get_excluded_urls("URLLIB")
+
+_RequestHookT = typing.Optional[typing.Callable[[Span, Request], None]]
+_ResponseHookT = typing.Optional[
+    typing.Callable[[Span, Request, client.HTTPResponse], None]
+]
+
+
+class URLLibInstrumentor(BaseInstrumentor):
+    """An instrumentor for urllib
+    See `BaseInstrumentor`
+    """
+
+    def instrumentation_dependencies(self) -> Collection[str]:
+        return _instruments
+
+    def _instrument(self, **kwargs: Any):
+        """Instruments urllib module
+
+        Args:
+            **kwargs: Optional arguments
+                ``tracer_provider``: a TracerProvider, defaults to global
+                ``request_hook``: An optional callback invoked that is invoked right after a span is created.
+                ``response_hook``: An optional callback which is invoked right before the span is finished processing a response
+                ``excluded_urls``: A string containing a comma-delimited
+                    list of regexes used to exclude URLs from tracking
+        """
+        # initialize semantic conventions opt-in if needed
+        _OpenTelemetrySemanticConventionStability._initialize()
+        sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+            _OpenTelemetryStabilitySignalType.HTTP,
+        )
+        schema_url = _get_schema_url(sem_conv_opt_in_mode)
+        tracer_provider = kwargs.get("tracer_provider")
+        tracer = get_tracer(
+            __name__,
+            __version__,
+            tracer_provider,
+            schema_url=schema_url,
+        )
+        excluded_urls = kwargs.get("excluded_urls")
+        meter_provider = kwargs.get("meter_provider")
+        meter = get_meter(
+            __name__,
+            __version__,
+            meter_provider,
+            schema_url=schema_url,
+        )
+
+        histograms = _create_client_histograms(meter, sem_conv_opt_in_mode)
+
+        _instrument(
+            tracer,
+            histograms,
+            request_hook=kwargs.get("request_hook"),
+            response_hook=kwargs.get("response_hook"),
+            excluded_urls=(
+                _excluded_urls_from_env
+                if excluded_urls is None
+                else parse_excluded_urls(excluded_urls)
+            ),
+            sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+        )
+
+    def _uninstrument(self, **kwargs: Any):
+        _uninstrument()
+
+    def uninstrument_opener(self, opener: OpenerDirector):  # pylint: disable=no-self-use
+        """uninstrument_opener a specific instance of urllib.request.OpenerDirector"""
+        _uninstrument_from(opener, restore_as_bound_func=True)
+
+
+# pylint: disable=too-many-statements
+def _instrument(
+    tracer: Tracer,
+    histograms: dict[str, Histogram],
+    request_hook: _RequestHookT = None,
+    response_hook: _ResponseHookT = None,
+    excluded_urls: ExcludeList | None = None,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    """Enables tracing of all requests calls that go through
+    :code:`urllib.Client._make_request`"""
+
+    opener_open = OpenerDirector.open
+
+    @functools.wraps(opener_open)
+    def instrumented_open(opener, fullurl, data=None, timeout=None):
+        if isinstance(fullurl, str):
+            request_ = Request(fullurl, data)
+        else:
+            request_ = fullurl
+
+        def get_or_create_headers():
+            return getattr(request_, "headers", {})
+
+        def call_wrapped():
+            return opener_open(opener, request_, data=data, timeout=timeout)
+
+        return _instrumented_open_call(
+            opener, request_, call_wrapped, get_or_create_headers
+        )
+
+    def _instrumented_open_call(
+        _, request, call_wrapped, get_or_create_headers
+    ):  # pylint: disable=too-many-locals
+        if not is_http_instrumentation_enabled():
+            return call_wrapped()
+
+        url = request.full_url
+        if excluded_urls and excluded_urls.url_disabled(url):
+            return call_wrapped()
+
+        method = request.get_method().upper()
+
+        span_name = _get_span_name(method)
+
+        url = remove_url_credentials(url)
+
+        data = getattr(request, "data", None)
+        request_size = 0 if data is None else len(data)
+
+        labels = {}
+
+        _set_http_method(
+            labels,
+            method,
+            sanitize_method(method),
+            sem_conv_opt_in_mode,
+        )
+        _set_http_url(labels, url, sem_conv_opt_in_mode)
+
+        with tracer.start_as_current_span(
+            span_name, kind=SpanKind.CLIENT, attributes=labels
+        ) as span:
+            exception = None
+            if callable(request_hook):
+                request_hook(span, request)
+
+            headers = get_or_create_headers()
+            inject(headers)
+
+            with suppress_http_instrumentation():
+                start_time = default_timer()
+                try:
+                    result = call_wrapped()  # *** PROCEED
+                except Exception as exc:  # pylint: disable=W0703
+                    exception = exc
+                    result = getattr(exc, "file", None)
+                finally:
+                    duration_s = default_timer() - start_time
+            response_size = 0
+            if result is not None:
+                response_size = int(result.headers.get("Content-Length", 0))
+                code_ = result.getcode()
+                # set http status code based on semconv
+                if code_:
+                    _set_status_code_attribute(
+                        span, code_, labels, sem_conv_opt_in_mode
+                    )
+
+                ver_ = str(getattr(result, "version", ""))
+                if ver_:
+                    _set_http_network_protocol_version(
+                        labels, f"{ver_[:1]}.{ver_[:-1]}", sem_conv_opt_in_mode
+                    )
+
+            if exception is not None and _report_new(sem_conv_opt_in_mode):
+                span.set_attribute(ERROR_TYPE, type(exception).__qualname__)
+                labels[ERROR_TYPE] = type(exception).__qualname__
+
+            duration_attrs_old = _filter_semconv_duration_attrs(
+                labels,
+                _client_duration_attrs_old,
+                _client_duration_attrs_new,
+                sem_conv_opt_in_mode=_StabilityMode.DEFAULT,
+            )
+            duration_attrs_new = _filter_semconv_duration_attrs(
+                labels,
+                _client_duration_attrs_old,
+                _client_duration_attrs_new,
+                sem_conv_opt_in_mode=_StabilityMode.HTTP,
+            )
+
+            duration_attrs_old[SpanAttributes.HTTP_URL] = url
+
+            _record_histograms(
+                histograms,
+                duration_attrs_old,
+                duration_attrs_new,
+                request_size,
+                response_size,
+                duration_s,
+                sem_conv_opt_in_mode,
+            )
+
+            if callable(response_hook):
+                response_hook(span, request, result)
+
+            if exception is not None:
+                raise exception.with_traceback(exception.__traceback__)
+
+        return result
+
+    instrumented_open.opentelemetry_instrumentation_urllib_applied = True
+    OpenerDirector.open = instrumented_open
+
+
+def _uninstrument():
+    """Disables instrumentation of :code:`urllib` through this module.
+
+    Note that this only works if no other module also patches urllib."""
+    _uninstrument_from(OpenerDirector)
+
+
+def _uninstrument_from(instr_root, restore_as_bound_func: bool = False):
+    instr_func_name = "open"
+    instr_func = getattr(instr_root, instr_func_name)
+    if not getattr(
+        instr_func,
+        "opentelemetry_instrumentation_urllib_applied",
+        False,
+    ):
+        return
+
+    original = instr_func.__wrapped__  # pylint:disable=no-member
+    if restore_as_bound_func:
+        original = types.MethodType(original, instr_root)
+    setattr(instr_root, instr_func_name, original)
+
+
+def _get_span_name(method: str) -> str:
+    method = sanitize_method(method.strip())
+    if method == "_OTHER":
+        method = "HTTP"
+    return method
+
+
+def _set_status_code_attribute(
+    span: Span,
+    status_code: int,
+    metric_attributes: dict[str, Any] | None = None,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+) -> None:
+    status_code_str = str(status_code)
+    try:
+        status_code = int(status_code)
+    except ValueError:
+        status_code = -1
+
+    if metric_attributes is None:
+        metric_attributes = {}
+
+    _set_status(
+        span,
+        metric_attributes,
+        status_code,
+        status_code_str,
+        server_span=False,
+        sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+    )
+
+
+def _create_client_histograms(
+    meter: Meter, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT
+) -> dict[str, Histogram]:
+    histograms = {}
+    if _report_old(sem_conv_opt_in_mode):
+        histograms[MetricInstruments.HTTP_CLIENT_DURATION] = (
+            meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_DURATION,
+                unit="ms",
+                description="Measures the duration of the outbound HTTP request",
+            )
+        )
+        histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE] = (
+            meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
+                unit="By",
+                description="Measures the size of HTTP request messages.",
+            )
+        )
+        histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE] = (
+            meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
+                unit="By",
+                description="Measures the size of HTTP response messages.",
+            )
+        )
+    if _report_new(sem_conv_opt_in_mode):
+        histograms[HTTP_CLIENT_REQUEST_DURATION] = meter.create_histogram(
+            name=HTTP_CLIENT_REQUEST_DURATION,
+            unit="s",
+            description="Duration of HTTP client requests.",
+        )
+        histograms[HTTP_CLIENT_REQUEST_BODY_SIZE] = (
+            create_http_client_request_body_size(meter)
+        )
+        histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE] = (
+            create_http_client_response_body_size(meter)
+        )
+
+    return histograms
+
+
+def _record_histograms(
+    histograms: dict[str, Histogram],
+    metric_attributes_old: Attributes,
+    metric_attributes_new: Attributes,
+    request_size: int,
+    response_size: int,
+    duration_s: float,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    if _report_old(sem_conv_opt_in_mode):
+        duration = max(round(duration_s * 1000), 0)
+        histograms[MetricInstruments.HTTP_CLIENT_DURATION].record(
+            duration, attributes=metric_attributes_old
+        )
+        histograms[MetricInstruments.HTTP_CLIENT_REQUEST_SIZE].record(
+            request_size, attributes=metric_attributes_old
+        )
+        histograms[MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE].record(
+            response_size, attributes=metric_attributes_old
+        )
+    if _report_new(sem_conv_opt_in_mode):
+        histograms[HTTP_CLIENT_REQUEST_DURATION].record(
+            duration_s, attributes=metric_attributes_new
+        )
+        histograms[HTTP_CLIENT_REQUEST_BODY_SIZE].record(
+            request_size, attributes=metric_attributes_new
+        )
+        histograms[HTTP_CLIENT_RESPONSE_BODY_SIZE].record(
+            response_size, attributes=metric_attributes_new
+        )
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/package.py
new file mode 100644
index 00000000..2dbb1905
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/package.py
@@ -0,0 +1,21 @@
+# 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 __future__ import annotations
+
+_instruments: tuple[str, ...] = tuple()
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/py.typed b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/py.typed
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/__init__.py
new file mode 100644
index 00000000..551c67f7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/__init__.py
@@ -0,0 +1,599 @@
+# 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.
+
+"""
+This library allows tracing HTTP requests made by the
+`urllib3 <https://urllib3.readthedocs.io/>`_ library.
+
+Usage
+-----
+.. code-block:: python
+
+    import urllib3
+    from opentelemetry.instrumentation.urllib3 import URLLib3Instrumentor
+
+    def strip_query_params(url: str) -> str:
+        return url.split("?")[0]
+
+    URLLib3Instrumentor().instrument(
+        # Remove all query params from the URL attribute on the span.
+        url_filter=strip_query_params,
+    )
+
+    http = urllib3.PoolManager()
+    response = http.request("GET", "https://www.example.org/")
+
+Configuration
+-------------
+
+Request/Response hooks
+**********************
+
+The urllib3 instrumentation supports extending tracing behavior with the help of
+request and response hooks. These are functions that are called back by the instrumentation
+right after a Span is created for a request and right before the span is finished processing a response respectively.
+The hooks can be configured as follows:
+
+.. code:: python
+
+    from typing import Any
+
+    from urllib3.connectionpool import HTTPConnectionPool
+    from urllib3.response import HTTPResponse
+
+    from opentelemetry.instrumentation.urllib3 import RequestInfo, URLLib3Instrumentor
+    from opentelemetry.trace import Span
+
+    def request_hook(
+        span: Span,
+        pool: HTTPConnectionPool,
+        request_info: RequestInfo,
+    ) -> Any:
+        pass
+
+    def response_hook(
+        span: Span,
+        pool: HTTPConnectionPool,
+        response: HTTPResponse,
+    ) -> Any:
+        pass
+
+    URLLib3Instrumentor().instrument(
+        request_hook=request_hook,
+        response_hook=response_hook,
+    )
+
+Exclude lists
+*************
+
+To exclude certain URLs from being tracked, set the environment variable ``OTEL_PYTHON_URLLIB3_EXCLUDED_URLS``
+(or ``OTEL_PYTHON_EXCLUDED_URLS`` as fallback) with comma delimited regexes representing which URLs to exclude.
+
+For example,
+
+::
+
+    export OTEL_PYTHON_URLLIB3_EXCLUDED_URLS="client/.*/info,healthcheck"
+
+will exclude requests such as ``https://site/client/123/info`` and ``https://site/xyz/healthcheck``.
+
+API
+---
+"""
+
+import collections.abc
+import io
+import typing
+from dataclasses import dataclass
+from timeit import default_timer
+from typing import Collection
+
+import urllib3.connectionpool
+import wrapt
+
+from opentelemetry.instrumentation._semconv import (
+    _client_duration_attrs_new,
+    _client_duration_attrs_old,
+    _filter_semconv_duration_attrs,
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _report_new,
+    _report_old,
+    _set_http_host_client,
+    _set_http_method,
+    _set_http_net_peer_name_client,
+    _set_http_network_protocol_version,
+    _set_http_peer_port_client,
+    _set_http_scheme,
+    _set_http_url,
+    _set_status,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
+from opentelemetry.instrumentation.urllib3.package import _instruments
+from opentelemetry.instrumentation.urllib3.version import __version__
+from opentelemetry.instrumentation.utils import (
+    is_http_instrumentation_enabled,
+    suppress_http_instrumentation,
+    unwrap,
+)
+from opentelemetry.metrics import Histogram, get_meter
+from opentelemetry.propagate import inject
+from opentelemetry.semconv._incubating.metrics.http_metrics import (
+    create_http_client_request_body_size,
+    create_http_client_response_body_size,
+)
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+    HTTP_CLIENT_REQUEST_DURATION,
+)
+from opentelemetry.trace import Span, SpanKind, Tracer, get_tracer
+from opentelemetry.util.http import (
+    ExcludeList,
+    get_excluded_urls,
+    parse_excluded_urls,
+    sanitize_method,
+)
+from opentelemetry.util.http.httplib import set_ip_on_next_http_connection
+
+_excluded_urls_from_env = get_excluded_urls("URLLIB3")
+
+
+@dataclass
+class RequestInfo:
+    """Arguments that were passed to the ``urlopen()`` call."""
+
+    __slots__ = ("method", "url", "headers", "body")
+
+    # The type annotations here come from ``HTTPConnectionPool.urlopen()``.
+    method: str
+    url: str
+    headers: typing.Optional[typing.Mapping[str, str]]
+    body: typing.Union[
+        bytes, typing.IO[typing.Any], typing.Iterable[bytes], str, None
+    ]
+
+
+_UrlFilterT = typing.Optional[typing.Callable[[str], str]]
+_RequestHookT = typing.Optional[
+    typing.Callable[
+        [
+            Span,
+            urllib3.connectionpool.HTTPConnectionPool,
+            RequestInfo,
+        ],
+        None,
+    ]
+]
+_ResponseHookT = typing.Optional[
+    typing.Callable[
+        [
+            Span,
+            urllib3.connectionpool.HTTPConnectionPool,
+            urllib3.response.HTTPResponse,
+        ],
+        None,
+    ]
+]
+
+_URL_OPEN_ARG_TO_INDEX_MAPPING = {
+    "method": 0,
+    "url": 1,
+    "body": 2,
+}
+
+
+class URLLib3Instrumentor(BaseInstrumentor):
+    def instrumentation_dependencies(self) -> Collection[str]:
+        return _instruments
+
+    def _instrument(self, **kwargs):
+        """Instruments the urllib3 module
+
+        Args:
+            **kwargs: Optional arguments
+                ``tracer_provider``: a TracerProvider, defaults to global.
+                ``request_hook``: An optional callback that is invoked right after a span is created.
+                ``response_hook``: An optional callback which is invoked right before the span is finished processing a response.
+                ``url_filter``: A callback to process the requested URL prior
+                    to adding it as a span attribute.
+                ``excluded_urls``: A string containing a comma-delimited
+                    list of regexes used to exclude URLs from tracking
+        """
+        # initialize semantic conventions opt-in if needed
+        _OpenTelemetrySemanticConventionStability._initialize()
+        sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+            _OpenTelemetryStabilitySignalType.HTTP,
+        )
+        schema_url = _get_schema_url(sem_conv_opt_in_mode)
+        tracer_provider = kwargs.get("tracer_provider")
+        tracer = get_tracer(
+            __name__,
+            __version__,
+            tracer_provider,
+            schema_url=schema_url,
+        )
+
+        excluded_urls = kwargs.get("excluded_urls")
+
+        meter_provider = kwargs.get("meter_provider")
+        meter = get_meter(
+            __name__,
+            __version__,
+            meter_provider,
+            schema_url=schema_url,
+        )
+        duration_histogram_old = None
+        request_size_histogram_old = None
+        response_size_histogram_old = None
+        if _report_old(sem_conv_opt_in_mode):
+            # http.client.duration histogram
+            duration_histogram_old = meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_DURATION,
+                unit="ms",
+                description="Measures the duration of the outbound HTTP request",
+            )
+            # http.client.request.size histogram
+            request_size_histogram_old = meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_REQUEST_SIZE,
+                unit="By",
+                description="Measures the size of HTTP request messages.",
+            )
+            # http.client.response.size histogram
+            response_size_histogram_old = meter.create_histogram(
+                name=MetricInstruments.HTTP_CLIENT_RESPONSE_SIZE,
+                unit="By",
+                description="Measures the size of HTTP response messages.",
+            )
+
+        duration_histogram_new = None
+        request_size_histogram_new = None
+        response_size_histogram_new = None
+        if _report_new(sem_conv_opt_in_mode):
+            # http.client.request.duration histogram
+            duration_histogram_new = meter.create_histogram(
+                name=HTTP_CLIENT_REQUEST_DURATION,
+                unit="s",
+                description="Duration of HTTP client requests.",
+            )
+            # http.client.request.body.size histogram
+            request_size_histogram_new = create_http_client_request_body_size(
+                meter
+            )
+            # http.client.response.body.size histogram
+            response_size_histogram_new = (
+                create_http_client_response_body_size(meter)
+            )
+        _instrument(
+            tracer,
+            duration_histogram_old,
+            duration_histogram_new,
+            request_size_histogram_old,
+            request_size_histogram_new,
+            response_size_histogram_old,
+            response_size_histogram_new,
+            request_hook=kwargs.get("request_hook"),
+            response_hook=kwargs.get("response_hook"),
+            url_filter=kwargs.get("url_filter"),
+            excluded_urls=(
+                _excluded_urls_from_env
+                if excluded_urls is None
+                else parse_excluded_urls(excluded_urls)
+            ),
+            sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+        )
+
+    def _uninstrument(self, **kwargs):
+        _uninstrument()
+
+
+def _get_span_name(method: str) -> str:
+    method = sanitize_method(method.strip())
+    if method == "_OTHER":
+        method = "HTTP"
+    return method
+
+
+def _instrument(
+    tracer: Tracer,
+    duration_histogram_old: Histogram,
+    duration_histogram_new: Histogram,
+    request_size_histogram_old: Histogram,
+    request_size_histogram_new: Histogram,
+    response_size_histogram_old: Histogram,
+    response_size_histogram_new: Histogram,
+    request_hook: _RequestHookT = None,
+    response_hook: _ResponseHookT = None,
+    url_filter: _UrlFilterT = None,
+    excluded_urls: ExcludeList = None,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    def instrumented_urlopen(wrapped, instance, args, kwargs):
+        if not is_http_instrumentation_enabled():
+            return wrapped(*args, **kwargs)
+
+        url = _get_url(instance, args, kwargs, url_filter)
+        if excluded_urls and excluded_urls.url_disabled(url):
+            return wrapped(*args, **kwargs)
+
+        method = _get_url_open_arg("method", args, kwargs).upper()
+        headers = _prepare_headers(kwargs)
+        body = _get_url_open_arg("body", args, kwargs)
+
+        span_name = _get_span_name(method)
+        span_attributes = {}
+
+        _set_http_method(
+            span_attributes,
+            method,
+            sanitize_method(method),
+            sem_conv_opt_in_mode,
+        )
+        _set_http_url(span_attributes, url, sem_conv_opt_in_mode)
+
+        with tracer.start_as_current_span(
+            span_name, kind=SpanKind.CLIENT, attributes=span_attributes
+        ) as span, set_ip_on_next_http_connection(span):
+            if callable(request_hook):
+                request_hook(
+                    span,
+                    instance,
+                    RequestInfo(
+                        method=method,
+                        url=url,
+                        headers=headers,
+                        body=body,
+                    ),
+                )
+            inject(headers)
+            # TODO: add error handling to also set exception `error.type` in new semconv
+            with suppress_http_instrumentation():
+                start_time = default_timer()
+                response = wrapped(*args, **kwargs)
+                duration_s = default_timer() - start_time
+            # set http status code based on semconv
+            metric_attributes = {}
+            _set_status_code_attribute(
+                span, response.status, metric_attributes, sem_conv_opt_in_mode
+            )
+
+            if callable(response_hook):
+                response_hook(span, instance, response)
+
+            request_size = _get_body_size(body)
+            response_size = int(response.headers.get("Content-Length", 0))
+
+            _set_metric_attributes(
+                metric_attributes,
+                instance,
+                response,
+                method,
+                sem_conv_opt_in_mode,
+            )
+
+            _record_metrics(
+                metric_attributes,
+                duration_histogram_old,
+                duration_histogram_new,
+                request_size_histogram_old,
+                request_size_histogram_new,
+                response_size_histogram_old,
+                response_size_histogram_new,
+                duration_s,
+                request_size,
+                response_size,
+                sem_conv_opt_in_mode,
+            )
+
+            return response
+
+    wrapt.wrap_function_wrapper(
+        urllib3.connectionpool.HTTPConnectionPool,
+        "urlopen",
+        instrumented_urlopen,
+    )
+
+
+def _get_url_open_arg(name: str, args: typing.List, kwargs: typing.Mapping):
+    arg_idx = _URL_OPEN_ARG_TO_INDEX_MAPPING.get(name)
+    if arg_idx is not None:
+        try:
+            return args[arg_idx]
+        except IndexError:
+            pass
+    return kwargs.get(name)
+
+
+def _get_url(
+    instance: urllib3.connectionpool.HTTPConnectionPool,
+    args: typing.List,
+    kwargs: typing.Mapping,
+    url_filter: _UrlFilterT,
+) -> str:
+    url_or_path = _get_url_open_arg("url", args, kwargs)
+    if not url_or_path.startswith("/"):
+        url = url_or_path
+    else:
+        url = instance.scheme + "://" + instance.host
+        if _should_append_port(instance.scheme, instance.port):
+            url += ":" + str(instance.port)
+        url += url_or_path
+
+    if url_filter:
+        return url_filter(url)
+    return url
+
+
+def _get_body_size(body: object) -> typing.Optional[int]:
+    if body is None:
+        return 0
+    if isinstance(body, collections.abc.Sized):
+        return len(body)
+    if isinstance(body, io.BytesIO):
+        return body.getbuffer().nbytes
+    return None
+
+
+def _should_append_port(scheme: str, port: typing.Optional[int]) -> bool:
+    if not port:
+        return False
+    if scheme == "http" and port == 80:
+        return False
+    if scheme == "https" and port == 443:
+        return False
+    return True
+
+
+def _prepare_headers(urlopen_kwargs: typing.Dict) -> typing.Dict:
+    headers = urlopen_kwargs.get("headers")
+
+    # avoid modifying original headers on inject
+    headers = headers.copy() if headers is not None else {}
+    urlopen_kwargs["headers"] = headers
+
+    return headers
+
+
+def _set_status_code_attribute(
+    span: Span,
+    status_code: int,
+    metric_attributes: dict = None,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+) -> None:
+    status_code_str = str(status_code)
+    try:
+        status_code = int(status_code)
+    except ValueError:
+        status_code = -1
+
+    if metric_attributes is None:
+        metric_attributes = {}
+
+    _set_status(
+        span,
+        metric_attributes,
+        status_code,
+        status_code_str,
+        server_span=False,
+        sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+    )
+
+
+def _set_metric_attributes(
+    metric_attributes: dict,
+    instance: urllib3.connectionpool.HTTPConnectionPool,
+    response: urllib3.response.HTTPResponse,
+    method: str,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+) -> None:
+    _set_http_host_client(
+        metric_attributes, instance.host, sem_conv_opt_in_mode
+    )
+    _set_http_scheme(metric_attributes, instance.scheme, sem_conv_opt_in_mode)
+    _set_http_method(
+        metric_attributes,
+        method,
+        sanitize_method(method),
+        sem_conv_opt_in_mode,
+    )
+    _set_http_net_peer_name_client(
+        metric_attributes, instance.host, sem_conv_opt_in_mode
+    )
+    _set_http_peer_port_client(
+        metric_attributes, instance.port, sem_conv_opt_in_mode
+    )
+
+    version = getattr(response, "version")
+    if version:
+        http_version = "1.1" if version == 11 else "1.0"
+        _set_http_network_protocol_version(
+            metric_attributes, http_version, sem_conv_opt_in_mode
+        )
+
+
+def _filter_attributes_semconv(
+    metric_attributes,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    duration_attrs_old = None
+    duration_attrs_new = None
+    if _report_old(sem_conv_opt_in_mode):
+        duration_attrs_old = _filter_semconv_duration_attrs(
+            metric_attributes,
+            _client_duration_attrs_old,
+            _client_duration_attrs_new,
+            _StabilityMode.DEFAULT,
+        )
+    if _report_new(sem_conv_opt_in_mode):
+        duration_attrs_new = _filter_semconv_duration_attrs(
+            metric_attributes,
+            _client_duration_attrs_old,
+            _client_duration_attrs_new,
+            _StabilityMode.HTTP,
+        )
+
+    return (duration_attrs_old, duration_attrs_new)
+
+
+def _record_metrics(
+    metric_attributes: dict,
+    duration_histogram_old: Histogram,
+    duration_histogram_new: Histogram,
+    request_size_histogram_old: Histogram,
+    request_size_histogram_new: Histogram,
+    response_size_histogram_old: Histogram,
+    response_size_histogram_new: Histogram,
+    duration_s: float,
+    request_size: typing.Optional[int],
+    response_size: int,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    attrs_old, attrs_new = _filter_attributes_semconv(
+        metric_attributes, sem_conv_opt_in_mode
+    )
+    if duration_histogram_old:
+        # Default behavior is to record the duration in milliseconds
+        duration_histogram_old.record(
+            max(round(duration_s * 1000), 0),
+            attributes=attrs_old,
+        )
+
+    if duration_histogram_new:
+        # New semconv record the duration in seconds
+        duration_histogram_new.record(
+            duration_s,
+            attributes=attrs_new,
+        )
+
+    if request_size is not None:
+        if request_size_histogram_old:
+            request_size_histogram_old.record(
+                request_size, attributes=attrs_old
+            )
+
+        if request_size_histogram_new:
+            request_size_histogram_new.record(
+                request_size, attributes=attrs_new
+            )
+
+    if response_size_histogram_old:
+        response_size_histogram_old.record(response_size, attributes=attrs_old)
+
+    if response_size_histogram_new:
+        response_size_histogram_new.record(response_size, attributes=attrs_new)
+
+
+def _uninstrument():
+    unwrap(urllib3.connectionpool.HTTPConnectionPool, "urlopen")
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/package.py
new file mode 100644
index 00000000..568120c4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/package.py
@@ -0,0 +1,20 @@
+# 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 = ("urllib3 >= 1.0.0, < 3.0.0",)
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/urllib3/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/utils.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/utils.py
new file mode 100644
index 00000000..d5bf5db7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/utils.py
@@ -0,0 +1,226 @@
+# 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 __future__ import annotations
+
+import urllib.parse
+from contextlib import contextmanager
+from importlib import import_module
+from re import escape, sub
+from typing import Any, Dict, Generator, Sequence
+
+from wrapt import ObjectProxy
+
+from opentelemetry import context, trace
+
+# pylint: disable=E0611
+# FIXME: fix the importing of these private attributes when the location of the _SUPPRESS_HTTP_INSTRUMENTATION_KEY is defined.=
+from opentelemetry.context import (
+    _SUPPRESS_HTTP_INSTRUMENTATION_KEY,
+    _SUPPRESS_INSTRUMENTATION_KEY,
+)
+
+# pylint: disable=E0611
+from opentelemetry.propagate import extract
+from opentelemetry.trace import StatusCode
+from opentelemetry.trace.propagation.tracecontext import (
+    TraceContextTextMapPropagator,
+)
+
+propagator = TraceContextTextMapPropagator()
+
+_SUPPRESS_INSTRUMENTATION_KEY_PLAIN = (
+    "suppress_instrumentation"  # Set for backward compatibility
+)
+
+
+def extract_attributes_from_object(
+    obj: Any, attributes: Sequence[str], existing: Dict[str, str] | None = None
+) -> Dict[str, str]:
+    extracted: dict[str, str] = {}
+    if existing:
+        extracted.update(existing)
+    for attr in attributes:
+        value = getattr(obj, attr, None)
+        if value is not None:
+            extracted[attr] = str(value)
+    return extracted
+
+
+def http_status_to_status_code(
+    status: int,
+    allow_redirect: bool = True,
+    server_span: bool = False,
+) -> StatusCode:
+    """Converts an HTTP status code to an OpenTelemetry canonical status code
+
+    Args:
+        status (int): HTTP status code
+    """
+    # See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#status
+    if not isinstance(status, int):
+        return StatusCode.UNSET
+
+    if status < 100:
+        return StatusCode.ERROR
+    if status <= 299:
+        return StatusCode.UNSET
+    if status <= 399 and allow_redirect:
+        return StatusCode.UNSET
+    if status <= 499 and server_span:
+        return StatusCode.UNSET
+    return StatusCode.ERROR
+
+
+def unwrap(obj: object, attr: str):
+    """Given a function that was wrapped by wrapt.wrap_function_wrapper, unwrap it
+
+    The object containing the function to unwrap may be passed as dotted module path string.
+
+    Args:
+        obj: Object that holds a reference to the wrapped function or dotted import path as string
+        attr (str): Name of the wrapped function
+    """
+    if isinstance(obj, str):
+        try:
+            module_path, class_name = obj.rsplit(".", 1)
+        except ValueError as exc:
+            raise ImportError(
+                f"Cannot parse '{obj}' as dotted import path"
+            ) from exc
+        module = import_module(module_path)
+        try:
+            obj = getattr(module, class_name)
+        except AttributeError as exc:
+            raise ImportError(
+                f"Cannot import '{class_name}' from '{module}'"
+            ) from exc
+
+    func = getattr(obj, attr, None)
+    if func and isinstance(func, ObjectProxy) and hasattr(func, "__wrapped__"):
+        setattr(obj, attr, func.__wrapped__)
+
+
+def _start_internal_or_server_span(
+    tracer,
+    span_name,
+    start_time,
+    context_carrier,
+    context_getter,
+    attributes=None,
+):
+    """Returns internal or server span along with the token which can be used by caller to reset context
+
+
+    Args:
+        tracer : tracer in use by given instrumentation library
+        span_name (string): name of the span
+        start_time : start time of the span
+        context_carrier : object which contains values that are
+            used to construct a Context. This object
+            must be paired with an appropriate getter
+            which understands how to extract a value from it.
+        context_getter : an object which contains a get function that can retrieve zero
+            or more values from the carrier and a keys function that can get all the keys
+            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()
+        span_kind = trace.SpanKind.INTERNAL
+    span = tracer.start_span(
+        name=span_name,
+        context=ctx,
+        kind=span_kind,
+        start_time=start_time,
+        attributes=attributes,
+    )
+    return span, token
+
+
+def _url_quote(s: Any) -> str:  # pylint: disable=invalid-name
+    if not isinstance(s, (str, bytes)):
+        return s
+    quoted = urllib.parse.quote(s)
+    # Since SQL uses '%' as a keyword, '%' is a by-product of url quoting
+    # e.g. foo,bar --> foo%2Cbar
+    # thus in our quoting, we need to escape it too to finally give
+    #      foo,bar --> foo%%2Cbar
+    return quoted.replace("%", "%%")
+
+
+def _get_opentelemetry_values() -> dict[str, Any]:
+    """
+    Return the OpenTelemetry Trace and Span IDs if Span ID is set in the
+    OpenTelemetry execution context.
+    """
+    # Insert the W3C TraceContext generated
+    _headers: dict[str, Any] = {}
+    propagator.inject(_headers)
+    return _headers
+
+
+def _python_path_without_directory(python_path, directory, path_separator):
+    return sub(
+        rf"{escape(directory)}{path_separator}(?!$)",
+        "",
+        python_path,
+    )
+
+
+def is_instrumentation_enabled() -> bool:
+    return not (
+        context.get_value(_SUPPRESS_INSTRUMENTATION_KEY)
+        or context.get_value(_SUPPRESS_INSTRUMENTATION_KEY_PLAIN)
+    )
+
+
+def is_http_instrumentation_enabled() -> bool:
+    return is_instrumentation_enabled() and not context.get_value(
+        _SUPPRESS_HTTP_INSTRUMENTATION_KEY
+    )
+
+
+@contextmanager
+def _suppress_instrumentation(*keys: str) -> Generator[None]:
+    """Suppress instrumentation within the context."""
+    ctx = context.get_current()
+    for key in keys:
+        ctx = context.set_value(key, True, ctx)
+    token = context.attach(ctx)
+    try:
+        yield
+    finally:
+        context.detach(token)
+
+
+@contextmanager
+def suppress_instrumentation() -> Generator[None]:
+    """Suppress instrumentation within the context."""
+    with _suppress_instrumentation(
+        _SUPPRESS_INSTRUMENTATION_KEY, _SUPPRESS_INSTRUMENTATION_KEY_PLAIN
+    ):
+        yield
+
+
+@contextmanager
+def suppress_http_instrumentation() -> Generator[None]:
+    """Suppress instrumentation within the context."""
+    with _suppress_instrumentation(_SUPPRESS_HTTP_INSTRUMENTATION_KEY):
+        yield
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/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"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/__init__.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/__init__.py
new file mode 100644
index 00000000..a0a2ce9a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/__init__.py
@@ -0,0 +1,747 @@
+# 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.
+"""
+This library provides a WSGI middleware that can be used on any WSGI framework
+(such as Django / Flask / Web.py) to track requests timing through OpenTelemetry.
+
+Usage (Flask)
+-------------
+
+.. code-block:: python
+
+    from flask import Flask
+    from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware
+
+    app = Flask(__name__)
+    app.wsgi_app = OpenTelemetryMiddleware(app.wsgi_app)
+
+    @app.route("/")
+    def hello():
+        return "Hello!"
+
+    if __name__ == "__main__":
+        app.run(debug=True)
+
+
+Usage (Django)
+--------------
+
+Modify the application's ``wsgi.py`` file as shown below.
+
+.. code-block:: python
+
+    import os
+    from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware
+    from django.core.wsgi import get_wsgi_application
+
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings')
+
+    application = get_wsgi_application()
+    application = OpenTelemetryMiddleware(application)
+
+Usage (Web.py)
+--------------
+
+.. code-block:: python
+
+    import web
+    from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware
+    from cheroot import wsgi
+
+    urls = ('/', 'index')
+
+
+    class index:
+
+        def GET(self):
+            return "Hello, world!"
+
+
+    if __name__ == "__main__":
+        app = web.application(urls, globals())
+        func = app.wsgifunc()
+
+        func = OpenTelemetryMiddleware(func)
+
+        server = wsgi.WSGIServer(
+            ("localhost", 5100), func, server_name="localhost"
+        )
+        server.start()
+
+Configuration
+-------------
+
+Request/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 client request hook is called with the internal span and an instance of WSGIEnvironment when the method
+  ``receive`` is called.
+- The client response hook is called with the internal span, the status of the response and a list of key-value (tuples)
+  representing the response headers returned from the response when the method ``send`` is called.
+
+For example,
+
+.. code-block:: python
+
+    from wsgiref.types import WSGIEnvironment, StartResponse
+    from opentelemetry.instrumentation.wsgi import OpenTelemetryMiddleware
+
+    def app(environ: WSGIEnvironment, start_response: StartResponse):
+        start_response("200 OK", [("Content-Type", "text/plain"), ("Content-Length", "13")])
+        return [b"Hello, World!"]
+
+    def request_hook(span: Span, environ: WSGIEnvironment):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_request_hook", "some-value")
+
+    def response_hook(span: Span, environ: WSGIEnvironment, status: str, response_headers: list[tuple[str, str]]):
+        if span and span.is_recording():
+            span.set_attribute("custom_user_attribute_from_response_hook", "some-value")
+
+    OpenTelemetryMiddleware(app, request_hook=request_hook, response_hook=response_hook)
+
+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 WSGI are case-insensitive and ``-`` characters are replaced by ``_``. 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 WSGI 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.
+
+Sanitizing methods
+******************
+In order to prevent unbound cardinality for HTTP methods by default nonstandard ones are labeled as ``NONSTANDARD``.
+To record all of the names set the environment variable  ``OTEL_PYTHON_INSTRUMENTATION_HTTP_CAPTURE_ALL_METHODS``
+to a value that evaluates to true, e.g. ``1``.
+
+API
+---
+"""
+
+from __future__ import annotations
+
+import functools
+import wsgiref.util as wsgiref_util
+from timeit import default_timer
+from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, TypeVar, cast
+
+from opentelemetry import context, trace
+from opentelemetry.instrumentation._semconv import (
+    _filter_semconv_active_request_count_attr,
+    _filter_semconv_duration_attrs,
+    _get_schema_url,
+    _OpenTelemetrySemanticConventionStability,
+    _OpenTelemetryStabilitySignalType,
+    _report_new,
+    _report_old,
+    _server_active_requests_count_attrs_new,
+    _server_active_requests_count_attrs_old,
+    _server_duration_attrs_new,
+    _server_duration_attrs_old,
+    _set_http_flavor_version,
+    _set_http_method,
+    _set_http_net_host,
+    _set_http_net_host_port,
+    _set_http_net_peer_name_server,
+    _set_http_peer_ip_server,
+    _set_http_peer_port_server,
+    _set_http_scheme,
+    _set_http_target,
+    _set_http_user_agent,
+    _set_status,
+    _StabilityMode,
+)
+from opentelemetry.instrumentation.utils import _start_internal_or_server_span
+from opentelemetry.instrumentation.wsgi.version import __version__
+from opentelemetry.metrics import MeterProvider, get_meter
+from opentelemetry.propagators.textmap import Getter
+from opentelemetry.semconv.attributes.error_attributes import ERROR_TYPE
+from opentelemetry.semconv.metrics import MetricInstruments
+from opentelemetry.semconv.metrics.http_metrics import (
+    HTTP_SERVER_REQUEST_DURATION,
+)
+from opentelemetry.semconv.trace import SpanAttributes
+from opentelemetry.trace import TracerProvider
+from opentelemetry.trace.status import Status, StatusCode
+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,
+    _parse_url_query,
+    get_custom_headers,
+    normalise_request_header_name,
+    normalise_response_header_name,
+    remove_url_credentials,
+    sanitize_method,
+)
+
+if TYPE_CHECKING:
+    from wsgiref.types import StartResponse, WSGIApplication, WSGIEnvironment
+
+
+T = TypeVar("T")
+RequestHook = Callable[[trace.Span, "WSGIEnvironment"], None]
+ResponseHook = Callable[
+    [trace.Span, "WSGIEnvironment", str, "list[tuple[str, str]]"], None
+]
+
+_HTTP_VERSION_PREFIX = "HTTP/"
+_CARRIER_KEY_PREFIX = "HTTP_"
+_CARRIER_KEY_PREFIX_LEN = len(_CARRIER_KEY_PREFIX)
+
+
+class WSGIGetter(Getter[Dict[str, Any]]):
+    def get(self, carrier: dict[str, Any], key: str) -> list[str] | None:
+        """Getter implementation to retrieve a HTTP header value from the
+             PEP3333-conforming WSGI environ
+
+        Args:
+             carrier: WSGI environ object
+             key: header name in environ object
+         Returns:
+             A list with a single string with the header value if it exists,
+             else None.
+        """
+        environ_key = "HTTP_" + key.upper().replace("-", "_")
+        value = carrier.get(environ_key)
+        if value is not None:
+            return [value]
+        return None
+
+    def keys(self, carrier: dict[str, Any]):
+        return [
+            key[_CARRIER_KEY_PREFIX_LEN:].lower().replace("_", "-")
+            for key in carrier
+            if key.startswith(_CARRIER_KEY_PREFIX)
+        ]
+
+
+wsgi_getter = WSGIGetter()
+
+
+# pylint: disable=too-many-branches
+def collect_request_attributes(
+    environ: WSGIEnvironment,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    """Collects HTTP request attributes from the PEP3333-conforming
+    WSGI environ and returns a dictionary to be used as span creation attributes.
+    """
+    result: dict[str, str | None] = {}
+    _set_http_method(
+        result,
+        environ.get("REQUEST_METHOD", ""),
+        sanitize_method(cast(str, environ.get("REQUEST_METHOD", ""))),
+        sem_conv_opt_in_mode,
+    )
+    # old semconv v1.12.0
+    server_name = environ.get("SERVER_NAME")
+    if _report_old(sem_conv_opt_in_mode):
+        result[SpanAttributes.HTTP_SERVER_NAME] = server_name
+
+    _set_http_scheme(
+        result,
+        environ.get("wsgi.url_scheme"),
+        sem_conv_opt_in_mode,
+    )
+
+    host = environ.get("HTTP_HOST")
+    host_port = environ.get("SERVER_PORT")
+    if host:
+        _set_http_net_host(result, host, sem_conv_opt_in_mode)
+        # old semconv v1.12.0
+        if _report_old(sem_conv_opt_in_mode):
+            result[SpanAttributes.HTTP_HOST] = host
+    if host_port:
+        _set_http_net_host_port(
+            result,
+            int(host_port),
+            sem_conv_opt_in_mode,
+        )
+
+    target = environ.get("RAW_URI")
+    if target is None:  # Note: `"" or None is None`
+        target = environ.get("REQUEST_URI")
+    if target:
+        path, query = _parse_url_query(target)
+        _set_http_target(result, target, path, query, sem_conv_opt_in_mode)
+    else:
+        # old semconv v1.20.0
+        if _report_old(sem_conv_opt_in_mode):
+            result[SpanAttributes.HTTP_URL] = remove_url_credentials(
+                wsgiref_util.request_uri(environ)
+            )
+
+    remote_addr = environ.get("REMOTE_ADDR")
+    if remote_addr:
+        _set_http_peer_ip_server(result, remote_addr, sem_conv_opt_in_mode)
+
+    peer_port = environ.get("REMOTE_PORT")
+    if peer_port:
+        _set_http_peer_port_server(result, peer_port, sem_conv_opt_in_mode)
+
+    remote_host = environ.get("REMOTE_HOST")
+    if remote_host and remote_host != remote_addr:
+        _set_http_net_peer_name_server(
+            result, remote_host, sem_conv_opt_in_mode
+        )
+
+    user_agent = environ.get("HTTP_USER_AGENT")
+    if user_agent is not None and len(user_agent) > 0:
+        _set_http_user_agent(result, user_agent, sem_conv_opt_in_mode)
+
+    flavor = environ.get("SERVER_PROTOCOL", "")
+    if flavor.upper().startswith(_HTTP_VERSION_PREFIX):
+        flavor = flavor[len(_HTTP_VERSION_PREFIX) :]
+    if flavor:
+        _set_http_flavor_version(result, flavor, sem_conv_opt_in_mode)
+
+    return result
+
+
+def collect_custom_request_headers_attributes(environ: WSGIEnvironment):
+    """Returns custom HTTP request headers which are configured by the user
+    from the PEP3333-conforming WSGI environ to be used as span creation attributes as described
+    in the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
+    """
+
+    sanitize = SanitizeValue(
+        get_custom_headers(
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
+        )
+    )
+    headers = {
+        key[_CARRIER_KEY_PREFIX_LEN:].replace("_", "-"): val
+        for key, val in environ.items()
+        if key.startswith(_CARRIER_KEY_PREFIX)
+    }
+
+    return sanitize.sanitize_header_values(
+        headers,
+        get_custom_headers(
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_REQUEST
+        ),
+        normalise_request_header_name,
+    )
+
+
+def collect_custom_response_headers_attributes(
+    response_headers: list[tuple[str, str]],
+):
+    """Returns custom HTTP response headers which are configured by the user from the
+    PEP3333-conforming WSGI environ as described in the specification
+    https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md#http-request-and-response-headers
+    """
+
+    sanitize = SanitizeValue(
+        get_custom_headers(
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SANITIZE_FIELDS
+        )
+    )
+    response_headers_dict: dict[str, str] = {}
+    if response_headers:
+        for key, val in response_headers:
+            key = key.lower()
+            if key in response_headers_dict:
+                response_headers_dict[key] += "," + val
+            else:
+                response_headers_dict[key] = val
+
+    return sanitize.sanitize_header_values(
+        response_headers_dict,
+        get_custom_headers(
+            OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE
+        ),
+        normalise_response_header_name,
+    )
+
+
+# TODO: Used only on the `opentelemetry-instrumentation-pyramid` package - It can be moved there.
+def _parse_status_code(resp_status: str) -> int | None:
+    status_code, _ = resp_status.split(" ", 1)
+    try:
+        return int(status_code)
+    except ValueError:
+        return None
+
+
+def _parse_active_request_count_attrs(
+    req_attrs, sem_conv_opt_in_mode: _StabilityMode = _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,
+    )
+
+
+def _parse_duration_attrs(
+    req_attrs: dict[str, str | None],
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):
+    return _filter_semconv_duration_attrs(
+        req_attrs,
+        _server_duration_attrs_old,
+        _server_duration_attrs_new,
+        sem_conv_opt_in_mode,
+    )
+
+
+def add_response_attributes(
+    span: trace.Span,
+    start_response_status: str,
+    response_headers: list[tuple[str, str]],
+    duration_attrs: dict[str, str | None] | None = None,
+    sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
+):  # pylint: disable=unused-argument
+    """Adds HTTP response attributes to span using the arguments
+    passed to a PEP3333-conforming start_response callable.
+    """
+    status_code_str, _ = start_response_status.split(" ", 1)
+    try:
+        status_code = int(status_code_str)
+    except ValueError:
+        status_code = -1
+    if duration_attrs is None:
+        duration_attrs = {}
+    _set_status(
+        span,
+        duration_attrs,
+        status_code,
+        status_code_str,
+        server_span=True,
+        sem_conv_opt_in_mode=sem_conv_opt_in_mode,
+    )
+
+
+def get_default_span_name(environ: WSGIEnvironment) -> str:
+    """
+    Default span name is the HTTP method and URL path, or just the method.
+    https://github.com/open-telemetry/opentelemetry-specification/pull/3165
+    https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
+
+    Args:
+        environ: The WSGI environ object.
+    Returns:
+        The span name.
+    """
+    method = sanitize_method(
+        cast(str, environ.get("REQUEST_METHOD", "")).strip()
+    )
+    if method == "_OTHER":
+        return "HTTP"
+    path = cast(str, environ.get("PATH_INFO", "")).strip()
+    if method and path:
+        return f"{method} {path}"
+    return method
+
+
+class OpenTelemetryMiddleware:
+    """The WSGI application middleware.
+
+    This class is a PEP 3333 conforming WSGI middleware that starts and
+    annotates spans for any requests it is invoked with.
+
+    Args:
+        wsgi: The WSGI application callable to forward requests to.
+        request_hook: Optional callback which is called with the server span and WSGI
+                      environ object for every incoming request.
+        response_hook: Optional callback which is called with the server span,
+                       WSGI environ, status_code and response_headers for every
+                       incoming request.
+        tracer_provider: Optional tracer provider to use. If omitted the current
+                         globally configured one is used.
+        meter_provider: Optional meter provider to use. If omitted the current
+                         globally configured one is used.
+    """
+
+    def __init__(
+        self,
+        wsgi: WSGIApplication,
+        request_hook: RequestHook | None = None,
+        response_hook: ResponseHook | None = None,
+        tracer_provider: TracerProvider | None = None,
+        meter_provider: MeterProvider | None = None,
+    ):
+        # initialize semantic conventions opt-in if needed
+        _OpenTelemetrySemanticConventionStability._initialize()
+        sem_conv_opt_in_mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(
+            _OpenTelemetryStabilitySignalType.HTTP,
+        )
+        self.wsgi = wsgi
+        self.tracer = trace.get_tracer(
+            __name__,
+            __version__,
+            tracer_provider,
+            schema_url=_get_schema_url(sem_conv_opt_in_mode),
+        )
+        self.meter = get_meter(
+            __name__,
+            __version__,
+            meter_provider,
+            schema_url=_get_schema_url(sem_conv_opt_in_mode),
+        )
+        self.duration_histogram_old = None
+        if _report_old(sem_conv_opt_in_mode):
+            self.duration_histogram_old = self.meter.create_histogram(
+                name=MetricInstruments.HTTP_SERVER_DURATION,
+                unit="ms",
+                description="Measures the duration of inbound HTTP requests.",
+            )
+        self.duration_histogram_new = None
+        if _report_new(sem_conv_opt_in_mode):
+            self.duration_histogram_new = self.meter.create_histogram(
+                name=HTTP_SERVER_REQUEST_DURATION,
+                unit="s",
+                description="Duration of HTTP server requests.",
+            )
+        # We don't need a separate active request counter for old/new semantic conventions
+        # because the new attributes are a subset of the old attributes
+        self.active_requests_counter = self.meter.create_up_down_counter(
+            name=MetricInstruments.HTTP_SERVER_ACTIVE_REQUESTS,
+            unit="{request}",
+            description="Number of active HTTP server requests.",
+        )
+        self.request_hook = request_hook
+        self.response_hook = response_hook
+        self._sem_conv_opt_in_mode = sem_conv_opt_in_mode
+
+    @staticmethod
+    def _create_start_response(
+        span: trace.Span,
+        start_response: StartResponse,
+        response_hook: Callable[[str, list[tuple[str, str]]], None] | None,
+        duration_attrs: dict[str, str | None],
+        sem_conv_opt_in_mode: _StabilityMode,
+    ):
+        @functools.wraps(start_response)
+        def _start_response(
+            status: str,
+            response_headers: list[tuple[str, str]],
+            *args: Any,
+            **kwargs: Any,
+        ):
+            add_response_attributes(
+                span,
+                status,
+                response_headers,
+                duration_attrs,
+                sem_conv_opt_in_mode,
+            )
+            if span.is_recording() and span.kind == trace.SpanKind.SERVER:
+                custom_attributes = collect_custom_response_headers_attributes(
+                    response_headers
+                )
+                if len(custom_attributes) > 0:
+                    span.set_attributes(custom_attributes)
+            if response_hook:
+                response_hook(status, response_headers)
+            return start_response(status, response_headers, *args, **kwargs)
+
+        return _start_response
+
+    # pylint: disable=too-many-branches
+    def __call__(
+        self, environ: WSGIEnvironment, start_response: StartResponse
+    ):
+        """The WSGI application
+
+        Args:
+            environ: A WSGI environment.
+            start_response: The WSGI start_response callable.
+        """
+        req_attrs = collect_request_attributes(
+            environ, self._sem_conv_opt_in_mode
+        )
+        active_requests_count_attrs = _parse_active_request_count_attrs(
+            req_attrs,
+            self._sem_conv_opt_in_mode,
+        )
+
+        span, token = _start_internal_or_server_span(
+            tracer=self.tracer,
+            span_name=get_default_span_name(environ),
+            start_time=None,
+            context_carrier=environ,
+            context_getter=wsgi_getter,
+            attributes=req_attrs,
+        )
+        if span.is_recording() and span.kind == trace.SpanKind.SERVER:
+            custom_attributes = collect_custom_request_headers_attributes(
+                environ
+            )
+            if len(custom_attributes) > 0:
+                span.set_attributes(custom_attributes)
+
+        if self.request_hook:
+            self.request_hook(span, environ)
+
+        response_hook = self.response_hook
+        if response_hook:
+            response_hook = functools.partial(response_hook, span, environ)
+
+        start = default_timer()
+        self.active_requests_counter.add(1, active_requests_count_attrs)
+        try:
+            with trace.use_span(span):
+                start_response = self._create_start_response(
+                    span,
+                    start_response,
+                    response_hook,
+                    req_attrs,
+                    self._sem_conv_opt_in_mode,
+                )
+                iterable = self.wsgi(environ, start_response)
+                return _end_span_after_iterating(iterable, span, token)
+        except Exception as ex:
+            if _report_new(self._sem_conv_opt_in_mode):
+                req_attrs[ERROR_TYPE] = type(ex).__qualname__
+                if span.is_recording():
+                    span.set_attribute(ERROR_TYPE, type(ex).__qualname__)
+                span.set_status(Status(StatusCode.ERROR, str(ex)))
+            span.end()
+            if token is not None:
+                context.detach(token)
+            raise
+        finally:
+            duration_s = default_timer() - start
+            if self.duration_histogram_old:
+                duration_attrs_old = _parse_duration_attrs(
+                    req_attrs, _StabilityMode.DEFAULT
+                )
+                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(
+                    req_attrs, _StabilityMode.HTTP
+                )
+                self.duration_histogram_new.record(
+                    max(duration_s, 0), duration_attrs_new
+                )
+            self.active_requests_counter.add(-1, active_requests_count_attrs)
+
+
+# Put this in a subfunction to not delay the call to the wrapped
+# WSGI application (instrumentation should change the application
+# behavior as little as possible).
+def _end_span_after_iterating(
+    iterable: Iterable[T], span: trace.Span, token: object
+) -> Iterable[T]:
+    try:
+        with trace.use_span(span):
+            yield from iterable
+    finally:
+        close = getattr(iterable, "close", None)
+        if close:
+            close()
+        span.end()
+        if token is not None:
+            context.detach(token)
+
+
+# TODO: inherit from opentelemetry.instrumentation.propagators.Setter
+class ResponsePropagationSetter:
+    def set(self, carrier: list[tuple[str, T]], key: str, value: T):  # pylint: disable=no-self-use
+        carrier.append((key, value))
+
+
+default_response_propagation_setter = ResponsePropagationSetter()
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/package.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/package.py
new file mode 100644
index 00000000..2dbb1905
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/package.py
@@ -0,0 +1,21 @@
+# 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 __future__ import annotations
+
+_instruments: tuple[str, ...] = tuple()
+
+_supports_metrics = True
+
+_semconv_status = "migration"
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/py.typed b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/py.typed
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/py.typed
diff --git a/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/version.py b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/version.py
new file mode 100644
index 00000000..7fb5b98b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/opentelemetry/instrumentation/wsgi/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"