2424import java .util .Collections ;
2525import java .util .List ;
2626import java .util .Map ;
27+ import java .util .Objects ;
2728import java .util .logging .Logger ;
2829import java .util .regex .MatchResult ;
2930import java .util .regex .Matcher ;
3031import java .util .regex .Pattern ;
3132import javax .annotation .Nullable ;
3233import org .snakeyaml .engine .v2 .api .Load ;
3334import org .snakeyaml .engine .v2 .api .LoadSettings ;
35+ import org .snakeyaml .engine .v2 .api .YamlUnicodeReader ;
3436import 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 ;
37+ import org .snakeyaml .engine .v2 .composer .Composer ;
3838import org .snakeyaml .engine .v2 .nodes .MappingNode ;
3939import org .snakeyaml .engine .v2 .nodes .Node ;
4040import org .snakeyaml .engine .v2 .nodes .NodeTuple ;
4141import org .snakeyaml .engine .v2 .nodes .ScalarNode ;
42+ import org .snakeyaml .engine .v2 .nodes .Tag ;
43+ import org .snakeyaml .engine .v2 .parser .ParserImpl ;
44+ import org .snakeyaml .engine .v2 .resolver .ScalarResolver ;
45+ import org .snakeyaml .engine .v2 .scanner .StreamReader ;
4246import org .snakeyaml .engine .v2 .schema .CoreSchema ;
4347
4448/**
@@ -126,8 +130,9 @@ public static OpenTelemetrySdk create(
126130 /**
127131 * Parse the {@code configuration} YAML and return the {@link OpenTelemetryConfigurationModel}.
128132 *
129- * <p>Before parsing, environment variable substitution is performed as described in {@link
130- * EnvSubstitutionConstructor}.
133+ * <p>During parsing, environment variable substitution is performed as defined in the
134+ * <a href="https://opentelemetry.io/docs/specs/otel/configuration/data-model/#environment-variable-substitution">
135+ * OpenTelemetry Configuration Data Model specification</a>.
131136 *
132137 * @throws DeclarativeConfigException if unable to parse
133138 */
@@ -149,7 +154,7 @@ static OpenTelemetryConfigurationModel parse(
149154 // Visible for testing
150155 static Object loadYaml (InputStream inputStream , Map <String , String > environmentVariables ) {
151156 LoadSettings settings = LoadSettings .builder ().setSchema (new CoreSchema ()).build ();
152- Load yaml = new Load (settings , new EnvSubstitutionConstructor ( settings , environmentVariables ) );
157+ Load yaml = new EnvLoad (settings , environmentVariables );
153158 return yaml .loadFromInputStream (inputStream );
154159 }
155160
@@ -241,89 +246,102 @@ static <M, R> R createAndMaybeCleanup(Factory<M, R> factory, SpiHelper spiHelper
241246 }
242247 }
243248
249+ private static final class EnvLoad extends Load {
250+
251+ private final LoadSettings settings ;
252+ private final Map <String , String > environmentVariables ;
253+
254+ public EnvLoad (LoadSettings settings , Map <String , String > environmentVariables ) {
255+ super (settings );
256+ this .settings = settings ;
257+ this .environmentVariables = environmentVariables ;
258+ }
259+
260+ @ Override
261+ public Object loadFromInputStream (InputStream yamlStream ) {
262+ Objects .requireNonNull (yamlStream , "InputStream cannot be null" );
263+ return loadOne (new EnvComposer (settings ,
264+ new ParserImpl (settings , new StreamReader (settings , new YamlUnicodeReader (yamlStream ))),
265+ environmentVariables ));
266+ }
267+ }
268+
244269 /**
245- * {@link StandardConstructor} which substitutes environment variables.
270+ * A YAML Composer that performs environment variable substitution according to the
271+ * <a href="https://opentelemetry.io/docs/specs/otel/configuration/data-model/#environment-variable-substitution">
272+ * OpenTelemetry Configuration Data Model specification</a>.
246273 *
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_]*}.
274+ * <p>This composer supports:
275+ * <ul>
276+ * <li>Environment variable references: {@code ${ENV_VAR}} or {@code ${env:ENV_VAR}}
277+ * <li>Default values: {@code ${ENV_VAR:-default_value}}
278+ * <li>Escape sequences: {@code $$} is replaced with a single {@code $}
279+ * </ul>
249280 *
250- * <p>Environment variable substitution only takes place on scalar values of maps. References to
251- * environment variables in keys or sets are ignored.
281+ * <p>Environment variable substitution only applies to scalar values. Mapping keys are not
282+ * candidates for substitution. Referenced environment variables that are undefined, null, or
283+ * empty are replaced with empty values unless a default value is provided.
252284 *
253- * <p>If a referenced environment variable is not defined, it is replaced with {@code ""}.
285+ * <p>The {@code $} character serves as an escape sequence where {@code $$} in the input is
286+ * translated to a single {@code $} in the output. This prevents environment variable
287+ * substitution for the escaped content.
254288 */
255- private static final class EnvSubstitutionConstructor extends StandardConstructor {
289+ private static final class EnvComposer extends Composer {
256290
257- // Load is not thread safe but this instance is always used on the same thread
258291 private final Load load ;
259292 private final Map <String , String > environmentVariables ;
293+ private final ScalarResolver scalarResolver ;
294+
295+ private static final String ESCAPE_SEQUENCE = "$$" ;
296+ private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE .length ();
297+ private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$' ;
260298
261- private EnvSubstitutionConstructor (
262- LoadSettings loadSettings , Map <String , String > environmentVariables ) {
263- super (loadSettings );
264- load = new Load (loadSettings );
299+ public EnvComposer ( LoadSettings settings , ParserImpl parser ,
300+ Map <String , String > environmentVariables ) {
301+ super (settings , parser );
302+ this . load = new Load (settings );
265303 this .environmentVariables = environmentVariables ;
304+ this .scalarResolver = settings .getSchema ().getScalarResolver ();
266305 }
267306
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- */
274307 @ 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- }
308+ protected void composeMappingChildren (List <NodeTuple > children , MappingNode node ) {
309+ Node itemKey = composeKeyNode (node );
310+ Node itemValue = composeValueNode (node );
311+ // Only apply environment variable substitution to ScalarNodes
312+ if (!(itemValue instanceof ScalarNode )) {
313+ children .add (new NodeTuple (itemKey , itemValue ));
314+ return ;
306315 }
307316
308- return mapping ;
309- }
317+ String envSubstitution = envSubstitution (((ScalarNode ) itemValue ).getValue ());
310318
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 = '$' ;
314-
315- private Object constructValueObject (Node node ) {
316- Object value = constructObject (node );
317- if (!(node instanceof ScalarNode )) {
318- return value ;
319+ // If the environment variable substitution does not change the value, do not modify the node
320+ if (envSubstitution .equals (((ScalarNode ) itemValue ).getValue ())) {
321+ children .add (new NodeTuple (itemKey , itemValue ));
322+ return ;
319323 }
320- if (!(value instanceof String )) {
321- return value ;
324+
325+ Object envSubstitutionObj = load .loadFromString (envSubstitution );
326+ Tag tag = itemValue .getTag ();
327+ ScalarStyle scalarStyle = ((ScalarNode ) itemValue ).getScalarStyle ();
328+
329+ Tag resolvedTag = envSubstitutionObj == null ? Tag .NULL
330+ : scalarResolver .resolve (envSubstitutionObj .toString (), true );
331+
332+ if ((!scalarStyle .equals (ScalarStyle .SINGLE_QUOTED ) && !scalarStyle .equals (
333+ ScalarStyle .DOUBLE_QUOTED )) || itemValue .getTag ().equals (resolvedTag )) {
334+ tag = resolvedTag ;
322335 }
323336
324- String val = (String ) value ;
325- ScalarStyle scalarStyle = ((ScalarNode ) node ).getScalarStyle ();
337+ boolean resolved = true ;
338+ itemValue = new ScalarNode (tag , resolved , envSubstitution ,
339+ scalarStyle , itemValue .getStartMark (),
340+ itemValue .getEndMark ());
341+ children .add (new NodeTuple (itemKey , itemValue ));
342+ }
326343
344+ private String envSubstitution (String val ) {
327345 // Iterate through val left to right, search for escape sequence "$$"
328346 // For the substring of val between the last escape sequence and the next found, perform
329347 // environment variable substitution
@@ -346,13 +364,7 @@ private Object constructValueObject(Node node) {
346364 }
347365 }
348366
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 ());
367+ return newVal .toString ();
356368 }
357369
358370 private StringBuilder envVarSubstitution (
0 commit comments