22
22
import java .io .IOException ;
23
23
import java .io .InputStream ;
24
24
import java .util .Collections ;
25
- import java .util .List ;
26
25
import java .util .Map ;
26
+ import java .util .Objects ;
27
27
import java .util .logging .Logger ;
28
28
import java .util .regex .MatchResult ;
29
29
import java .util .regex .Matcher ;
30
30
import java .util .regex .Pattern ;
31
31
import javax .annotation .Nullable ;
32
32
import org .snakeyaml .engine .v2 .api .Load ;
33
33
import org .snakeyaml .engine .v2 .api .LoadSettings ;
34
+ import org .snakeyaml .engine .v2 .api .YamlUnicodeReader ;
34
35
import org .snakeyaml .engine .v2 .common .ScalarStyle ;
35
- import org .snakeyaml .engine .v2 .constructor .StandardConstructor ;
36
- import org .snakeyaml .engine .v2 .exceptions .ConstructorException ;
37
- import org .snakeyaml .engine .v2 .exceptions .YamlEngineException ;
36
+ import org .snakeyaml .engine .v2 .composer .Composer ;
38
37
import org .snakeyaml .engine .v2 .nodes .MappingNode ;
39
38
import org .snakeyaml .engine .v2 .nodes .Node ;
40
- import org .snakeyaml .engine .v2 .nodes .NodeTuple ;
41
39
import org .snakeyaml .engine .v2 .nodes .ScalarNode ;
40
+ import org .snakeyaml .engine .v2 .nodes .Tag ;
41
+ import org .snakeyaml .engine .v2 .parser .ParserImpl ;
42
+ import org .snakeyaml .engine .v2 .resolver .ScalarResolver ;
43
+ import org .snakeyaml .engine .v2 .scanner .StreamReader ;
42
44
import org .snakeyaml .engine .v2 .schema .CoreSchema ;
43
45
44
46
/**
@@ -126,8 +128,9 @@ public static OpenTelemetrySdk create(
126
128
/**
127
129
* Parse the {@code configuration} YAML and return the {@link OpenTelemetryConfigurationModel}.
128
130
*
129
- * <p>Before parsing, environment variable substitution is performed as described in {@link
130
- * EnvSubstitutionConstructor}.
131
+ * <p>During parsing, environment variable substitution is performed as defined in the <a
132
+ * href="https://opentelemetry.io/docs/specs/otel/configuration/data-model/#environment-variable-substitution">
133
+ * OpenTelemetry Configuration Data Model specification</a>.
131
134
*
132
135
* @throws DeclarativeConfigException if unable to parse
133
136
*/
@@ -149,7 +152,7 @@ static OpenTelemetryConfigurationModel parse(
149
152
// Visible for testing
150
153
static Object loadYaml (InputStream inputStream , Map <String , String > environmentVariables ) {
151
154
LoadSettings settings = LoadSettings .builder ().setSchema (new CoreSchema ()).build ();
152
- Load yaml = new Load (settings , new EnvSubstitutionConstructor ( settings , environmentVariables ) );
155
+ Load yaml = new EnvLoad (settings , environmentVariables );
153
156
return yaml .loadFromInputStream (inputStream );
154
157
}
155
158
@@ -241,89 +244,111 @@ static <M, R> R createAndMaybeCleanup(Factory<M, R> factory, SpiHelper spiHelper
241
244
}
242
245
}
243
246
247
+ private static final class EnvLoad extends Load {
248
+
249
+ private final LoadSettings settings ;
250
+ private final Map <String , String > environmentVariables ;
251
+
252
+ public EnvLoad (LoadSettings settings , Map <String , String > environmentVariables ) {
253
+ super (settings );
254
+ this .settings = settings ;
255
+ this .environmentVariables = environmentVariables ;
256
+ }
257
+
258
+ @ Override
259
+ public Object loadFromInputStream (InputStream yamlStream ) {
260
+ Objects .requireNonNull (yamlStream , "InputStream cannot be null" );
261
+ return loadOne (
262
+ new EnvComposer (
263
+ settings ,
264
+ new ParserImpl (
265
+ settings , new StreamReader (settings , new YamlUnicodeReader (yamlStream ))),
266
+ environmentVariables ));
267
+ }
268
+ }
269
+
244
270
/**
245
- * {@link StandardConstructor} which substitutes environment variables.
271
+ * A YAML Composer that performs environment variable substitution according to the <a
272
+ * href="https://opentelemetry.io/docs/specs/otel/configuration/data-model/#environment-variable-substitution">
273
+ * OpenTelemetry Configuration Data Model specification</a>.
274
+ *
275
+ * <p>This composer supports:
246
276
*
247
- * <p>Environment variables follow the syntax {@code ${VARIABLE}}, where {@code VARIABLE} is an
248
- * environment variable matching the regular expression {@code [a-zA-Z_]+[a-zA-Z0-9_]*}.
277
+ * <ul>
278
+ * <li>Environment variable references: {@code ${ENV_VAR}} or {@code ${env:ENV_VAR}}
279
+ * <li>Default values: {@code ${ENV_VAR:-default_value}}
280
+ * <li>Escape sequences: {@code $$} is replaced with a single {@code $}
281
+ * </ul>
249
282
*
250
- * <p>Environment variable substitution only takes place on scalar values of maps. References to
251
- * environment variables in keys or sets are ignored.
283
+ * <p>Environment variable substitution only applies to scalar values. Mapping keys are not
284
+ * candidates for substitution. Referenced environment variables that are undefined, null, or
285
+ * empty are replaced with empty values unless a default value is provided.
252
286
*
253
- * <p>If a referenced environment variable is not defined, it is replaced with {@code ""}.
287
+ * <p>The {@code $} character serves as an escape sequence where {@code $$} in the input is
288
+ * translated to a single {@code $} in the output. This prevents environment variable substitution
289
+ * for the escaped content.
254
290
*/
255
- private static final class EnvSubstitutionConstructor extends StandardConstructor {
291
+ private static final class EnvComposer extends Composer {
256
292
257
- // Load is not thread safe but this instance is always used on the same thread
258
293
private final Load load ;
259
294
private final Map <String , String > environmentVariables ;
295
+ private final ScalarResolver scalarResolver ;
296
+
297
+ private static final String ESCAPE_SEQUENCE = "$$" ;
298
+ private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE .length ();
299
+ private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$' ;
260
300
261
- private EnvSubstitutionConstructor (
262
- LoadSettings loadSettings , Map <String , String > environmentVariables ) {
263
- super (loadSettings );
264
- load = new Load (loadSettings );
301
+ public EnvComposer (
302
+ LoadSettings settings , ParserImpl parser , Map <String , String > environmentVariables ) {
303
+ super (settings , parser );
304
+ this . load = new Load (settings );
265
305
this .environmentVariables = environmentVariables ;
306
+ this .scalarResolver = settings .getSchema ().getScalarResolver ();
266
307
}
267
308
268
- /**
269
- * Implementation is same as {@link
270
- * org.snakeyaml.engine.v2.constructor.BaseConstructor#constructMapping(MappingNode)} except we
271
- * override the resolution of values with our custom {@link #constructValueObject(Node)}, which
272
- * performs environment variable substitution.
273
- */
274
309
@ Override
275
- @ SuppressWarnings ({"ReturnValueIgnored" , "CatchingUnchecked" })
276
- protected Map <Object , Object > constructMapping (MappingNode node ) {
277
- Map <Object , Object > mapping = settings .getDefaultMap ().apply (node .getValue ().size ());
278
- List <NodeTuple > nodeValue = node .getValue ();
279
- for (NodeTuple tuple : nodeValue ) {
280
- Node keyNode = tuple .getKeyNode ();
281
- Object key = constructObject (keyNode );
282
- if (key != null ) {
283
- try {
284
- key .hashCode (); // check circular dependencies
285
- } catch (Exception e ) {
286
- throw new ConstructorException (
287
- "while constructing a mapping" ,
288
- node .getStartMark (),
289
- "found unacceptable key " + key ,
290
- tuple .getKeyNode ().getStartMark (),
291
- e );
292
- }
293
- }
294
- Node valueNode = tuple .getValueNode ();
295
- Object value = constructValueObject (valueNode );
296
- if (keyNode .isRecursive ()) {
297
- if (settings .getAllowRecursiveKeys ()) {
298
- postponeMapFilling (mapping , key , value );
299
- } else {
300
- throw new YamlEngineException (
301
- "Recursive key for mapping is detected but it is not configured to be allowed." );
302
- }
303
- } else {
304
- mapping .put (key , value );
305
- }
310
+ protected Node composeValueNode (MappingNode node ) {
311
+ Node itemValue = super .composeValueNode (node );
312
+ if (!(itemValue instanceof ScalarNode )) {
313
+ // Only apply environment variable substitution to ScalarNodes
314
+ return itemValue ;
306
315
}
316
+ ScalarNode scalarNode = (ScalarNode ) itemValue ;
317
+ String envSubstitution = envSubstitution (scalarNode .getValue ());
307
318
308
- return mapping ;
309
- }
319
+ // If the environment variable substitution does not change the value, do not modify the node
320
+ if (envSubstitution .equals (scalarNode .getValue ())) {
321
+ return itemValue ;
322
+ }
310
323
311
- private static final String ESCAPE_SEQUENCE = "$$" ;
312
- private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE . length ();
313
- private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$' ;
324
+ Object envSubstitutionObj = load . loadFromString ( envSubstitution ) ;
325
+ Tag tag = itemValue . getTag ();
326
+ ScalarStyle scalarStyle = scalarNode . getScalarStyle () ;
314
327
315
- private Object constructValueObject (Node node ) {
316
- Object value = constructObject (node );
317
- if (!(node instanceof ScalarNode )) {
318
- return value ;
319
- }
320
- if (!(value instanceof String )) {
321
- return value ;
328
+ Tag resolvedTag =
329
+ envSubstitutionObj == null
330
+ ? Tag .NULL
331
+ : scalarResolver .resolve (envSubstitutionObj .toString (), true );
332
+
333
+ // Only non-quoted substituted scalars can have their tag changed
334
+ if (!itemValue .getTag ().equals (resolvedTag )) {
335
+ if (!scalarStyle .equals (ScalarStyle .SINGLE_QUOTED )
336
+ && !scalarStyle .equals (ScalarStyle .DOUBLE_QUOTED )) {
337
+ tag = resolvedTag ;
338
+ }
322
339
}
323
340
324
- String val = (String ) value ;
325
- ScalarStyle scalarStyle = ((ScalarNode ) node ).getScalarStyle ();
341
+ boolean resolved = true ;
342
+ return new ScalarNode (
343
+ tag ,
344
+ resolved ,
345
+ envSubstitution ,
346
+ scalarStyle ,
347
+ itemValue .getStartMark (),
348
+ itemValue .getEndMark ());
349
+ }
326
350
351
+ private String envSubstitution (String val ) {
327
352
// Iterate through val left to right, search for escape sequence "$$"
328
353
// For the substring of val between the last escape sequence and the next found, perform
329
354
// environment variable substitution
@@ -346,13 +371,7 @@ private Object constructValueObject(Node node) {
346
371
}
347
372
}
348
373
349
- // If the value was double quoted, retain the double quotes so we don't change a value
350
- // intended to be a string to a different type after environment variable substitution
351
- if (scalarStyle == ScalarStyle .DOUBLE_QUOTED ) {
352
- newVal .insert (0 , "\" " );
353
- newVal .append ("\" " );
354
- }
355
- return load .loadFromString (newVal .toString ());
374
+ return newVal .toString ();
356
375
}
357
376
358
377
private StringBuilder envVarSubstitution (
0 commit comments