Skip to content

Commit 73748e9

Browse files
pellaredherin049lzchenxrmx
authored
opentelemetry-api: remove env carrier environment snapshot caching (#5366)
* opentelemetry-api: remove env carrier environment snapshot caching * add chlog entry * Apply suggestions from code review Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> --------- Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com> Co-authored-by: Leighton Chen <lechen@microsoft.com> Co-authored-by: Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com>
1 parent c7690ed commit 73748e9

3 files changed

Lines changed: 86 additions & 102 deletions

File tree

.changelog/5366.changed

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-api`: remove env carrier environment snapshot caching, you now have to pass explicitly the mapping where to get the environment variables from as the `EnvironmentGetter.get` first argument.

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

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
# Copyright The OpenTelemetry Authors
22
# SPDX-License-Identifier: Apache-2.0
33

4-
import os
4+
"""Environment variable carriers for text map propagators.
5+
6+
Use :class:`EnvironmentGetter` with the environment mapping to extract from,
7+
usually ``os.environ`` during application or child-process initialization.
8+
Use :class:`EnvironmentSetter` with a mutable environment copy when preparing
9+
the environment for a child process.
10+
"""
11+
512
import re
613
from collections.abc import Mapping, MutableMapping
714

@@ -30,37 +37,25 @@ def _is_normalized_key(key: str) -> bool:
3037
class EnvironmentGetter(Getter[Mapping[str, str]]):
3138
"""Getter implementation for extracting context and baggage from environment variables.
3239
33-
EnvironmentGetter creates a lookup from the current environment variables
34-
whose names are already normalized at initialization time and provides
35-
simple data access without validation.
36-
37-
Per the OpenTelemetry specification, environment variables are treated as immutable
38-
within a process. For environments where context-carrying environment variables
39-
change between logical requests (e.g., AWS Lambda's _X_AMZN_TRACE_ID), create a
40-
new EnvironmentGetter instance at the start of each request.
40+
EnvironmentGetter reads from the mapping provided as the carrier, normalizes
41+
requested keys, and provides simple data access without validation.
4142
4243
Example usage:
4344
getter = EnvironmentGetter()
44-
traceparent = getter.get({}, "traceparent")
45+
traceparent = getter.get(os.environ, "traceparent")
4546
"""
4647

47-
def __init__(self):
48-
# Per spec, Get reads only normalized environment variable names.
49-
self.carrier: dict[str, str] = {
50-
k: v for k, v in os.environ.items() if _is_normalized_key(k)
51-
}
52-
5348
def get(self, carrier: Mapping[str, str], key: str) -> list[str] | None:
5449
"""Get a value from the environment carrier for the given key.
5550
5651
Args:
57-
carrier: Not used; maintained for interface compatibility with Getter[CarrierT]
52+
carrier: Mapping to read environment variables from
5853
key: The key to look up (will be normalized)
5954
6055
Returns:
6156
A list with a single string value if the key exists, None otherwise.
6257
"""
63-
val = self.carrier.get(_normalize_key(key))
58+
val = carrier.get(_normalize_key(key))
6459
if val is None:
6560
return None
6661
return [val]
@@ -69,12 +64,12 @@ def keys(self, carrier: Mapping[str, str]) -> list[str]:
6964
"""Get all keys from the environment carrier.
7065
7166
Args:
72-
carrier: Not used; maintained for interface compatibility with Getter[CarrierT]
67+
carrier: Mapping to read environment variable keys from
7368
7469
Returns:
7570
List of all already-normalized environment variable keys.
7671
"""
77-
return list(self.carrier.keys())
72+
return [key for key in carrier.keys() if _is_normalized_key(key)]
7873

7974

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

opentelemetry-api/tests/propagators/test__envcarrier.py

Lines changed: 70 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -67,74 +67,64 @@ class TestEnvironmentGetter(unittest.TestCase):
6767

6868
def test_get_existing_env_var(self):
6969
"""Test retrieving an existing environment variable."""
70-
with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
71-
getter = EnvironmentGetter()
72-
result = getter.get({}, "test_key")
73-
self.assertEqual(result, ["test_value"])
74-
75-
def test_get_case_insensitive(self):
76-
"""Test case insensitive lookup for environment variables."""
77-
with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
78-
getter = EnvironmentGetter()
79-
self.assertEqual(getter.get({}, "test_key"), ["test_value"])
80-
self.assertEqual(getter.get({}, "TEST_KEY"), ["test_value"])
81-
self.assertEqual(getter.get({}, "Test_Key"), ["test_value"])
70+
getter = EnvironmentGetter()
71+
result = getter.get({"TEST_KEY": "test_value"}, "test_key")
72+
self.assertEqual(result, ["test_value"])
73+
74+
def test_get_normalizes_requested_key_case(self):
75+
"""Test that requested key case is normalized."""
76+
getter = EnvironmentGetter()
77+
carrier = {"TEST_KEY": "test_value"}
78+
self.assertEqual(getter.get(carrier, "test_key"), ["test_value"])
79+
self.assertEqual(getter.get(carrier, "TEST_KEY"), ["test_value"])
80+
self.assertEqual(getter.get(carrier, "Test_Key"), ["test_value"])
8281

8382
def test_get_nonexistent_env_var(self):
8483
"""Test retrieving a non-existent environment variable."""
85-
with patch.dict(os.environ, {}, clear=True):
86-
getter = EnvironmentGetter()
87-
result = getter.get({}, "nonexistent_key")
88-
self.assertIsNone(result)
84+
getter = EnvironmentGetter()
85+
result = getter.get({}, "nonexistent_key")
86+
self.assertIsNone(result)
8987

9088
def test_get_empty_value(self):
9189
"""Test retrieving an environment variable with empty value."""
92-
with patch.dict(os.environ, {"EMPTY_KEY": ""}):
93-
getter = EnvironmentGetter()
94-
result = getter.get({}, "empty_key")
95-
self.assertEqual(result, [""])
90+
getter = EnvironmentGetter()
91+
result = getter.get({"EMPTY_KEY": ""}, "empty_key")
92+
self.assertEqual(result, [""])
9693

9794
def test_get_empty_key_maps_to_underscore(self):
9895
"""Test empty key lookup uses the normalized underscore name."""
99-
with patch.dict(os.environ, {"_": "underscore_value"}, clear=True):
100-
getter = EnvironmentGetter()
101-
result = getter.get({}, "")
102-
self.assertEqual(result, ["underscore_value"])
96+
getter = EnvironmentGetter()
97+
result = getter.get({"_": "underscore_value"}, "")
98+
self.assertEqual(result, ["underscore_value"])
10399

104100
def test_get_with_special_characters(self):
105101
"""Test environment variables with special characters."""
106-
with patch.dict(
107-
os.environ, {"TEST_KEY": "value with spaces and !@#$%"}
108-
):
109-
getter = EnvironmentGetter()
110-
result = getter.get({}, "test_key")
111-
self.assertEqual(result, ["value with spaces and !@#$%"])
102+
getter = EnvironmentGetter()
103+
result = getter.get(
104+
{"TEST_KEY": "value with spaces and !@#$%"}, "test_key"
105+
)
106+
self.assertEqual(result, ["value with spaces and !@#$%"])
112107

113108
def test_get_ignores_non_normalized_env_var_name(self):
114109
"""Test that non-normalized environment variable names are ignored."""
115-
with patch.dict(os.environ, {"X-B3-TRACEID": "ignored"}, clear=True):
116-
getter = EnvironmentGetter()
117-
self.assertIsNone(getter.get({}, "x-b3-traceid"))
118-
self.assertIsNone(getter.get({}, "X_B3_TRACEID"))
110+
getter = EnvironmentGetter()
111+
carrier = {"X-B3-TRACEID": "ignored"}
112+
self.assertIsNone(getter.get(carrier, "x-b3-traceid"))
113+
self.assertIsNone(getter.get(carrier, "X_B3_TRACEID"))
119114

120115
def test_get_prefers_normalized_env_var_name(self):
121116
"""Test deterministic lookup when normalized names collide."""
122-
with patch.dict(
123-
os.environ,
124-
{"X_B3_TRACEID": "expected", "X-B3-TRACEID": "ignored"},
125-
clear=True,
126-
):
127-
getter = EnvironmentGetter()
128-
self.assertEqual(getter.get({}, "x-b3-traceid"), ["expected"])
117+
getter = EnvironmentGetter()
118+
carrier = {"X_B3_TRACEID": "expected", "X-B3-TRACEID": "ignored"}
119+
self.assertEqual(getter.get(carrier, "x-b3-traceid"), ["expected"])
129120

130121
def test_keys(self):
131122
"""Test getting all environment variable keys."""
132123
test_env = {"KEY1": "value1", "KEY2": "value2", "KEY3": "value3"}
133-
with patch.dict(os.environ, test_env, clear=True):
134-
getter = EnvironmentGetter()
135-
keys = getter.keys({})
136-
expected_keys = {"KEY1", "KEY2", "KEY3"}
137-
self.assertEqual(set(keys), expected_keys)
124+
getter = EnvironmentGetter()
125+
keys = getter.keys(test_env)
126+
expected_keys = {"KEY1", "KEY2", "KEY3"}
127+
self.assertEqual(set(keys), expected_keys)
138128

139129
def test_keys_ignores_non_normalized_env_var_names(self):
140130
"""Test that keys returns only already-normalized names."""
@@ -144,44 +134,40 @@ def test_keys_ignores_non_normalized_env_var_names(self):
144134
"1START": "ignored",
145135
"_1START": "value2",
146136
}
147-
with patch.dict(os.environ, test_env, clear=True):
148-
getter = EnvironmentGetter()
149-
keys = getter.keys({})
150-
self.assertEqual(set(keys), {"KEY1", "_1START"})
137+
getter = EnvironmentGetter()
138+
keys = getter.keys(test_env)
139+
self.assertEqual(set(keys), {"KEY1", "_1START"})
151140

152141
def test_keys_empty_environment(self):
153142
"""Test getting keys when environment is empty."""
154-
with patch.dict(os.environ, {}, clear=True):
155-
getter = EnvironmentGetter()
156-
keys = getter.keys({})
157-
self.assertEqual(keys, [])
143+
getter = EnvironmentGetter()
144+
keys = getter.keys({})
145+
self.assertEqual(keys, [])
146+
147+
def test_uses_carrier_parameter(self):
148+
"""Test that getter reads from the supplied carrier."""
149+
getter = EnvironmentGetter()
150+
result = getter.get({"TEST_KEY": "test_value"}, "test_key")
151+
self.assertEqual(result, ["test_value"])
152+
self.assertIsNone(getter.get({}, "test_key"))
153+
154+
def test_reads_current_carrier_without_snapshot(self):
155+
"""Test that getter sees current carrier values."""
156+
getter = EnvironmentGetter()
157+
carrier = {}
158+
self.assertIsNone(getter.get(carrier, "test_key"))
158159

159-
def test_uses_snapshot_not_carrier_parameter(self):
160-
"""Test that getter uses internal snapshot, not carrier parameter.
160+
carrier["TEST_KEY"] = "new_value"
161+
self.assertEqual(getter.get(carrier, "test_key"), ["new_value"])
161162

162-
The carrier parameter exists for interface compatibility with
163-
Getter[CarrierT], but EnvironmentGetter reads from os.environ at
164-
initialization, creating an immutable snapshot.
165-
"""
166-
with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
167-
getter = EnvironmentGetter()
168-
# Both return same value from snapshot, carrier is ignored
169-
result1 = getter.get({}, "test_key")
170-
result2 = getter.get({"test_key": "different"}, "test_key")
171-
self.assertEqual(result1, ["test_value"])
172-
self.assertEqual(result2, ["test_value"])
173-
174-
def test_snapshot_immutability(self):
175-
"""Test that getter snapshot doesn't see changes after initialization."""
163+
def test_extracts_from_os_environ_when_provided_as_carrier(self):
164+
"""Test using os.environ directly as the carrier."""
176165
with patch.dict(os.environ, {}, clear=True):
177166
getter = EnvironmentGetter()
178-
self.assertIsNone(getter.get({}, "test_key"))
167+
self.assertIsNone(getter.get(os.environ, "test_key"))
179168

180-
# Add environment variable after initialization
181169
os.environ["TEST_KEY"] = "new_value"
182-
183-
# Getter should still not see the new value
184-
self.assertIsNone(getter.get({}, "test_key"))
170+
self.assertEqual(getter.get(os.environ, "test_key"), ["new_value"])
185171

186172

187173
class TestEnvironmentSetter(unittest.TestCase):
@@ -271,7 +257,7 @@ def _extract_with_env(self, env_vars):
271257
"""Helper: Extract context from environment variables."""
272258
with patch.dict(os.environ, env_vars, clear=True):
273259
getter = EnvironmentGetter()
274-
return self.propagator.extract({}, getter=getter)
260+
return self.propagator.extract(os.environ, getter=getter)
275261

276262
def _inject_to_env(self, context):
277263
"""Helper: Inject context into environment dict."""
@@ -359,7 +345,9 @@ def test_extract_preserves_context_on_invalid_traceparent(self):
359345

360346
with patch.dict(os.environ, {"TRACEPARENT": "invalid"}, clear=True):
361347
getter = EnvironmentGetter()
362-
ctx = self.propagator.extract({}, context=orig_ctx, getter=getter)
348+
ctx = self.propagator.extract(
349+
os.environ, context=orig_ctx, getter=getter
350+
)
363351

364352
self.assertDictEqual(ctx, orig_ctx)
365353

@@ -450,8 +438,8 @@ def test_case_handling(self):
450438
with patch.dict(os.environ, test_env, clear=True):
451439
getter = EnvironmentGetter()
452440
# Propagator uses lowercase keys
453-
self.assertIsNotNone(getter.get({}, "traceparent"))
454-
self.assertIsNotNone(getter.get({}, "tracestate"))
441+
self.assertIsNotNone(getter.get(os.environ, "traceparent"))
442+
self.assertIsNotNone(getter.get(os.environ, "tracestate"))
455443

456444
@patch("opentelemetry.trace.INVALID_SPAN_CONTEXT")
457445
@patch("opentelemetry.trace.get_current_span")
@@ -495,7 +483,7 @@ def _extract_with_env(self, env_vars):
495483
"""Helper: Extract baggage from environment variables."""
496484
with patch.dict(os.environ, env_vars, clear=True):
497485
getter = EnvironmentGetter()
498-
return self.propagator.extract({}, getter=getter)
486+
return self.propagator.extract(os.environ, getter=getter)
499487

500488
def _inject_to_env(self, context):
501489
"""Helper: Inject baggage into environment dict."""
@@ -599,7 +587,7 @@ def test_extract_all_w3c_headers(self):
599587

600588
with patch.dict(os.environ, env_vars, clear=True):
601589
getter = EnvironmentGetter()
602-
ctx = self.propagator.extract({}, getter=getter)
590+
ctx = self.propagator.extract(os.environ, getter=getter)
603591

604592
# Verify traceparent was extracted
605593
span_context = trace.get_current_span(ctx).get_span_context()
@@ -635,7 +623,7 @@ def test_empty_environment(self):
635623
"""Test behavior with completely empty environment."""
636624
with patch.dict(os.environ, {}, clear=True):
637625
getter = EnvironmentGetter()
638-
ctx = self.propagator.extract({}, getter=getter)
626+
ctx = self.propagator.extract(os.environ, getter=getter)
639627

640628
# Should not crash, return valid context
641629
self.assertIsInstance(ctx, Context)

0 commit comments

Comments
 (0)