aboutsummaryrefslogtreecommitdiff
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 hereHEADmaster
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"