summaryrefslogtreecommitdiff
path: root/opentelemetry-api
diff options
context:
space:
mode:
authorCarsten Schoenert <c.schoenert@t-online.de>2026-07-01 09:35:22 +0200
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2026-07-01 22:36:34 +0000
commit8f2c4118ad4f64f67b91d8e13b750b2fd4080613 (patch)
treed7988886e937c0a92eddbbc46cd6945f96a4b114 /opentelemetry-api
parentfa4b19cc510b29c1f5e59dce046a9ca22f38f9ad (diff)
Imported using git-ubuntu import.
Notes
Notes: * [11a6262] New upstream version 1.43.0
Diffstat (limited to 'opentelemetry-api')
-rw-r--r--opentelemetry-api/src/opentelemetry/_events/__init__.py10
-rw-r--r--opentelemetry-api/src/opentelemetry/attributes/__init__.py104
-rw-r--r--opentelemetry-api/src/opentelemetry/context/__init__.py38
-rw-r--r--opentelemetry-api/src/opentelemetry/propagate/__init__.py89
-rw-r--r--opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py39
-rw-r--r--opentelemetry-api/src/opentelemetry/propagators/composite.py4
-rw-r--r--opentelemetry-api/src/opentelemetry/propagators/textmap.py13
-rw-r--r--opentelemetry-api/src/opentelemetry/trace/span.py49
-rw-r--r--opentelemetry-api/src/opentelemetry/util/_importlib_metadata.py14
-rw-r--r--opentelemetry-api/src/opentelemetry/util/_providers.py7
-rw-r--r--opentelemetry-api/src/opentelemetry/version/__init__.py2
-rw-r--r--opentelemetry-api/tests/metrics/test_meter.py24
-rw-r--r--opentelemetry-api/tests/propagators/test__envcarrier.py82
-rw-r--r--opentelemetry-api/tests/propagators/test_propagators.py4
-rw-r--r--opentelemetry-api/tests/util/test__importlib_metadata.py26
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])