diff options
| author | Carsten Schoenert <c.schoenert@t-online.de> | 2026-07-01 09:35:22 +0200 |
|---|---|---|
| committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2026-07-01 22:36:34 +0000 |
| commit | 8f2c4118ad4f64f67b91d8e13b750b2fd4080613 (patch) | |
| tree | d7988886e937c0a92eddbbc46cd6945f96a4b114 /opentelemetry-api | |
| parent | fa4b19cc510b29c1f5e59dce046a9ca22f38f9ad (diff) | |
1.43.0-1 (patches unapplied)HEADimport/1.43.0-1ubuntu/stonking-proposedubuntu/stonking-develubuntu/stonkingubuntu/develdebian/sid
Imported using git-ubuntu import.
Notes
Notes:
* [11a6262] New upstream version 1.43.0
Diffstat (limited to 'opentelemetry-api')
15 files changed, 337 insertions, 168 deletions
diff --git a/opentelemetry-api/src/opentelemetry/_events/__init__.py b/opentelemetry-api/src/opentelemetry/_events/__init__.py index 9c70b90..e9e60f7 100644 --- a/opentelemetry-api/src/opentelemetry/_events/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_events/__init__.py @@ -1,10 +1,12 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + from abc import ABC, abstractmethod from logging import getLogger from os import environ -from typing import Optional, cast +from typing import cast from typing_extensions import deprecated @@ -32,7 +34,7 @@ class Event(LogRecord): timestamp: int | None = None, trace_id: int | None = None, span_id: int | None = None, - trace_flags: Optional["TraceFlags"] = None, + trace_flags: TraceFlags | None = None, body: AnyValue | None = None, severity_number: SeverityNumber | None = None, attributes: _ExtendedAttributes | None = None, @@ -72,7 +74,7 @@ class EventLogger(ABC): self._attributes = attributes @abstractmethod - def emit(self, event: "Event") -> None: + def emit(self, event: Event) -> None: """Emits a :class:`Event` representing an event.""" @@ -244,7 +246,7 @@ def get_event_logger( schema_url: str | None = None, attributes: _ExtendedAttributes | None = None, event_logger_provider: EventLoggerProvider | None = None, -) -> "EventLogger": +) -> EventLogger: if event_logger_provider is None: event_logger_provider = get_event_logger_provider() return event_logger_provider.get_event_logger( diff --git a/opentelemetry-api/src/opentelemetry/attributes/__init__.py b/opentelemetry-api/src/opentelemetry/attributes/__init__.py index 09d01d3..15519ce 100644 --- a/opentelemetry-api/src/opentelemetry/attributes/__init__.py +++ b/opentelemetry-api/src/opentelemetry/attributes/__init__.py @@ -35,6 +35,8 @@ def _type_name(t): _logger = logging.getLogger(__name__) +# pylint: disable=too-many-return-statements +# pylint: disable=too-many-branches def _clean_attribute( key: str, value: types.AttributeValue, max_len: int | None ) -> types.AttributeValue | tuple[str | int | float, ...] | None: @@ -58,16 +60,32 @@ def _clean_attribute( return None if isinstance(value, _VALID_ATTR_VALUE_TYPES): - return _clean_attribute_value(value, max_len) + if isinstance(value, bytes): + try: + value = value.decode() + except UnicodeDecodeError: + _logger.warning("Byte attribute could not be decoded.") + return None + if max_len is not None and isinstance(value, str): + value = value[:max_len] + return value if isinstance(value, Sequence): sequence_first_valid_type = None cleaned_seq = [] for element in value: - element = _clean_attribute_value(element, max_len) # type: ignore - if element is None: - cleaned_seq.append(element) + if isinstance(element, bytes): + try: + element = element.decode() + except UnicodeDecodeError: + _logger.warning("Byte attribute could not be decoded.") + cleaned_seq.append(None) + continue + if max_len is not None and isinstance(element, str): + element = element[:max_len] + elif element is None: + cleaned_seq.append(None) continue element_type = type(element) @@ -214,24 +232,6 @@ def _clean_extended_attribute( return None -def _clean_attribute_value( - value: types.AttributeValue, limit: int | None -) -> types.AttributeValue | None: - if value is None: - return None - - if isinstance(value, bytes): - try: - value = value.decode() - except UnicodeDecodeError: - _logger.warning("Byte attribute could not be decoded.") - return None - - if limit is not None and isinstance(value, str): - value = value[:limit] - return value - - class BoundedAttributes(MutableMapping): # type: ignore """An ordered dict with a fixed max capacity. @@ -277,29 +277,49 @@ class BoundedAttributes(MutableMapping): # type: ignore def __setitem__(self, key: str, value: types.AnyValue) -> None: if getattr(self, "_immutable", False): # type: ignore raise TypeError - with self._lock: - if self.maxlen is not None and self.maxlen == 0: + if self.maxlen is not None and self.maxlen == 0: + with self._lock: self.dropped += 1 + return + if self._extended_attributes: + value = _clean_extended_attribute(key, value, self.max_value_len) + else: + value = _clean_attribute(key, value, self.max_value_len) # type: ignore + if value is None: return + with self._lock: + self._setitem_locked(key, value) + def _set_items(self, attributes: "types._ExtendedAttributes") -> None: + if getattr(self, "_immutable", False): # type: ignore + raise TypeError + if self.maxlen is not None and self.maxlen == 0: + with self._lock: + self.dropped += len(attributes) + return + cleaned = [] + for key, value in attributes.items(): if self._extended_attributes: - value = _clean_extended_attribute( - key, value, self.max_value_len - ) + cv = _clean_extended_attribute(key, value, self.max_value_len) else: - value = _clean_attribute(key, value, self.max_value_len) # type: ignore - if value is None: - return - - if key in self._dict: - del self._dict[key] - elif self.maxlen is not None and len(self._dict) == self.maxlen: - if not isinstance(self._dict, OrderedDict): - self._dict = OrderedDict(self._dict) - self._dict.popitem(last=False) # type: ignore - self.dropped += 1 + cv = _clean_attribute(key, value, self.max_value_len) # type: ignore + if cv is None: + continue + cleaned.append((key, cv)) + with self._lock: + for key, cv in cleaned: + self._setitem_locked(key, cv) + + def _setitem_locked(self, key: str, value: types.AnyValue) -> None: + if key in self._dict: + del self._dict[key] + elif self.maxlen is not None and len(self._dict) == self.maxlen: + if not isinstance(self._dict, OrderedDict): + self._dict = OrderedDict(self._dict) + self._dict.popitem(last=False) # type: ignore + self.dropped += 1 - self._dict[key] = value # type: ignore + self._dict[key] = value # type: ignore def __delitem__(self, key: str) -> None: if getattr(self, "_immutable", False): # type: ignore @@ -307,9 +327,11 @@ class BoundedAttributes(MutableMapping): # type: ignore with self._lock: del self._dict[key] - def __iter__(self): # type: ignore + def __iter__(self): + if self._immutable: + return iter(self._dict) with self._lock: - return iter(self._dict.copy()) # type: ignore + return iter(list(self._dict)) def __len__(self) -> int: return len(self._dict) diff --git a/opentelemetry-api/src/opentelemetry/context/__init__.py b/opentelemetry-api/src/opentelemetry/context/__init__.py index 6518f03..80174f6 100644 --- a/opentelemetry-api/src/opentelemetry/context/__init__.py +++ b/opentelemetry-api/src/opentelemetry/context/__init__.py @@ -4,14 +4,14 @@ from __future__ import annotations import logging +import os from contextvars import Token -from os import environ from uuid import uuid4 # pylint: disable=wrong-import-position from opentelemetry.context.context import Context, _RuntimeContext # noqa +from opentelemetry.context.contextvars_context import ContextVarsRuntimeContext from opentelemetry.environment_variables import OTEL_PYTHON_CONTEXT -from opentelemetry.util._importlib_metadata import entry_points logger = logging.getLogger(__name__) @@ -22,37 +22,29 @@ def _load_runtime_context() -> _RuntimeContext: Returns: An instance of RuntimeContext. """ + configured_context = os.environ.get(OTEL_PYTHON_CONTEXT) + if not configured_context: + return ContextVarsRuntimeContext() - # FIXME use a better implementation of a configuration manager - # to avoid having to get configuration values straight from - # environment variables - default_context = "contextvars_context" - - configured_context = environ.get(OTEL_PYTHON_CONTEXT, default_context) # type: str + # pylint: disable=import-outside-toplevel,no-name-in-module + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, + ) try: - return next( # type: ignore - iter( # type: ignore - entry_points( # type: ignore - group="opentelemetry_context", - name=configured_context, + return next( + iter( + entry_points( + group="opentelemetry_context", name=configured_context ) ) ).load()() except Exception: # pylint: disable=broad-exception-caught logger.exception( - "Failed to load context: %s, fallback to %s", + "Failed to load context: %s, falling back to contextvars_context", configured_context, - default_context, ) - return next( # type: ignore - iter( # type: ignore - entry_points( # type: ignore - group="opentelemetry_context", - name=default_context, - ) - ) - ).load()() + return ContextVarsRuntimeContext() _RUNTIME_CONTEXT = _load_runtime_context() diff --git a/opentelemetry-api/src/opentelemetry/propagate/__init__.py b/opentelemetry-api/src/opentelemetry/propagate/__init__.py index 207f639..d53a1b9 100644 --- a/opentelemetry-api/src/opentelemetry/propagate/__init__.py +++ b/opentelemetry-api/src/opentelemetry/propagate/__init__.py @@ -63,7 +63,6 @@ from os import environ from opentelemetry.context.context import Context from opentelemetry.environment_variables import OTEL_PROPAGATORS from opentelemetry.propagators import composite, textmap -from opentelemetry.util._importlib_metadata import entry_points logger = getLogger(__name__) @@ -109,46 +108,66 @@ def inject( get_global_textmap().inject(carrier, context=context, setter=setter) -propagators: list[textmap.TextMapPropagator] = [] - -# Single use variable here to hack black and make lint pass -environ_propagators = environ.get( - OTEL_PROPAGATORS, - "tracecontext,baggage", -) +def _load_propagators() -> textmap.TextMapPropagator: + configured = environ.get(OTEL_PROPAGATORS) + if not configured: + # pylint: disable=import-outside-toplevel,no-name-in-module + from opentelemetry.baggage.propagation import ( # noqa: PLC0415 + W3CBaggagePropagator, + ) + # pylint: disable=import-outside-toplevel,no-name-in-module + from opentelemetry.trace.propagation.tracecontext import ( # noqa: PLC0415 + TraceContextTextMapPropagator, + ) -for propagator in environ_propagators.split(","): - propagator = propagator.strip() - if propagator.lower() == "none": - logger.debug( - "OTEL_PROPAGATORS environment variable contains none, removing all propagators" + return composite.CompositePropagator( + [TraceContextTextMapPropagator(), W3CBaggagePropagator()] ) - propagators = [] - break - try: - propagators.append( - next( # type: ignore - iter( # type: ignore - entry_points( # type: ignore[misc] - group="opentelemetry_propagator", - name=propagator, + + # pylint: disable=import-outside-toplevel,no-name-in-module + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, + ) + + _propagators: list[textmap.TextMapPropagator] = [] + for _propagator in configured.split(","): + _propagator = _propagator.strip() + if _propagator.lower() == "none": + logger.debug( + "OTEL_PROPAGATORS environment variable contains none, removing all propagators" + ) + return composite.CompositePropagator([]) + try: + _propagators.append( + next( # type: ignore + iter( # type: ignore + entry_points( # type: ignore[misc] + group="opentelemetry_propagator", + name=_propagator, + ) ) - ) - ).load()() - ) - except StopIteration: - raise ValueError( - f"Propagator {propagator} not found. It is either misspelled or not installed." - ) - except Exception: # pylint: disable=broad-exception-caught - logger.exception("Failed to load propagator: %s", propagator) - raise + ).load()() + ) + except StopIteration: + raise ValueError( + f"Propagator {_propagator} not found. It is either misspelled or not installed." + ) + except Exception: # pylint: disable=broad-exception-caught + logger.exception("Failed to load propagator: %s", _propagator) + raise + return composite.CompositePropagator(_propagators) + + +_HTTP_TEXT_FORMAT: textmap.TextMapPropagator = _load_propagators() +# Deprecated: propagators, environ_propagators and propagator names were never intendended to be part of the public API. +propagators: list[textmap.TextMapPropagator] = [] + +environ_propagators = environ.get(OTEL_PROPAGATORS, "tracecontext,baggage") -_HTTP_TEXT_FORMAT: textmap.TextMapPropagator = composite.CompositePropagator( - propagators -) +for propagator in environ_propagators.split(","): # type: ignore[assignment] + propagator = propagator.strip() def get_global_textmap() -> textmap.TextMapPropagator: diff --git a/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py b/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py index 6b65148..5b597ce 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py +++ b/opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py @@ -3,24 +3,36 @@ import os import re -import typing -from collections.abc import MutableMapping +from collections.abc import Mapping, MutableMapping from opentelemetry.propagators.textmap import Getter, Setter def _normalize_key(key: str) -> str: + if not key: + return "_" result = re.sub(r"[^A-Za-z0-9_]", "_", key).upper() if result and result[0].isdigit(): result = "_" + result return result -class EnvironmentGetter(Getter[typing.Mapping[str, str]]): +def _is_normalized_key(key: str) -> bool: + if not key: + return False + if "0" <= key[0] <= "9": + return False + return all( + "A" <= char <= "Z" or "0" <= char <= "9" or char == "_" for char in key + ) + + +class EnvironmentGetter(Getter[Mapping[str, str]]): """Getter implementation for extracting context and baggage from environment variables. - EnvironmentGetter creates a normalized lookup from the current environment - variables at initialization time and provides simple data access without validation. + EnvironmentGetter creates a lookup from the current environment variables + whose names are already normalized at initialization time and provides + simple data access without validation. Per the OpenTelemetry specification, environment variables are treated as immutable within a process. For environments where context-carrying environment variables @@ -33,20 +45,17 @@ class EnvironmentGetter(Getter[typing.Mapping[str, str]]): """ def __init__(self): - # Create a normalized lookup from current environment - # Per spec: "creates an in-memory copy of the current environment variables" + # Per spec, Get reads only normalized environment variable names. self.carrier: dict[str, str] = { - _normalize_key(k): v for k, v in os.environ.items() + k: v for k, v in os.environ.items() if _is_normalized_key(k) } - def get( - self, carrier: typing.Mapping[str, str], key: str - ) -> list[str] | None: + def get(self, carrier: Mapping[str, str], key: str) -> list[str] | None: """Get a value from the environment carrier for the given key. Args: carrier: Not used; maintained for interface compatibility with Getter[CarrierT] - key: The key to look up (case-insensitive) + key: The key to look up (will be normalized) Returns: A list with a single string value if the key exists, None otherwise. @@ -56,14 +65,14 @@ class EnvironmentGetter(Getter[typing.Mapping[str, str]]): return None return [val] - def keys(self, carrier: typing.Mapping[str, str]) -> list[str]: + def keys(self, carrier: Mapping[str, str]) -> list[str]: """Get all keys from the environment carrier. Args: carrier: Not used; maintained for interface compatibility with Getter[CarrierT] Returns: - List of all environment variable keys (normalized). + List of all already-normalized environment variable keys. """ return list(self.carrier.keys()) @@ -88,7 +97,7 @@ class EnvironmentSetter(Setter[MutableMapping[str, str]]): Args: carrier: Dictionary to store environment variables - key: The key to set (will be converted to uppercase) + key: The key to set (normalized) value: The value to set """ carrier[_normalize_key(key)] = value diff --git a/opentelemetry-api/src/opentelemetry/propagators/composite.py b/opentelemetry-api/src/opentelemetry/propagators/composite.py index f6d864d..1d3c391 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/composite.py +++ b/opentelemetry-api/src/opentelemetry/propagators/composite.py @@ -1,7 +1,7 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +import collections.abc import logging -import typing from typing_extensions import deprecated @@ -20,7 +20,7 @@ class CompositePropagator(textmap.TextMapPropagator): """ def __init__( - self, propagators: typing.Sequence[textmap.TextMapPropagator] + self, propagators: collections.abc.Sequence[textmap.TextMapPropagator] ) -> None: self._propagators = propagators diff --git a/opentelemetry-api/src/opentelemetry/propagators/textmap.py b/opentelemetry-api/src/opentelemetry/propagators/textmap.py index de37e65..04f5fe8 100644 --- a/opentelemetry-api/src/opentelemetry/propagators/textmap.py +++ b/opentelemetry-api/src/opentelemetry/propagators/textmap.py @@ -3,6 +3,7 @@ import abc import typing +from collections.abc import Iterable, Mapping, MutableMapping from opentelemetry.context.context import Context @@ -59,9 +60,9 @@ class Setter(abc.ABC, typing.Generic[CarrierT]): """ -class DefaultGetter(Getter[typing.Mapping[str, CarrierValT]]): +class DefaultGetter(Getter[Mapping[str, CarrierValT]]): def get( - self, carrier: typing.Mapping[str, CarrierValT], key: str + self, carrier: Mapping[str, CarrierValT], key: str ) -> list[str] | None: """Getter implementation to retrieve a value from a dictionary. @@ -74,11 +75,11 @@ class DefaultGetter(Getter[typing.Mapping[str, CarrierValT]]): val = carrier.get(key, None) if val is None: return None - if isinstance(val, typing.Iterable) and not isinstance(val, str): + if isinstance(val, Iterable) and not isinstance(val, str): return list(val) return [val] - def keys(self, carrier: typing.Mapping[str, CarrierValT]) -> list[str]: + def keys(self, carrier: Mapping[str, CarrierValT]) -> list[str]: """Keys implementation that returns all keys from a dictionary.""" return list(carrier.keys()) @@ -86,10 +87,10 @@ class DefaultGetter(Getter[typing.Mapping[str, CarrierValT]]): default_getter: Getter[CarrierT] = DefaultGetter() # type: ignore -class DefaultSetter(Setter[typing.MutableMapping[str, CarrierValT]]): +class DefaultSetter(Setter[MutableMapping[str, CarrierValT]]): def set( self, - carrier: typing.MutableMapping[str, CarrierValT], + carrier: MutableMapping[str, CarrierValT], key: str, value: CarrierValT, ) -> None: diff --git a/opentelemetry-api/src/opentelemetry/trace/span.py b/opentelemetry-api/src/opentelemetry/trace/span.py index 60befed..23a24f1 100644 --- a/opentelemetry-api/src/opentelemetry/trace/span.py +++ b/opentelemetry-api/src/opentelemetry/trace/span.py @@ -1,12 +1,15 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +from __future__ import annotations + import abc import logging import re import types as python_types import typing import warnings +from collections.abc import Iterator, Mapping, Sequence from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types @@ -70,7 +73,7 @@ class Span(abc.ABC): """ @abc.abstractmethod - def get_span_context(self) -> "SpanContext": + def get_span_context(self) -> SpanContext: """Gets the span's SpanContext. Get an immutable, serializable identifier for this span that can be @@ -82,7 +85,7 @@ class Span(abc.ABC): @abc.abstractmethod def set_attributes( - self, attributes: typing.Mapping[str, types.AttributeValue] + self, attributes: Mapping[str, types.AttributeValue] ) -> None: """Sets Attributes. @@ -122,7 +125,7 @@ class Span(abc.ABC): def add_link( # pylint: disable=no-self-use self, - context: "SpanContext", + context: SpanContext, attributes: types.Attributes = None, ) -> None: """Adds a `Link`. @@ -179,7 +182,7 @@ class Span(abc.ABC): ) -> None: """Records an exception as a span event.""" - def __enter__(self) -> "Span": + def __enter__(self) -> Span: """Invoked when `Span` is used as a context manager. Returns the `Span` itself. @@ -217,7 +220,7 @@ class TraceFlags(int): RANDOM_TRACE_ID = 0x02 @classmethod - def get_default(cls) -> "TraceFlags": + def get_default(cls) -> TraceFlags: return cls(cls.DEFAULT) @property @@ -232,7 +235,7 @@ class TraceFlags(int): DEFAULT_TRACE_OPTIONS = TraceFlags.get_default() -class TraceState(typing.Mapping[str, str]): +class TraceState(Mapping[str, str]): """A list of key-value pairs representing vendor-specific trace info. Keys and values are strings of up to 256 printable US-ASCII characters. @@ -245,7 +248,7 @@ class TraceState(typing.Mapping[str, str]): def __init__( self, - entries: typing.Sequence[tuple[str, str]] | None = None, + entries: Sequence[tuple[str, str]] | None = None, ) -> None: self._dict = {} # type: dict[str, str] if entries is None: @@ -274,7 +277,7 @@ class TraceState(typing.Mapping[str, str]): def __getitem__(self, key: str) -> str: return self._dict[key] - def __iter__(self) -> typing.Iterator[str]: + def __iter__(self) -> Iterator[str]: return iter(self._dict) def __len__(self) -> int: @@ -287,7 +290,7 @@ class TraceState(typing.Mapping[str, str]): ] return str(pairs) - def add(self, key: str, value: str) -> "TraceState": + def add(self, key: str, value: str) -> TraceState: """Adds a key-value pair to tracestate. The provided pair should adhere to w3c tracestate identifiers format. @@ -318,7 +321,7 @@ class TraceState(typing.Mapping[str, str]): new_state = [(key, value)] + list(self._dict.items()) return TraceState(new_state) - def update(self, key: str, value: str) -> "TraceState": + def update(self, key: str, value: str) -> TraceState: """Updates a key-value pair in tracestate. The provided pair should adhere to w3c tracestate identifiers format. @@ -343,7 +346,7 @@ class TraceState(typing.Mapping[str, str]): new_state = [(key, value), *prev_state.items()] return TraceState(new_state) - def delete(self, key: str) -> "TraceState": + def delete(self, key: str) -> TraceState: """Deletes a key-value from tracestate. Args: @@ -374,7 +377,7 @@ class TraceState(typing.Mapping[str, str]): return ",".join(key + "=" + value for key, value in self._dict.items()) @classmethod - def from_header(cls, header_list: list[str]) -> "TraceState": + def from_header(cls, header_list: list[str]) -> TraceState: """Parses one or more w3c tracestate header into a TraceState. Args: @@ -413,7 +416,7 @@ class TraceState(typing.Mapping[str, str]): return cls(list(pairs.items())) @classmethod - def get_default(cls) -> "TraceState": + def get_default(cls) -> TraceState: return cls() def keys(self) -> typing.KeysView[str]: @@ -450,9 +453,9 @@ class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]): trace_id: int, span_id: int, is_remote: bool, - trace_flags: typing.Optional["TraceFlags"] = DEFAULT_TRACE_OPTIONS, - trace_state: typing.Optional["TraceState"] = DEFAULT_TRACE_STATE, - ) -> "SpanContext": + trace_flags: TraceFlags | None = DEFAULT_TRACE_OPTIONS, + trace_state: TraceState | None = DEFAULT_TRACE_STATE, + ) -> SpanContext: if trace_flags is None: trace_flags = DEFAULT_TRACE_OPTIONS if trace_state is None: @@ -470,7 +473,7 @@ class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]): def __getnewargs__( self, - ) -> tuple[int, int, bool, "TraceFlags", "TraceState"]: + ) -> tuple[int, int, bool, TraceFlags, TraceState]: return ( self.trace_id, self.span_id, @@ -492,11 +495,11 @@ class SpanContext(tuple[int, int, bool, "TraceFlags", "TraceState", bool]): return self[2] # pylint: disable=unsubscriptable-object @property - def trace_flags(self) -> "TraceFlags": + def trace_flags(self) -> TraceFlags: return self[3] # pylint: disable=unsubscriptable-object @property - def trace_state(self) -> "TraceState": + def trace_state(self) -> TraceState: return self[4] # pylint: disable=unsubscriptable-object @property @@ -523,10 +526,10 @@ class NonRecordingSpan(Span): All operations are no-op except context propagation. """ - def __init__(self, context: "SpanContext") -> None: + def __init__(self, context: SpanContext) -> None: self._context = context - def get_span_context(self) -> "SpanContext": + def get_span_context(self) -> SpanContext: return self._context def is_recording(self) -> bool: @@ -536,7 +539,7 @@ class NonRecordingSpan(Span): pass def set_attributes( - self, attributes: typing.Mapping[str, types.AttributeValue] + self, attributes: Mapping[str, types.AttributeValue] ) -> None: pass @@ -553,7 +556,7 @@ class NonRecordingSpan(Span): def add_link( self, - context: "SpanContext", + context: SpanContext, attributes: types.Attributes = None, ) -> None: pass diff --git a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py index be779f1..7e685f5 100644 --- a/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py +++ b/opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py @@ -11,6 +11,7 @@ It also normalizes minor differences across python versions 3.10+. References to - https://github.com/open-telemetry/opentelemetry-python/pull/5203 """ +import itertools from functools import cache from importlib.metadata import ( Distribution, @@ -26,10 +27,19 @@ from typing import Any def _as_entry_points(eps: Any) -> EntryPoints: + # Python versions greater than 3.11 return EntryPoints. if isinstance(eps, EntryPoints): return eps - # Handle Python 3.10 SelectableGroups (dict-like) - return EntryPoints(ep for group_eps in eps.values() for ep in group_eps) + # In Python 3.10 and 3.11 entry_points() returns + # a dict-like SelectableGroups object. + # Use dict.values() instead of eps.values() to avoid the DeprecationWarning + # that SelectableGroups raises when calling .values(). + if isinstance(eps, dict): + return EntryPoints(itertools.chain.from_iterable(dict.values(eps))) + # This case should be unreachable, but is included as a fallback. + return EntryPoints( + ep for group in eps.groups for ep in eps.select(group=group) + ) @cache diff --git a/opentelemetry-api/src/opentelemetry/util/_providers.py b/opentelemetry-api/src/opentelemetry/util/_providers.py index ac801ba..37cc823 100644 --- a/opentelemetry-api/src/opentelemetry/util/_providers.py +++ b/opentelemetry-api/src/opentelemetry/util/_providers.py @@ -5,8 +5,6 @@ from logging import getLogger from os import environ from typing import TYPE_CHECKING, TypeVar, cast -from opentelemetry.util._importlib_metadata import entry_points - if TYPE_CHECKING: from opentelemetry.metrics import MeterProvider from opentelemetry.trace import TracerProvider @@ -19,6 +17,11 @@ logger = getLogger(__name__) def _load_provider( provider_environment_variable: str, provider: str ) -> Provider: # type: ignore[type-var] + # pylint: disable=import-outside-toplevel,no-name-in-module + from opentelemetry.util._importlib_metadata import ( # noqa: PLC0415 + entry_points, + ) + try: provider_name = cast( str, diff --git a/opentelemetry-api/src/opentelemetry/version/__init__.py b/opentelemetry-api/src/opentelemetry/version/__init__.py index a400b9b..50b2dc1 100644 --- a/opentelemetry-api/src/opentelemetry/version/__init__.py +++ b/opentelemetry-api/src/opentelemetry/version/__init__.py @@ -1,4 +1,4 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.42.1" +__version__ = "1.43.0" diff --git a/opentelemetry-api/tests/metrics/test_meter.py b/opentelemetry-api/tests/metrics/test_meter.py index b910046..79b30b6 100644 --- a/opentelemetry-api/tests/metrics/test_meter.py +++ b/opentelemetry-api/tests/metrics/test_meter.py @@ -4,7 +4,7 @@ from logging import WARNING from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import Mock from opentelemetry.metrics import Meter, NoOpMeter @@ -72,9 +72,7 @@ class ChildMeter(Meter): class TestMeter(TestCase): # pylint: disable=no-member - # TODO: convert to assertNoLogs instead of mocking logger when 3.10 is baseline - @patch("opentelemetry.metrics._internal._logger") - def test_repeated_instrument_names(self, logger_mock): + def test_repeated_instrument_names(self): try: test_meter = NoOpMeter("name") @@ -96,18 +94,24 @@ class TestMeter(TestCase): "histogram", "gauge", ]: - getattr(test_meter, f"create_{instrument_name}")(instrument_name) - logger_mock.warning.assert_not_called() + with self.assertNoLogs( + "opentelemetry.metrics._internal", level="WARNING" + ): + getattr(test_meter, f"create_{instrument_name}")( + instrument_name + ) for instrument_name in [ "observable_counter", "observable_gauge", "observable_up_down_counter", ]: - getattr(test_meter, f"create_{instrument_name}")( - instrument_name, Mock() - ) - logger_mock.warning.assert_not_called() + with self.assertNoLogs( + "opentelemetry.metrics._internal", level="WARNING" + ): + getattr(test_meter, f"create_{instrument_name}")( + instrument_name, Mock() + ) def test_repeated_instrument_names_with_different_advisory(self): try: diff --git a/opentelemetry-api/tests/propagators/test__envcarrier.py b/opentelemetry-api/tests/propagators/test__envcarrier.py index e3e36f4..8d00c62 100644 --- a/opentelemetry-api/tests/propagators/test__envcarrier.py +++ b/opentelemetry-api/tests/propagators/test__envcarrier.py @@ -14,6 +14,7 @@ from opentelemetry.context import Context, get_current from opentelemetry.propagators._envcarrier import ( EnvironmentGetter, EnvironmentSetter, + _is_normalized_key, _normalize_key, ) from opentelemetry.trace.propagation.tracecontext import ( @@ -40,7 +41,25 @@ class TestNormalizeKey(unittest.TestCase): self.assertEqual(_normalize_key("ALREADY_VALID"), "ALREADY_VALID") def test_empty_string(self): - self.assertEqual(_normalize_key(""), "") + self.assertEqual(_normalize_key(""), "_") + + +class TestIsNormalizedKey(unittest.TestCase): + """Unit tests for _is_normalized_key.""" + + def test_normalized_keys(self): + self.assertTrue(_is_normalized_key("TRACEPARENT")) + self.assertTrue(_is_normalized_key("X_B3_TRACEID")) + self.assertTrue(_is_normalized_key("H_LLO")) + self.assertTrue(_is_normalized_key("_1ABC")) + + def test_non_normalized_keys(self): + self.assertFalse(_is_normalized_key("")) + self.assertFalse(_is_normalized_key("traceparent")) + self.assertFalse(_is_normalized_key("TraceParent")) + self.assertFalse(_is_normalized_key("X-B3-TRACEID")) + self.assertFalse(_is_normalized_key("1ABC")) + self.assertFalse(_is_normalized_key("héllo")) class TestEnvironmentGetter(unittest.TestCase): @@ -75,6 +94,13 @@ class TestEnvironmentGetter(unittest.TestCase): result = getter.get({}, "empty_key") self.assertEqual(result, [""]) + def test_get_empty_key_maps_to_underscore(self): + """Test empty key lookup uses the normalized underscore name.""" + with patch.dict(os.environ, {"_": "underscore_value"}, clear=True): + getter = EnvironmentGetter() + result = getter.get({}, "") + self.assertEqual(result, ["underscore_value"]) + def test_get_with_special_characters(self): """Test environment variables with special characters.""" with patch.dict( @@ -84,15 +110,45 @@ class TestEnvironmentGetter(unittest.TestCase): result = getter.get({}, "test_key") self.assertEqual(result, ["value with spaces and !@#$%"]) + def test_get_ignores_non_normalized_env_var_name(self): + """Test that non-normalized environment variable names are ignored.""" + with patch.dict(os.environ, {"X-B3-TRACEID": "ignored"}, clear=True): + getter = EnvironmentGetter() + self.assertIsNone(getter.get({}, "x-b3-traceid")) + self.assertIsNone(getter.get({}, "X_B3_TRACEID")) + + def test_get_prefers_normalized_env_var_name(self): + """Test deterministic lookup when normalized names collide.""" + with patch.dict( + os.environ, + {"X_B3_TRACEID": "expected", "X-B3-TRACEID": "ignored"}, + clear=True, + ): + getter = EnvironmentGetter() + self.assertEqual(getter.get({}, "x-b3-traceid"), ["expected"]) + def test_keys(self): """Test getting all environment variable keys.""" - test_env = {"KEY1": "value1", "KEY2": "value2", "key3": "value3"} + test_env = {"KEY1": "value1", "KEY2": "value2", "KEY3": "value3"} with patch.dict(os.environ, test_env, clear=True): getter = EnvironmentGetter() keys = getter.keys({}) expected_keys = {"KEY1", "KEY2", "KEY3"} self.assertEqual(set(keys), expected_keys) + def test_keys_ignores_non_normalized_env_var_names(self): + """Test that keys returns only already-normalized names.""" + test_env = { + "KEY1": "value1", + "X-B3-TRACEID": "ignored", + "1START": "ignored", + "_1START": "value2", + } + with patch.dict(os.environ, test_env, clear=True): + getter = EnvironmentGetter() + keys = getter.keys({}) + self.assertEqual(set(keys), {"KEY1", "_1START"}) + def test_keys_empty_environment(self): """Test getting keys when environment is empty.""" with patch.dict(os.environ, {}, clear=True): @@ -185,6 +241,13 @@ class TestEnvironmentSetter(unittest.TestCase): setter.set(carrier, "empty_key", "") self.assertEqual(carrier, {"EMPTY_KEY": ""}) + def test_set_empty_key_maps_to_underscore(self): + """Test setting an empty key uses the normalized underscore name.""" + setter = EnvironmentSetter() + carrier = {} + setter.set(carrier, "", "value") + self.assertEqual(carrier, {"_": "value"}) + def test_does_not_modify_os_environ(self): """Test that setter does not modify os.environ.""" setter = EnvironmentSetter() @@ -249,6 +312,21 @@ class TestEnvironmentCarrierWithTraceContext(unittest.TestCase): self.assertEqual(span_context.trace_state.get("vendor1"), "value1") self.assertEqual(span_context.trace_state.get("vendor2"), "value2") + @unittest.skipIf( + os.name == "nt", + "Windows environment variable names are case-insensitive", + ) + def test_extract_ignores_lowercase_trace_context_names(self): + """Test extraction ignores non-normalized trace context env names.""" + traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01" + + ctx = self._extract_with_env( + {"traceparent": traceparent, "tracestate": "vendor=value"} + ) + + span_context = trace.get_current_span(ctx).get_span_context() + self.assertFalse(span_context.is_valid) + def test_extract_invalid_traceparent(self): """Test that invalid traceparent formats are handled gracefully. diff --git a/opentelemetry-api/tests/propagators/test_propagators.py b/opentelemetry-api/tests/propagators/test_propagators.py index a62f8c0..a4cc973 100644 --- a/opentelemetry-api/tests/propagators/test_propagators.py +++ b/opentelemetry-api/tests/propagators/test_propagators.py @@ -40,7 +40,7 @@ class TestPropagators(TestCase): @patch.dict(environ, {OTEL_PROPAGATORS: "None"}) @patch("opentelemetry.propagators.composite.CompositePropagator") - def test_none_propogators(self, mock_compositehttppropagator): + def test_none_propagators(self, mock_compositehttppropagator): def test_propagators(propagators): propagators = {propagator.__class__ for propagator in propagators} @@ -61,7 +61,7 @@ class TestPropagators(TestCase): @patch.dict(environ, {OTEL_PROPAGATORS: "tracecontext, None"}) @patch("opentelemetry.propagators.composite.CompositePropagator") - def test_multiple_propogators_with_none( + def test_multiple_propagators_with_none( self, mock_compositehttppropagator ): def test_propagators(propagators): diff --git a/opentelemetry-api/tests/util/test__importlib_metadata.py b/opentelemetry-api/tests/util/test__importlib_metadata.py index f7e5152..18b7505 100644 --- a/opentelemetry-api/tests/util/test__importlib_metadata.py +++ b/opentelemetry-api/tests/util/test__importlib_metadata.py @@ -1,8 +1,16 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +import unittest +import warnings from unittest import TestCase +try: + # pylint: disable-next=no-name-in-module + from importlib.metadata import SelectableGroups as _SelectableGroups +except ImportError: + _SelectableGroups = None + from opentelemetry.metrics import MeterProvider from opentelemetry.util._importlib_metadata import ( EntryPoint, @@ -117,3 +125,21 @@ class TestEntryPoints(TestCase): self.assertIsInstance(normalized, EntryPoints) self.assertEqual(len(normalized), 2) self.assertEqual(list(normalized), [ep1, ep2]) + + @unittest.skipIf( + _SelectableGroups is None, + "SelectableGroups not available on this Python version", + ) + def test_as_entry_points_selectable_groups_no_deprecation_warning(self): + ep1 = EntryPoint(name="foo", value="bar:baz", group="gp") + ep2 = EntryPoint(name="foo2", value="bar2:baz2", group="gp") + + sg = _SelectableGroups({"gp": [ep1, ep2]}) # type: ignore + + with warnings.catch_warnings(): + warnings.simplefilter("error") + normalized = _as_entry_points(sg) + + self.assertIsInstance(normalized, EntryPoints) + self.assertEqual(len(normalized), 2) + self.assertEqual(list(normalized), [ep1, ep2]) |
