# Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 from __future__ import annotations import os import threading from collections.abc import Container, Mapping, MutableMapping from enum import Enum from urllib.parse import urlparse from packaging import version as package_version from opentelemetry.instrumentation.utils import http_status_to_status_code from opentelemetry.semconv._incubating.attributes.db_attributes import ( DB_NAME, DB_OPERATION, DB_REDIS_DATABASE_INDEX, DB_STATEMENT, DB_SYSTEM, DB_USER, ) from opentelemetry.semconv._incubating.attributes.http_attributes import ( HTTP_FLAVOR, HTTP_HOST, HTTP_METHOD, HTTP_SCHEME, HTTP_SERVER_NAME, HTTP_STATUS_CODE, HTTP_TARGET, HTTP_URL, HTTP_USER_AGENT, ) from opentelemetry.semconv._incubating.attributes.net_attributes import ( NET_HOST_NAME, NET_HOST_PORT, NET_PEER_IP, NET_PEER_NAME, NET_PEER_PORT, NET_TRANSPORT, ) from opentelemetry.semconv.attributes.client_attributes import ( CLIENT_ADDRESS, CLIENT_PORT, ) from opentelemetry.semconv.attributes.db_attributes import ( DB_NAMESPACE, DB_OPERATION_NAME, DB_QUERY_TEXT, DB_SYSTEM_NAME, ) 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, NETWORK_TRANSPORT, ) 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.schemas import Schemas from opentelemetry.trace import Span from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util.types import AttributeValue # Values defined in milliseconds HTTP_DURATION_HISTOGRAM_BUCKETS_OLD = ( 0.0, 5.0, 10.0, 25.0, 50.0, 75.0, 100.0, 250.0, 500.0, 750.0, 1000.0, 2500.0, 5000.0, 7500.0, 10000.0, ) # Values defined in seconds HTTP_DURATION_HISTOGRAM_BUCKETS_NEW = ( 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10, ) # These lists represent attributes for metrics that are currently supported _client_duration_attrs_old = [ HTTP_STATUS_CODE, HTTP_HOST, HTTP_METHOD, HTTP_FLAVOR, HTTP_SCHEME, NET_PEER_PORT, NET_PEER_NAME, ] _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 = [ HTTP_METHOD, HTTP_HOST, HTTP_SCHEME, HTTP_STATUS_CODE, HTTP_FLAVOR, HTTP_SERVER_NAME, NET_HOST_NAME, 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 = [ HTTP_METHOD, HTTP_HOST, HTTP_SCHEME, HTTP_FLAVOR, 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" # Legacy/default schema version when schema_url was first introduced _LEGACY_SCHEMA_VERSION = "1.11.0" class _OpenTelemetryStabilitySignalType(Enum): HTTP = "http" DATABASE = "database" GEN_AI = "gen_ai" class _StabilityMode(Enum): DEFAULT = "default" HTTP = "http" HTTP_DUP = "http/dup" DATABASE = "database" DATABASE_DUP = "database/dup" GEN_AI_LATEST_EXPERIMENTAL = "gen_ai_latest_experimental" 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, gen ai, 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, _OpenTelemetryStabilitySignalType.GEN_AI: _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.GEN_AI] = cls._filter_mode( opt_in_list, _StabilityMode.DEFAULT, _StabilityMode.GEN_AI_LATEST_EXPERIMENTAL, ) 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 _get_semconv_opt_in_modes( signal_types: tuple[_OpenTelemetryStabilitySignalType, ...], ) -> dict[_OpenTelemetryStabilitySignalType, _StabilityMode]: """Returns a mapping of signal type to mode for the provided signal_types (one/more of DATABASE, HTTP, GEN_AI). """ _OpenTelemetrySemanticConventionStability._initialize() return { signal_type: _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(signal_type) for signal_type in signal_types } def _filter_semconv_duration_attrs( attrs: Mapping[str, AttributeValue], old_attrs: Container[AttributeValue], new_attrs: Container[AttributeValue], sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> dict[str, AttributeValue]: 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: Mapping[str, AttributeValue], old_attrs: Container[AttributeValue], new_attrs: Container[AttributeValue], sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> dict[str, AttributeValue]: 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: MutableMapping[str, AttributeValue], key: str, value: AttributeValue, ) -> None: if value: result[key] = value def set_int_attribute( result: MutableMapping[str, AttributeValue], key: str, value: AttributeValue, ) -> None: if value: try: result[key] = int(value) except (ValueError, TypeError): pass def _set_http_method( result: MutableMapping[str, AttributeValue], original: str, normalized: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: 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, 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: MutableMapping[str, AttributeValue], code: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, 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: MutableMapping[str, AttributeValue], url: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_URL, url) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, URL_FULL, url) def _set_http_scheme( result: MutableMapping[str, AttributeValue], scheme: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_SCHEME, scheme) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, URL_SCHEME, scheme) def _set_http_flavor_version( result: MutableMapping[str, AttributeValue], version: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], user_agent: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], host: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], peer_name: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], port: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, 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: MutableMapping[str, AttributeValue], version: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], host: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], port: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, NET_HOST_PORT, port) if _report_new(sem_conv_opt_in_mode): set_int_attribute(result, SERVER_PORT, port) def _set_http_target( result: MutableMapping[str, AttributeValue], target: str, path: str | None, query: str | None, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], host: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, HTTP_HOST, host) if _report_new(sem_conv_opt_in_mode): if not result.get(SERVER_ADDRESS): set_string_attribute(result, SERVER_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: MutableMapping[str, AttributeValue], ip: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, 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: MutableMapping[str, AttributeValue], port: str | int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_int_attribute(result, 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: MutableMapping[str, AttributeValue], name: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_PEER_NAME, name) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, CLIENT_ADDRESS, name) # Database def _set_db_system( result: MutableMapping[str, AttributeValue], system: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_SYSTEM, system) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, DB_SYSTEM_NAME, system) def _set_db_name( result: MutableMapping[str, AttributeValue], name: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_NAME, name) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, DB_NAMESPACE, name) def _set_db_statement( result: MutableMapping[str, AttributeValue], statement: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: # skip the statement if it's None but set it if it's an empty string if statement is None: return if _report_old(sem_conv_opt_in_mode): result[DB_STATEMENT] = statement if _report_new(sem_conv_opt_in_mode): result[DB_QUERY_TEXT] = statement def _set_db_user( result: MutableMapping[str, AttributeValue], user: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_USER, user) # No new attribute - db.user was removed with no replacement def _set_db_operation( result: MutableMapping[str, AttributeValue], operation: str, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, DB_OPERATION, operation) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, DB_OPERATION_NAME, operation) def _set_db_redis_database_index( result: MutableMapping[str, AttributeValue], database_index: int, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): if database_index is not None: result[DB_REDIS_DATABASE_INDEX] = int(database_index) # No new attribute - db.redis.database_index was removed with no replacement in semconv 1.38.0 def _set_net_transport( result: MutableMapping[str, AttributeValue], old_transport: AttributeValue, new_transport: AttributeValue, sem_conv_opt_in_mode: _StabilityMode, ) -> None: if _report_old(sem_conv_opt_in_mode): set_string_attribute(result, NET_TRANSPORT, old_transport) if _report_new(sem_conv_opt_in_mode): set_string_attribute(result, NETWORK_TRANSPORT, new_transport) # General def _set_status( span: Span, metrics_attributes: MutableMapping[str, AttributeValue], status_code: int, status_code_str: str, server_span: bool = True, sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT, ) -> None: 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(HTTP_STATUS_CODE, status_code) metrics_attributes[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)) def _get_schema_url(mode: _StabilityMode) -> str: """Get schema version URL for a single signal type's opt-in mode (backwards compatible). For new instrumentations using multiple signal types, use _get_schema_url_for_signal_types() """ if mode is _StabilityMode.DEFAULT: return f"https://opentelemetry.io/schemas/{_LEGACY_SCHEMA_VERSION}" return Schemas.V1_21_0.value def _get_schema_version_for_opt_in_mode( signal_type: _OpenTelemetryStabilitySignalType, mode: _StabilityMode, ) -> str: """Get the schema version for a specific signal type and opt-in mode.""" if mode == _StabilityMode.DEFAULT: return _LEGACY_SCHEMA_VERSION signal_versions = { _OpenTelemetryStabilitySignalType.HTTP: Schemas.V1_21_0.value, _OpenTelemetryStabilitySignalType.DATABASE: Schemas.V1_25_0.value, _OpenTelemetryStabilitySignalType.GEN_AI: Schemas.V1_26_0.value, } schema_url = signal_versions.get(signal_type) if not schema_url: return _LEGACY_SCHEMA_VERSION path = urlparse(schema_url).path schema_version = path.rstrip("/").split("/")[-1] return schema_version or _LEGACY_SCHEMA_VERSION def _get_schema_url_for_signal_types( signal_types: list[_OpenTelemetryStabilitySignalType], ) -> str: """Get the highest applicable schema URL for multiple signal types. Note: Instrumentors should call _OpenTelemetrySemanticConventionStability._initialize() before using this function to ensure proper initialization of stability modes. Args: signal_types: List of signal types used by the instrumentation Returns: Schema URL string representing the highest applicable semconv version """ highest_schema_version = _LEGACY_SCHEMA_VERSION for signal_type in signal_types: mode = _OpenTelemetrySemanticConventionStability._get_opentelemetry_stability_opt_in_mode(signal_type) schema_version = _get_schema_version_for_opt_in_mode(signal_type, mode) # Keep the highest for all signals if package_version.Version(schema_version) > package_version.Version(highest_schema_version): highest_schema_version = schema_version return f"https://opentelemetry.io/schemas/{highest_schema_version}"