Skip to content

Commit c71c4d9

Browse files
authored
Load file config YAML using core schema, ensure that env var substiut… (#6436)
1 parent 7953048 commit c71c4d9

File tree

2 files changed

+114
-36
lines changed

2 files changed

+114
-36
lines changed

sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfiguration.java

Lines changed: 81 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,15 @@
2424
import java.util.regex.Pattern;
2525
import org.snakeyaml.engine.v2.api.Load;
2626
import org.snakeyaml.engine.v2.api.LoadSettings;
27+
import org.snakeyaml.engine.v2.common.ScalarStyle;
2728
import org.snakeyaml.engine.v2.constructor.StandardConstructor;
29+
import org.snakeyaml.engine.v2.exceptions.ConstructorException;
30+
import org.snakeyaml.engine.v2.exceptions.YamlEngineException;
2831
import org.snakeyaml.engine.v2.nodes.MappingNode;
29-
import org.yaml.snakeyaml.Yaml;
32+
import org.snakeyaml.engine.v2.nodes.Node;
33+
import org.snakeyaml.engine.v2.nodes.NodeTuple;
34+
import org.snakeyaml.engine.v2.nodes.ScalarNode;
35+
import org.snakeyaml.engine.v2.schema.CoreSchema;
3036

3137
/**
3238
* Configure {@link OpenTelemetrySdk} from YAML configuration files conforming to the schema in <a
@@ -127,7 +133,7 @@ static OpenTelemetryConfiguration parse(
127133

128134
// Visible for testing
129135
static Object loadYaml(InputStream inputStream, Map<String, String> environmentVariables) {
130-
LoadSettings settings = LoadSettings.builder().build();
136+
LoadSettings settings = LoadSettings.builder().setSchema(new CoreSchema()).build();
131137
Load yaml = new Load(settings, new EnvSubstitutionConstructor(settings, environmentVariables));
132138
return yaml.loadFromInputStream(inputStream);
133139
}
@@ -145,52 +151,94 @@ static Object loadYaml(InputStream inputStream, Map<String, String> environmentV
145151
*/
146152
private static final class EnvSubstitutionConstructor extends StandardConstructor {
147153

148-
// Yaml is not thread safe but this instance is always used on the same thread
149-
private final Yaml yaml = new Yaml();
154+
// Load is not thread safe but this instance is always used on the same thread
155+
private final Load load;
150156
private final Map<String, String> environmentVariables;
151157

152158
private EnvSubstitutionConstructor(
153159
LoadSettings loadSettings, Map<String, String> environmentVariables) {
154160
super(loadSettings);
161+
load = new Load(loadSettings);
155162
this.environmentVariables = environmentVariables;
156163
}
157164

165+
/**
166+
* Implementation is same as {@link
167+
* org.snakeyaml.engine.v2.constructor.BaseConstructor#constructMapping(MappingNode)} except we
168+
* override the resolution of values with our custom {@link #constructValueObject(Node)}, which
169+
* performs environment variable substitution.
170+
*/
158171
@Override
172+
@SuppressWarnings({"ReturnValueIgnored", "CatchingUnchecked"})
159173
protected Map<Object, Object> constructMapping(MappingNode node) {
160-
// First call the super to construct mapping from MappingNode as usual
161-
Map<Object, Object> result = super.constructMapping(node);
162-
163-
// Iterate through the map entries, and:
164-
// 1. Identify entries which are scalar strings eligible for environment variable substitution
165-
// 2. Apply environment variable substitution
166-
// 3. Re-parse substituted value so it has correct type (i.e. yaml.load(newVal))
167-
for (Map.Entry<Object, Object> entry : result.entrySet()) {
168-
Object value = entry.getValue();
169-
if (!(value instanceof String)) {
170-
continue;
174+
Map<Object, Object> mapping = settings.getDefaultMap().apply(node.getValue().size());
175+
List<NodeTuple> nodeValue = node.getValue();
176+
for (NodeTuple tuple : nodeValue) {
177+
Node keyNode = tuple.getKeyNode();
178+
Object key = constructObject(keyNode);
179+
if (key != null) {
180+
try {
181+
key.hashCode(); // check circular dependencies
182+
} catch (Exception e) {
183+
throw new ConstructorException(
184+
"while constructing a mapping",
185+
node.getStartMark(),
186+
"found unacceptable key " + key,
187+
tuple.getKeyNode().getStartMark(),
188+
e);
189+
}
171190
}
172-
173-
String val = (String) value;
174-
Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val);
175-
if (!matcher.find()) {
176-
continue;
191+
Node valueNode = tuple.getValueNode();
192+
Object value = constructValueObject(valueNode);
193+
if (keyNode.isRecursive()) {
194+
if (settings.getAllowRecursiveKeys()) {
195+
postponeMapFilling(mapping, key, value);
196+
} else {
197+
throw new YamlEngineException(
198+
"Recursive key for mapping is detected but it is not configured to be allowed.");
199+
}
200+
} else {
201+
mapping.put(key, value);
177202
}
203+
}
178204

179-
int offset = 0;
180-
StringBuilder newVal = new StringBuilder();
181-
do {
182-
MatchResult matchResult = matcher.toMatchResult();
183-
String replacement = environmentVariables.getOrDefault(matcher.group(1), "");
184-
newVal.append(val, offset, matchResult.start()).append(replacement);
185-
offset = matchResult.end();
186-
} while (matcher.find());
187-
if (offset != val.length()) {
188-
newVal.append(val, offset, val.length());
189-
}
190-
entry.setValue(yaml.load(newVal.toString()));
205+
return mapping;
206+
}
207+
208+
private Object constructValueObject(Node node) {
209+
Object value = constructObject(node);
210+
if (!(node instanceof ScalarNode)) {
211+
return value;
212+
}
213+
if (!(value instanceof String)) {
214+
return value;
215+
}
216+
217+
String val = (String) value;
218+
Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val);
219+
if (!matcher.find()) {
220+
return value;
191221
}
192222

193-
return result;
223+
int offset = 0;
224+
StringBuilder newVal = new StringBuilder();
225+
ScalarStyle scalarStyle = ((ScalarNode) node).getScalarStyle();
226+
do {
227+
MatchResult matchResult = matcher.toMatchResult();
228+
String replacement = environmentVariables.getOrDefault(matcher.group(1), "");
229+
newVal.append(val, offset, matchResult.start()).append(replacement);
230+
offset = matchResult.end();
231+
} while (matcher.find());
232+
if (offset != val.length()) {
233+
newVal.append(val, offset, val.length());
234+
}
235+
// If the value was double quoted, retain the double quotes so we don't change a value
236+
// intended to be a string to a different type after environment variable substitution
237+
if (scalarStyle == ScalarStyle.DOUBLE_QUOTED) {
238+
newVal.insert(0, "\"");
239+
newVal.append("\"");
240+
}
241+
return load.loadFromString(newVal.toString());
194242
}
195243
}
196244
}

sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/FileConfigurationParseTest.java

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,15 +387,36 @@ void parse_nullBoxedPrimitivesParsedToNull() {
387387
new Sampler().withTraceIdRatioBased(new TraceIdRatioBased()))));
388388
}
389389

