Skip to content

Commit 2381201

Browse files
authored
Implement Environment Variable Context Propagation carriers in api/in… (#8074)
1 parent 6cdce86 commit 2381201

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-0
lines changed
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.incubator.propagation;
7+
8+
import io.opentelemetry.context.propagation.TextMapGetter;
9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import java.util.Locale;
13+
import java.util.Map;
14+
import java.util.logging.Level;
15+
import java.util.logging.Logger;
16+
import javax.annotation.Nullable;
17+
18+
/**
19+
* A {@link TextMapGetter} that extracts context from a map carrier, intended for use with
20+
* environment variables in child processes.
21+
*
22+
* <p>This is useful when a child process needs to extract propagated context from its environment.
23+
* For example:
24+
*
25+
* <pre>{@code
26+
* Map<String, String> env = System.getenv();
27+
* Context context = contextPropagators.getTextMapPropagator()
28+
* .extract(Context.current(), env, EnvironmentGetter.getInstance());
29+
* }</pre>
30+
*
31+
* <p>This getter automatically sanitizes keys to match environment variable naming conventions:
32+
*
33+
* <ul>
34+
* <li>Converts keys to uppercase (e.g., {@code traceparent} becomes {@code TRACEPARENT})
35+
* <li>Replaces {@code .} and {@code -} with underscores
36+
* </ul>
37+
*
38+
* <p>Values are validated to contain only characters valid in HTTP header fields per <a
39+
* href="https://datatracker.ietf.org/doc/html/rfc9110#section-5.5">RFC 9110</a> (visible ASCII
40+
* characters, space, and horizontal tab). Values containing invalid characters are treated as
41+
* absent and {@code null} is returned.
42+
*
43+
* @see <a href=
44+
* "https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md#format-restrictions">Environment
45+
* Variable Format Restrictions</a>
46+
*/
47+
public final class EnvironmentGetter implements TextMapGetter<Map<String, String>> {
48+
49+
private static final Logger logger = Logger.getLogger(EnvironmentGetter.class.getName());
50+
private static final EnvironmentGetter INSTANCE = new EnvironmentGetter();
51+
52+
private EnvironmentGetter() {}
53+
54+
/** Returns the singleton instance of {@link EnvironmentGetter}. */
55+
public static EnvironmentGetter getInstance() {
56+
return INSTANCE;
57+
}
58+
59+
@Override
60+
public Iterable<String> keys(Map<String, String> carrier) {
61+
if (carrier == null) {
62+
return Collections.emptyList();
63+
}
64+
List<String> result = new ArrayList<>(carrier.size());
65+
for (String key : carrier.keySet()) {
66+
result.add(key.toLowerCase(Locale.ROOT));
67+
}
68+
return result;
69+
}
70+
71+
@Nullable
72+
@Override
73+
public String get(@Nullable Map<String, String> carrier, String key) {
74+
if (carrier == null || key == null) {
75+
return null;
76+
}
77+
// Spec recommends using uppercase and underscores for environment variable
78+
// names for maximum
79+
// cross-platform compatibility.
80+
String sanitizedKey = key.replace('.', '_').replace('-', '_').toUpperCase(Locale.ROOT);
81+
String value = carrier.get(sanitizedKey);
82+
if (value != null && !EnvironmentSetter.isValidHttpHeaderValue(value)) {
83+
logger.log(
84+
Level.FINE,
85+
"Ignoring environment variable '{0}': "
86+
+ "value contains characters not valid in HTTP header fields per RFC 9110.",
87+
sanitizedKey);
88+
return null;
89+
}
90+
return value;
91+
}
92+
93+
@Override
94+
public String toString() {
95+
return "EnvironmentGetter";
96+
}
97+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.incubator.propagation;
7+
8+
import io.opentelemetry.context.propagation.TextMapSetter;
9+
import java.util.Locale;
10+
import java.util.Map;
11+
import java.util.logging.Level;
12+
import java.util.logging.Logger;
13+
import javax.annotation.Nullable;
14+
15+
/**
16+
* A {@link TextMapSetter} that injects context into a map carrier, intended for use with
17+
* environment variables when spawning child processes.
18+
*
19+
* <p>This is useful when an application needs to propagate context to sub-processes via their
20+
* environment. For example, when using {@link ProcessBuilder}:
21+
*
22+
* <pre>{@code
23+
* Map<String, String> env = new HashMap<>();
24+
* contextPropagators.getTextMapPropagator().inject(context, env, EnvironmentSetter.getInstance());
25+
* ProcessBuilder processBuilder = new ProcessBuilder();
26+
* processBuilder.environment().putAll(env);
27+
* }</pre>
28+
*
29+
* <p>This setter automatically sanitizes keys to be compatible with environment variable naming
30+
* conventions:
31+
*
32+
* <ul>
33+
* <li>Converts keys to uppercase (e.g., {@code traceparent} becomes {@code TRACEPARENT})
34+
* <li>Replaces {@code .} and {@code -} with underscores
35+
* </ul>
36+
*
37+
* <p>Values are validated to contain only characters valid in HTTP header fields per <a
38+
* href="https://datatracker.ietf.org/doc/html/rfc9110#section-5.5">RFC 9110</a> (visible ASCII
39+
* characters, space, and horizontal tab). Values containing invalid characters are silently
40+
* skipped.
41+
*
42+
* <p><strong>Size limitations:</strong> Environment variable sizes are platform-dependent (e.g.,
43+
* Windows limits name=value pairs to 32,767 characters). Callers are responsible for being aware of
44+
* platform-specific limits when injecting context.
45+
*
46+
* @see <a href=
47+
* "https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/context/env-carriers.md#format-restrictions">Environment
48+
* Variable Format Restrictions</a>
49+
*/
50+
public final class EnvironmentSetter implements TextMapSetter<Map<String, String>> {
51+
52+
private static final Logger logger = Logger.getLogger(EnvironmentSetter.class.getName());
53+
private static final EnvironmentSetter INSTANCE = new EnvironmentSetter();
54+
55+
private EnvironmentSetter() {}
56+
57+
/** Returns the singleton instance of {@link EnvironmentSetter}. */
58+
public static EnvironmentSetter getInstance() {
59+
return INSTANCE;
60+
}
61+
62+
@Override
63+
public void set(@Nullable Map<String, String> carrier, String key, String value) {
64+
if (carrier == null || key == null || value == null) {
65+
return;
66+
}
67+
if (!isValidHttpHeaderValue(value)) {
68+
logger.log(
69+
Level.FINE,
70+
"Skipping environment variable injection for key ''{0}'': "
71+
+ "value contains characters not valid in HTTP header fields per RFC 9110.",
72+
key);
73+
return;
74+
}
75+
// Spec recommends using uppercase and underscores for environment variable
76+
// names for maximum
77+
// cross-platform compatibility.
78+
String sanitizedKey = key.replace('.', '_').replace('-', '_').toUpperCase(Locale.ROOT);
79+
carrier.put(sanitizedKey, value);
80+
}
81+
82+
/**
83+
* Checks whether a string contains only characters valid in HTTP header field values per <a
84+
* href="https://datatracker.ietf.org/doc/html/rfc9110#section-5.5">RFC 9110 Section 5.5</a>.
85+
* Valid characters are: visible ASCII (0x21-0x7E), space (0x20), and horizontal tab (0x09).
86+
*/
87+
static boolean isValidHttpHeaderValue(String value) {
88+
for (int i = 0; i < value.length(); i++) {
89+
char ch = value.charAt(i);
90+
// VCHAR (0x21-0x7E), SP (0x20), HTAB (0x09)
91+
if (ch != '\t' && (ch < ' ' || ch > '~')) {
92+
return false;
93+
}
94+
}
95+
return true;
96+
}
97+
98+
@Override
99+
public String toString() {
100+
return "EnvironmentSetter";
101+
}
102+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.incubator.propagation;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
10+
import java.util.Collections;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.Test;
14+
15+
class EnvironmentGetterTest {
16+
17+
@Test
18+
void get() {
19+
Map<String, String> carrier = new HashMap<>();
20+
carrier.put("TRACEPARENT", "val1");
21+
carrier.put("TRACESTATE", "val2");
22+
carrier.put("BAGGAGE", "val3");
23+
carrier.put("OTHER", "val4");
24+
25+
assertThat(EnvironmentGetter.getInstance().get(carrier, "traceparent")).isEqualTo("val1");
26+
assertThat(EnvironmentGetter.getInstance().get(carrier, "TRACESTATE")).isEqualTo("val2");
27+
assertThat(EnvironmentGetter.getInstance().get(carrier, "Baggage")).isEqualTo("val3");
28+
assertThat(EnvironmentGetter.getInstance().get(carrier, "other")).isEqualTo("val4");
29+
}
30+
31+
@Test
32+
void get_sanitization() {
33+
Map<String, String> carrier = new HashMap<>();
34+
carrier.put("OTEL_TRACE_ID", "val1");
35+
carrier.put("OTEL_BAGGAGE_KEY", "val2");
36+
37+
assertThat(EnvironmentGetter.getInstance().get(carrier, "otel.trace.id")).isEqualTo("val1");
38+
assertThat(EnvironmentGetter.getInstance().get(carrier, "otel-baggage-key")).isEqualTo("val2");
39+
}
40+
41+
@Test
42+
void get_null() {
43+
assertThat(EnvironmentGetter.getInstance().get(null, "key")).isNull();
44+
assertThat(EnvironmentGetter.getInstance().get(Collections.emptyMap(), null)).isNull();
45+
}
46+
47+
@Test
48+
void keys() {
49+
Map<String, String> carrier = new HashMap<>();
50+
carrier.put("K1", "V1");
51+
carrier.put("K2", "V2");
52+
53+
assertThat(EnvironmentGetter.getInstance().keys(carrier)).containsExactlyInAnyOrder("k1", "k2");
54+
assertThat(EnvironmentGetter.getInstance().keys(null)).isEmpty();
55+
}
56+
57+
@Test
58+
void get_validHeaderValues() {
59+
Map<String, String> carrier = new HashMap<>();
60+
carrier.put("KEY1", "simple-value");
61+
carrier.put("KEY2", "value with spaces");
62+
carrier.put("KEY3", "value\twith\ttabs");
63+
64+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key1")).isEqualTo("simple-value");
65+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key2")).isEqualTo("value with spaces");
66+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key3")).isEqualTo("value\twith\ttabs");
67+
}
68+
69+
@Test
70+
void get_invalidHeaderValues() {
71+
Map<String, String> carrier = new HashMap<>();
72+
carrier.put("KEY1", "value\u0000with\u0001control");
73+
carrier.put("KEY2", "value\nwith\nnewlines");
74+
carrier.put("KEY3", "value\u0080non-ascii");
75+
76+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key1")).isNull();
77+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key2")).isNull();
78+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key3")).isNull();
79+
}
80+
81+
@Test
82+
void testToString() {
83+
assertThat(EnvironmentGetter.getInstance().toString()).isEqualTo("EnvironmentGetter");
84+
}
85+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.api.incubator.propagation;
7+
8+
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
9+
10+
import java.util.HashMap;
11+
import java.util.Map;
12+
import org.junit.jupiter.api.Test;
13+
14+
class EnvironmentSetterTest {
15+
16+
@Test
17+
void set() {
18+
Map<String, String> carrier = new HashMap<>();
19+
EnvironmentSetter.getInstance().set(carrier, "traceparent", "val1");
20+
EnvironmentSetter.getInstance().set(carrier, "TRACESTATE", "val2");
21+
EnvironmentSetter.getInstance().set(carrier, "Baggage", "val3");
22+
23+
assertThat(carrier).containsEntry("TRACEPARENT", "val1");
24+
assertThat(carrier).containsEntry("TRACESTATE", "val2");
25+
assertThat(carrier).containsEntry("BAGGAGE", "val3");
26+
}
27+
28+
@Test
29+
void set_sanitization() {
30+
Map<String, String> carrier = new HashMap<>();
31+
EnvironmentSetter.getInstance().set(carrier, "otel.trace.id", "val1");
32+
EnvironmentSetter.getInstance().set(carrier, "otel-baggage-key", "val2");
33+
34+
assertThat(carrier).containsEntry("OTEL_TRACE_ID", "val1");
35+
assertThat(carrier).containsEntry("OTEL_BAGGAGE_KEY", "val2");
36+
}
37+
38+
@Test
39+
void set_null() {
40+
Map<String, String> carrier = new HashMap<>();
41+
EnvironmentSetter.getInstance().set(null, "key", "val");
42+
EnvironmentSetter.getInstance().set(carrier, null, "val");
43+
EnvironmentSetter.getInstance().set(carrier, "key", null);
44+
assertThat(carrier).isEmpty();
45+
}
46+
47+
@Test
48+
void set_validHeaderValues() {
49+
Map<String, String> carrier = new HashMap<>();
50+
// Printable ASCII and tab are valid per RFC 9110
51+
EnvironmentSetter.getInstance().set(carrier, "key1", "simple-value");
52+
EnvironmentSetter.getInstance().set(carrier, "key2", "value with spaces");
53+
EnvironmentSetter.getInstance().set(carrier, "key3", "value\twith\ttabs");
54+
55+
assertThat(carrier).containsEntry("KEY1", "simple-value");
56+
assertThat(carrier).containsEntry("KEY2", "value with spaces");
57+
assertThat(carrier).containsEntry("KEY3", "value\twith\ttabs");
58+
}
59+
60+
@Test
61+
void set_invalidHeaderValues() {
62+
Map<String, String> carrier = new HashMap<>();
63+
// Control characters and non-ASCII are invalid per RFC 9110
64+
EnvironmentSetter.getInstance().set(carrier, "key1", "value\u0000with\u0001control");
65+
EnvironmentSetter.getInstance().set(carrier, "key2", "value\nwith\nnewlines");
66+
EnvironmentSetter.getInstance().set(carrier, "key3", "value\rwith\rcarriage");
67+
EnvironmentSetter.getInstance().set(carrier, "key4", "value\u0080non-ascii");
68+
69+
assertThat(carrier).isEmpty();
70+
}
71+
72+
@Test
73+
void testToString() {
74+
assertThat(EnvironmentSetter.getInstance().toString()).isEqualTo("EnvironmentSetter");
75+
}
76+
}

0 commit comments

Comments
 (0)