diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/opentelemetry/instrumentation')
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" |