Skip to content

Commit 9686498

Browse files
authored
Update Env var getter / setter to reflect spec changes (#8233)
1 parent 6d522eb commit 9686498

4 files changed

Lines changed: 95 additions & 80 deletions

File tree

api/incubator/src/main/java/io/opentelemetry/api/incubator/propagation/EnvironmentGetter.java

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
import java.util.ArrayList;
1010
import java.util.Collections;
1111
import java.util.List;
12-
import java.util.Locale;
1312
import java.util.Map;
13+
import java.util.concurrent.atomic.AtomicBoolean;
1414
import java.util.logging.Level;
1515
import java.util.logging.Logger;
1616
import javax.annotation.Nullable;
@@ -46,7 +46,8 @@
4646
*/
4747
public final class EnvironmentGetter implements TextMapGetter<Map<String, String>> {
4848

49-
private static final Logger logger = Logger.getLogger(EnvironmentGetter.class.getName());
49+
private static final AtomicBoolean LOG_KEYS_CALLED = new AtomicBoolean(false);
50+
private static final Logger LOGGER = Logger.getLogger(EnvironmentGetter.class.getName());
5051
private static final EnvironmentGetter INSTANCE = new EnvironmentGetter();
5152

5253
private EnvironmentGetter() {}
@@ -58,14 +59,21 @@ public static EnvironmentGetter getInstance() {
5859

5960
@Override
6061
public Iterable<String> keys(Map<String, String> carrier) {
62+
if (LOG_KEYS_CALLED.compareAndSet(false, true)) {
63+
LOGGER.log(
64+
Level.WARNING,
65+
"keys() called on EnvironmentGetter. "
66+
+ "This may produce unexpected results with propagators which depend on case sensitivity or special characters in keys.",
67+
new Throwable());
68+
}
6169
if (carrier == null) {
6270
return Collections.emptyList();
6371
}
6472
List<String> result = new ArrayList<>(carrier.size());
6573
for (String key : carrier.keySet()) {
66-
result.add(key.toLowerCase(Locale.ROOT));
74+
result.add(EnvironmentSetter.normalizeKey(key));
6775
}
68-
return result;
76+
return Collections.unmodifiableList(result);
6977
}
7078

7179
@Nullable
@@ -74,20 +82,21 @@ public String get(@Nullable Map<String, String> carrier, String key) {
7482
if (carrier == null || key == null) {
7583
return null;
7684
}
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;
85+
String normalizedKey = EnvironmentSetter.normalizeKey(key);
86+
// first, perform an optimistic lookup for an exact match on the normalized key
87+
String value = carrier.get(normalizedKey);
88+
if (value != null) {
89+
return value;
90+
}
91+
// next, iterate over the carrier normalizing each entry and evaluating for a match
92+
// if memory allocation becomes an issue, can implement using iterative normalization, comparing
93+
// an entry character by character to the normalized key, normalizing along the way.
94+
for (Map.Entry<String, String> entry : carrier.entrySet()) {
95+
if (EnvironmentSetter.normalizeKey(entry.getKey()).equals(normalizedKey)) {
96+
return entry.getValue();
97+
}
8998
}
90-
return value;
99+
return null;
91100
}
92101

93102
@Override

api/incubator/src/main/java/io/opentelemetry/api/incubator/propagation/EnvironmentSetter.java

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
package io.opentelemetry.api.incubator.propagation;
77

88
import io.opentelemetry.context.propagation.TextMapSetter;
9-
import java.util.Locale;
109
import java.util.Map;
11-
import java.util.logging.Level;
12-
import java.util.logging.Logger;
1310
import javax.annotation.Nullable;
1411

1512
/**
@@ -30,8 +27,10 @@
3027
* conventions:
3128
*
3229
* <ul>
33-
* <li>Converts keys to uppercase (e.g., {@code traceparent} becomes {@code TRACEPARENT})
34-
* <li>Replaces {@code .} and {@code -} with underscores
30+
* <li>ASCII letters are converted to uppercase
31+
* <li>Any character that is not an ASCII letter, digit, or underscore is replaced with an
32+
* underscore
33+
* <li>If the result would start with a digit, an underscore is prepended
3534
* </ul>
3635
*
3736
* <p>Values are validated to contain only characters valid in HTTP header fields per <a
@@ -49,7 +48,6 @@
4948
*/
5049
public final class EnvironmentSetter implements TextMapSetter<Map<String, String>> {
5150

52-
private static final Logger logger = Logger.getLogger(EnvironmentSetter.class.getName());
5351
private static final EnvironmentSetter INSTANCE = new EnvironmentSetter();
5452

5553
private EnvironmentSetter() {}
@@ -64,35 +62,36 @@ public void set(@Nullable Map<String, String> carrier, String key, String value)
6462
if (carrier == null || key == null || value == null) {
6563
return;
6664
}
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);
65+
String normalizedKey = normalizeKey(key);
66+
carrier.put(normalizedKey, value);
8067
}
8168

8269
/**
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).
70+
* Normalizes a key to be a valid environment variable name.
71+
*
72+
* <ul>
73+
* <li>ASCII letters are converted to uppercase
74+
* <li>Any character that is not an ASCII letter, digit, or underscore is replaced with an
75+
* underscore (including {@code .}, {@code -}, whitespace, and control characters)
76+
* <li>If the result would start with a digit, an underscore is prepended
77+
* </ul>
8678
*/
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;
79+
static String normalizeKey(String key) {
80+
StringBuilder sb = new StringBuilder(key.length() + 1);
81+
for (int i = 0; i < key.length(); i++) {
82+
char ch = key.charAt(i);
83+
if (ch >= 'a' && ch <= 'z') {
84+
sb.append((char) (ch - 32));
85+
} else if ((ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') {
86+
sb.append(ch);
87+
} else {
88+
sb.append('_');
9389
}
9490
}
95-
return true;
91+
if (sb.length() > 0 && sb.charAt(0) >= '0' && sb.charAt(0) <= '9') {
92+
sb.insert(0, '_');
93+
}
94+
return sb.toString();
9695
}
9796

9897
@Override

api/incubator/src/test/java/io/opentelemetry/api/incubator/propagation/EnvironmentGetterTest.java

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,19 @@
77

88
import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat;
99

10+
import io.github.netmikey.logunit.api.LogCapturer;
11+
import io.opentelemetry.internal.testing.slf4j.SuppressLogger;
1012
import java.util.Collections;
1113
import java.util.HashMap;
1214
import java.util.Map;
1315
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
1417

1518
class EnvironmentGetterTest {
1619

20+
@RegisterExtension
21+
LogCapturer logCapturer = LogCapturer.create().captureForType(EnvironmentGetter.class);
22+
1723
@Test
1824
void get() {
1925
Map<String, String> carrier = new HashMap<>();
@@ -29,10 +35,10 @@ void get() {
2935
}
3036

3137
@Test
32-
void get_sanitization() {
38+
void get_normalization() {
3339
Map<String, String> carrier = new HashMap<>();
3440
carrier.put("OTEL_TRACE_ID", "val1");
35-
carrier.put("OTEL_BAGGAGE_KEY", "val2");
41+
carrier.put("otel-baggage-key", "val2");
3642

3743
assertThat(EnvironmentGetter.getInstance().get(carrier, "otel.trace.id")).isEqualTo("val1");
3844
assertThat(EnvironmentGetter.getInstance().get(carrier, "otel-baggage-key")).isEqualTo("val2");
@@ -45,37 +51,45 @@ void get_null() {
4551
}
4652

4753
@Test
48-
void keys() {
54+
@SuppressLogger(EnvironmentGetter.class)
55+
void keys_valuesAreNormalized() {
4956
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");
57+
carrier.put("otel.trace.id", "V1");
58+
carrier.put("otel-baggage-key", "V2");
59+
carrier.put("OTEL_FOO", "V2");
60+
61+
// For a carrier containing keys that are both normalized and not normalized, verify all results
62+
// from keys() return values for get.
63+
assertThat(EnvironmentGetter.getInstance().keys(carrier))
64+
.containsExactlyInAnyOrder("OTEL_TRACE_ID", "OTEL_BAGGAGE_KEY", "OTEL_FOO");
65+
for (String key : EnvironmentGetter.getInstance().keys(carrier)) {
66+
assertThat(EnvironmentGetter.getInstance().get(carrier, key)).isNotNull();
67+
}
5468
assertThat(EnvironmentGetter.getInstance().keys(null)).isEmpty();
69+
70+
assertThat(logCapturer.size()).isEqualTo(1);
71+
logCapturer.assertContains("keys() called on EnvironmentGetter");
5572
}
5673

5774
@Test
58-
void get_validHeaderValues() {
75+
void get_valuesAreUnmodified() {
5976
Map<String, String> carrier = new HashMap<>();
6077
carrier.put("KEY1", "simple-value");
6178
carrier.put("KEY2", "value with spaces");
6279
carrier.put("KEY3", "value\twith\ttabs");
80+
carrier.put("KEY4", "value\u0000with\u0001control");
81+
carrier.put("KEY5", "value\nwith\nnewlines");
82+
carrier.put("KEY6", "value\u0080non-ascii");
6383

6484
assertThat(EnvironmentGetter.getInstance().get(carrier, "key1")).isEqualTo("simple-value");
6585
assertThat(EnvironmentGetter.getInstance().get(carrier, "key2")).isEqualTo("value with spaces");
6686
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();
87+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key4"))
88+
.isEqualTo("value\u0000with\u0001control");
89+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key5"))
90+
.isEqualTo("value\nwith\nnewlines");
91+
assertThat(EnvironmentGetter.getInstance().get(carrier, "key6"))
92+
.isEqualTo("value\u0080non-ascii");
7993
}
8094

8195
@Test

api/incubator/src/test/java/io/opentelemetry/api/incubator/propagation/EnvironmentSetterTest.java

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -45,28 +45,21 @@ void set_null() {
4545
}
4646

4747
@Test
48-
void set_validHeaderValues() {
48+
void set_valuesAreUnmodified() {
4949
Map<String, String> carrier = new HashMap<>();
50-
// Printable ASCII and tab are valid per RFC 9110
5150
EnvironmentSetter.getInstance().set(carrier, "key1", "simple-value");
5251
EnvironmentSetter.getInstance().set(carrier, "key2", "value with spaces");
5352
EnvironmentSetter.getInstance().set(carrier, "key3", "value\twith\ttabs");
53+
EnvironmentSetter.getInstance().set(carrier, "key4", "value\u0000with\u0001control");
54+
EnvironmentSetter.getInstance().set(carrier, "key5", "value\nwith\nnewlines");
55+
EnvironmentSetter.getInstance().set(carrier, "key6", "value\u0080non-ascii");
5456

5557
assertThat(carrier).containsEntry("KEY1", "simple-value");
5658
assertThat(carrier).containsEntry("KEY2", "value with spaces");
5759
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();
60+
assertThat(carrier).containsEntry("KEY4", "value\u0000with\u0001control");
61+
assertThat(carrier).containsEntry("KEY5", "value\nwith\nnewlines");
62+
assertThat(carrier).containsEntry("KEY6", "value\u0080non-ascii");
7063
}
7164

7265
@Test

0 commit comments

Comments
 (0)