Skip to content

Commit ce424cb

Browse files
committed
fix EnvironmentGetter carrier normalization
Assisted-by: ChatGPT 5
1 parent 0b690d2 commit ce424cb

3 files changed

Lines changed: 60 additions & 17 deletions

File tree

.changelog/5221.fixed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-api`: normalize `EnvironmentGetter` carrier keys

opentelemetry-api/src/opentelemetry/propagators/_envcarrier.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,19 +39,36 @@ def __init__(self):
3939
_normalize_key(k): v for k, v in os.environ.items()
4040
}
4141

42+
def _carrier(
43+
self, carrier: typing.Mapping[str, str]
44+
) -> typing.Mapping[str, str]:
45+
return carrier or self.carrier
46+
4247
def get(
4348
self, carrier: typing.Mapping[str, str], key: str
4449
) -> list[str] | None:
4550
"""Get a value from the environment carrier for the given key.
4651
4752
Args:
48-
carrier: Not used; maintained for interface compatibility with Getter[CarrierT]
49-
key: The key to look up (case-insensitive)
53+
carrier: Mapping to read from. If empty, uses the environment
54+
snapshot captured at initialization.
55+
key: The key to look up (normalized before lookup)
5056
5157
Returns:
5258
A list with a single string value if the key exists, None otherwise.
5359
"""
54-
val = self.carrier.get(_normalize_key(key))
60+
source = self._carrier(carrier)
61+
normalized_key = _normalize_key(key)
62+
val = source.get(normalized_key)
63+
if val is None:
64+
val = next(
65+
(
66+
carrier_value
67+
for carrier_key, carrier_value in source.items()
68+
if _normalize_key(carrier_key) == normalized_key
69+
),
70+
None,
71+
)
5572
if val is None:
5673
return None
5774
return [val]
@@ -60,12 +77,13 @@ def keys(self, carrier: typing.Mapping[str, str]) -> list[str]:
6077
"""Get all keys from the environment carrier.
6178
6279
Args:
63-
carrier: Not used; maintained for interface compatibility with Getter[CarrierT]
80+
carrier: Mapping to read from. If empty, uses the environment
81+
snapshot captured at initialization.
6482
6583
Returns:
6684
List of all environment variable keys (normalized).
6785
"""
68-
return list(self.carrier.keys())
86+
return [_normalize_key(key) for key in self._carrier(carrier)]
6987

7088

7189
class EnvironmentSetter(Setter[MutableMapping[str, str]]):

opentelemetry-api/tests/propagators/test__envcarrier.py

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,27 +93,51 @@ def test_keys(self):
9393
expected_keys = {"KEY1", "KEY2", "KEY3"}
9494
self.assertEqual(set(keys), expected_keys)
9595

96+
def test_get_normalizes_carrier_keys(self):
97+
"""Test lookup when the carrier stores non-normalized keys."""
98+
with patch.dict(os.environ, {}, clear=True):
99+
getter = EnvironmentGetter()
100+
carrier = {"x-b3-traceid": "abc123"}
101+
102+
result = getter.get(carrier, "x-b3-traceid")
103+
104+
self.assertEqual(result, ["abc123"])
105+
106+
def test_get_uses_carrier_before_environment_snapshot(self):
107+
"""Test that explicit carriers take precedence over the snapshot."""
108+
with patch.dict(os.environ, {"X_B3_TRACEID": "from_env"}):
109+
getter = EnvironmentGetter()
110+
carrier = {"x-b3-traceid": "from_carrier"}
111+
112+
result = getter.get(carrier, "x-b3-traceid")
113+
114+
self.assertEqual(result, ["from_carrier"])
115+
116+
def test_keys_normalizes_carrier_keys(self):
117+
"""Test getting normalized keys from an explicit carrier."""
118+
with patch.dict(os.environ, {}, clear=True):
119+
getter = EnvironmentGetter()
120+
keys = getter.keys(
121+
{"x-b3-traceid": "value1", "BAGGAGE": "value2"}
122+
)
123+
124+
self.assertEqual(set(keys), {"X_B3_TRACEID", "BAGGAGE"})
125+
96126
def test_keys_empty_environment(self):
97127
"""Test getting keys when environment is empty."""
98128
with patch.dict(os.environ, {}, clear=True):
99129
getter = EnvironmentGetter()
100130
keys = getter.keys({})
101131
self.assertEqual(keys, [])
102132

103-
def test_uses_snapshot_not_carrier_parameter(self):
104-
"""Test that getter uses internal snapshot, not carrier parameter.
105-
106-
The carrier parameter exists for interface compatibility with
107-
Getter[CarrierT], but EnvironmentGetter reads from os.environ at
108-
initialization, creating an immutable snapshot.
109-
"""
133+
def test_uses_snapshot_when_carrier_is_empty(self):
134+
"""Test that getter uses its internal snapshot for an empty carrier."""
110135
with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
111136
getter = EnvironmentGetter()
112-
# Both return same value from snapshot, carrier is ignored
113-
result1 = getter.get({}, "test_key")
114-
result2 = getter.get({"test_key": "different"}, "test_key")
115-
self.assertEqual(result1, ["test_value"])
116-
self.assertEqual(result2, ["test_value"])
137+
138+
result = getter.get({}, "test_key")
139+
140+
self.assertEqual(result, ["test_value"])
117141

118142
def test_snapshot_immutability(self):
119143
"""Test that getter snapshot doesn't see changes after initialization."""

0 commit comments

Comments
 (0)