Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#5076](https://github.com/open-telemetry/opentelemetry-python/pull/5076))
- `opentelemetry-semantic-conventions`: use `X | Y` union annotation
([#5096](https://github.com/open-telemetry/opentelemetry-python/pull/5096))
- `opentelemetry-api`: update `EnvironmentGetter` and `EnvironmentSetter` to use normalized environment variable names
([#5119](https://github.com/open-telemetry/opentelemetry-python/pull/5119))


## Version 1.41.0/0.62b0 (2026-04-09)
Expand Down
26 changes: 16 additions & 10 deletions opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,24 @@
# limitations under the License.

import os
import re
import typing
from collections.abc import MutableMapping

from opentelemetry.propagators.textmap import Getter, Setter


def _normalize_key(key: str) -> str:
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]]):
"""Getter implementation for extracting context and baggage from environment variables.

EnvironmentGetter creates a case-insensitive lookup from the current environment
EnvironmentGetter creates a normalized lookup from the current environment
variables at initialization time and provides simple data access without validation.

Per the OpenTelemetry specification, environment variables are treated as immutable
Expand All @@ -36,15 +44,15 @@ class EnvironmentGetter(Getter[typing.Mapping[str, str]]):
"""

def __init__(self):
# Create case-insensitive lookup from current environment
# Create a normalized lookup from current environment
# Per spec: "creates an in-memory copy of the current environment variables"
self.carrier: typing.Dict[str, str] = {
k.lower(): v for k, v in os.environ.items()
self.carrier: dict[str, str] = {
_normalize_key(k): v for k, v in os.environ.items()
}

def get(
self, carrier: typing.Mapping[str, str], key: str
) -> typing.Optional[typing.List[str]]:
) -> typing.Optional[list[str]]:
"""Get a value from the environment carrier for the given key.

Args:
Expand All @@ -54,11 +62,9 @@ def get(
Returns:
A list with a single string value if the key exists, None otherwise.
"""
val = self.carrier.get(key.lower())
val = self.carrier.get(_normalize_key(key))
if val is None:
return None
if isinstance(val, typing.Iterable) and not isinstance(val, str):
return list(val)
return [val]

def keys(self, carrier: typing.Mapping[str, str]) -> typing.List[str]:
Expand All @@ -68,7 +74,7 @@ def keys(self, carrier: typing.Mapping[str, str]) -> typing.List[str]:
carrier: Not used; maintained for interface compatibility with Getter[CarrierT]

Returns:
List of all environment variable keys (lowercase).
List of all environment variable keys (normalized).
"""
return list(self.carrier.keys())

Expand Down Expand Up @@ -96,4 +102,4 @@ def set(
key: The key to set (will be converted to uppercase)
value: The value to set
"""
carrier[key.upper()] = value
carrier[_normalize_key(key)] = value
25 changes: 24 additions & 1 deletion opentelemetry-api/tests/propagators/test__envcarrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,35 @@
from opentelemetry.propagators._envcarrier import (
EnvironmentGetter,
EnvironmentSetter,
_normalize_key,
)
from opentelemetry.trace.propagation.tracecontext import (
TraceContextTextMapPropagator,
)


class TestNormalizeKey(unittest.TestCase):
"""Unit tests for _normalize_key."""

def test_lowercase(self):
self.assertEqual(_normalize_key("traceparent"), "TRACEPARENT")

def test_hyphen_replaced(self):
self.assertEqual(_normalize_key("trace-parent"), "TRACE_PARENT")

def test_non_ascii_replaced(self):
self.assertEqual(_normalize_key("héllo"), "H_LLO")

def test_leading_digit_prefixed(self):
self.assertEqual(_normalize_key("1abc"), "_1ABC")

def test_already_valid(self):
self.assertEqual(_normalize_key("ALREADY_VALID"), "ALREADY_VALID")

def test_empty_string(self):
self.assertEqual(_normalize_key(""), "")


class TestEnvironmentGetter(unittest.TestCase):
"""Unit tests for EnvironmentGetter."""

Expand Down Expand Up @@ -78,7 +101,7 @@ def test_keys(self):
with patch.dict(os.environ, test_env, clear=True):
getter = EnvironmentGetter()
keys = getter.keys({})
expected_keys = {"key1", "key2", "key3"}
expected_keys = {"KEY1", "KEY2", "KEY3"}
self.assertEqual(set(keys), expected_keys)

def test_keys_empty_environment(self):
Expand Down
Loading