390+
@ParameterizedTest
391+
@MethodSource("coreSchemaValuesArgs")
392+
void coreSchemaValues(String rawYaml, Object expectedYamlResult) {
393+
Object yaml =
394+
FileConfiguration.loadYaml(
395+
new ByteArrayInputStream(rawYaml.getBytes(StandardCharsets.UTF_8)),
396+
Collections.emptyMap());
397+
assertThat(yaml).isEqualTo(expectedYamlResult);
398+
}
399+
400+
@SuppressWarnings("unchecked")
401+
private static java.util.stream.Stream<Arguments> coreSchemaValuesArgs() {
402+
return java.util.stream.Stream.of(
403+
Arguments.of("key1: 0o123\n", mapOf(entry("key1", 83))),
404+
Arguments.of("key1: 0123\n", mapOf(entry("key1", 123))),
405+
Arguments.of("key1: 0xdeadbeef\n", mapOf(entry("key1", 3735928559L))),
406+
Arguments.of("key1: \"0xdeadbeef\"\n", mapOf(entry("key1", "0xdeadbeef"))));
407+
}
408+
390409
@ParameterizedTest
391410
@MethodSource("envVarSubstitutionArgs")
392411
void envSubstituteAndLoadYaml(String rawYaml, Object expectedYamlResult) {
393412
Map<String, String> environmentVariables = new HashMap<>();
394413
environmentVariables.put("STR_1", "value1");
395414
environmentVariables.put("STR_2", "value2");
415+
environmentVariables.put("EMPTY_STR", "");
396416
environmentVariables.put("BOOL", "true");
397417
environmentVariables.put("INT", "1");
398418
environmentVariables.put("FLOAT", "1.1");
419+
environmentVariables.put("HEX", "0xdeadbeef");
399420

400421
Object yaml =
401422
FileConfiguration.loadYaml(
@@ -412,6 +433,7 @@ private static java.util.stream.Stream<Arguments> envVarSubstitutionArgs() {
412433
Arguments.of("key1: ${BOOL}\n", mapOf(entry("key1", true))),
413434
Arguments.of("key1: ${INT}\n", mapOf(entry("key1", 1))),
414435
Arguments.of("key1: ${FLOAT}\n", mapOf(entry("key1", 1.1))),
436+
Arguments.of("key1: ${HEX}\n", mapOf(entry("key1", 3735928559L))),
415437
Arguments.of(
416438
"key1: ${STR_1}\n" + "key2: value2\n",
417439
mapOf(entry("key1", "value1"), entry("key2", "value2"))),
@@ -421,7 +443,8 @@ private static java.util.stream.Stream<Arguments> envVarSubstitutionArgs() {
421443
// Multiple environment variables referenced
422444
Arguments.of("key1: ${STR_1}${STR_2}\n", mapOf(entry("key1", "value1value2"))),
423445
Arguments.of("key1: ${STR_1} ${STR_2}\n", mapOf(entry("key1", "value1 value2"))),
424-
// Undefined environment variable
446+
// Undefined / empty environment variable
447+
Arguments.of("key1: ${EMPTY_STR}\n", mapOf(entry("key1", null))),
425448
Arguments.of("key1: ${STR_3}\n", mapOf(entry("key1", null))),
426449
Arguments.of("key1: ${STR_1} ${STR_3}\n", mapOf(entry("key1", "value1"))),
427450
// Environment variable keys must match pattern: [a-zA-Z_]+[a-zA-Z0-9_]*
@@ -432,7 +455,14 @@ private static java.util.stream.Stream<Arguments> envVarSubstitutionArgs() {
432455
"key1:\n ${STR_1}: value1\n",
433456
mapOf(entry("key1", mapOf(entry("${STR_1}", "value1"))))),
434457
Arguments.of(
435-
"key1:\n - ${STR_1}\n", mapOf(entry("key1", Collections.singletonList("${STR_1}")))));
458+
"key1:\n - ${STR_1}\n", mapOf(entry("key1", Collections.singletonList("${STR_1}")))),
459+
// Quoted environment variables
460+
Arguments.of("key1: \"${HEX}\"\n", mapOf(entry("key1", "0xdeadbeef"))),
461+
Arguments.of("key1: \"${STR_1}\"\n", mapOf(entry("key1", "value1"))),
462+
Arguments.of("key1: \"${EMPTY_STR}\"\n", mapOf(entry("key1", ""))),
463+
Arguments.of("key1: \"${BOOL}\"\n", mapOf(entry("key1", "true"))),
464+
Arguments.of("key1: \"${INT}\"\n", mapOf(entry("key1", "1"))),
465+
Arguments.of("key1: \"${FLOAT}\"\n", mapOf(entry("key1", "1.1"))));
436466
}
437467

438468
private static <K, V> Map.Entry<K, V> entry(K key, @Nullable V value) {
@@ -461,7 +491,7 @@ void read_WithEnvironmentVariables() {
461491
+ " - batch:\n"
462492
+ " exporter:\n"
463493
+ " otlp:\n"
464-
+ " endpoint: \"${UNSET_ENV_VAR}\"\n";
494+
+ " endpoint: ${UNSET_ENV_VAR}\n";
465495
Map<String, String> envVars = new HashMap<>();
466496
envVars.put("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317");
467497
OpenTelemetryConfiguration model =

0 commit comments

Comments
 (0)