diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java
index 6e67bd0bdb4..6c89702f234 100644
--- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java
+++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java
@@ -57,8 +57,9 @@
public final class DeclarativeConfiguration {
private static final Logger logger = Logger.getLogger(DeclarativeConfiguration.class.getName());
+ // Matches ${VAR_NAME}, ${env:VAR_NAME}, or ${sys:property.name} with optional :-default
private static final Pattern ENV_VARIABLE_REFERENCE =
- Pattern.compile("\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)(:-([^\n}]*))?}");
+ Pattern.compile("\\$\\{(?:(env|sys):)?([a-zA-Z_][a-zA-Z0-9_.]*)(?::-([^\\n}]*))?}");
private static final ComponentLoader DEFAULT_COMPONENT_LOADER =
ComponentLoader.forClassLoader(DeclarativeConfigProperties.class.getClassLoader());
@@ -140,7 +141,8 @@ private static ExtendedOpenTelemetrySdk create(
/**
* Parse the {@code configuration} YAML and return the {@link OpenTelemetryConfigurationModel}.
*
- *
During parsing, environment variable substitution is performed as defined in the During parsing, environment variable and system property substitution is performed as
+ * defined in the
* OpenTelemetry Configuration Data Model specification .
*
@@ -148,7 +150,7 @@ private static ExtendedOpenTelemetrySdk create(
*/
public static OpenTelemetryConfigurationModel parse(InputStream configuration) {
try {
- return parse(configuration, System.getenv());
+ return parse(configuration, System.getenv(), System.getProperties());
} catch (RuntimeException e) {
throw new DeclarativeConfigException("Unable to parse configuration input stream", e);
}
@@ -156,15 +158,20 @@ public static OpenTelemetryConfigurationModel parse(InputStream configuration) {
// Visible for testing
static OpenTelemetryConfigurationModel parse(
- InputStream configuration, Map environmentVariables) {
- Object yamlObj = loadYaml(configuration, environmentVariables);
+ InputStream configuration,
+ Map environmentVariables,
+ Map systemProperties) {
+ Object yamlObj = loadYaml(configuration, environmentVariables, systemProperties);
return MAPPER.convertValue(yamlObj, OpenTelemetryConfigurationModel.class);
}
// Visible for testing
- static Object loadYaml(InputStream inputStream, Map environmentVariables) {
+ static Object loadYaml(
+ InputStream inputStream,
+ Map environmentVariables,
+ Map systemProperties) {
LoadSettings settings = LoadSettings.builder().setSchema(new CoreSchema()).build();
- Load yaml = new EnvLoad(settings, environmentVariables);
+ Load yaml = new EnvLoad(settings, environmentVariables, systemProperties);
return yaml.loadFromInputStream(inputStream);
}
@@ -185,7 +192,7 @@ public static DeclarativeConfigProperties toConfigProperties(Object model) {
* @return a generic {@link DeclarativeConfigProperties} representation of the model
*/
public static DeclarativeConfigProperties toConfigProperties(InputStream configuration) {
- Object yamlObj = loadYaml(configuration, System.getenv());
+ Object yamlObj = loadYaml(configuration, System.getenv(), System.getProperties());
return toConfigProperties(yamlObj, DEFAULT_COMPONENT_LOADER);
}
@@ -256,11 +263,16 @@ private static final class EnvLoad extends Load {
private final LoadSettings settings;
private final Map environmentVariables;
+ private final Map systemProperties;
- private EnvLoad(LoadSettings settings, Map environmentVariables) {
+ private EnvLoad(
+ LoadSettings settings,
+ Map environmentVariables,
+ Map systemProperties) {
super(settings);
this.settings = settings;
this.environmentVariables = environmentVariables;
+ this.systemProperties = systemProperties;
}
@Override
@@ -271,12 +283,14 @@ public Object loadFromInputStream(InputStream yamlStream) {
settings,
new ParserImpl(
settings, new StreamReader(settings, new YamlUnicodeReader(yamlStream))),
- environmentVariables));
+ environmentVariables,
+ systemProperties));
}
}
/**
- * A YAML Composer that performs environment variable substitution according to the
* OpenTelemetry Configuration Data Model specification .
*
@@ -284,22 +298,24 @@ settings, new StreamReader(settings, new YamlUnicodeReader(yamlStream))),
*
*
* Environment variable references: {@code ${ENV_VAR}} or {@code ${env:ENV_VAR}}
+ * System property references: {@code ${sys:property.name}}
* Default values: {@code ${ENV_VAR:-default_value}}
* Escape sequences: {@code $$} is replaced with a single {@code $}
*
*
- * Environment variable substitution only applies to scalar values. Mapping keys are not
- * candidates for substitution. Referenced environment variables that are undefined, null, or
- * empty are replaced with empty values unless a default value is provided.
+ *
Substitution only applies to scalar values. Mapping keys are not candidates for
+ * substitution. Referenced variables that are undefined, null, or empty are replaced with empty
+ * values unless a default value is provided.
*
*
The {@code $} character serves as an escape sequence where {@code $$} in the input is
- * translated to a single {@code $} in the output. This prevents environment variable substitution
- * for the escaped content.
+ * translated to a single {@code $} in the output. This prevents substitution for the escaped
+ * content.
*/
private static final class EnvComposer extends Composer {
private final Load load;
private final Map environmentVariables;
+ private final Map systemProperties;
private final ScalarResolver scalarResolver;
private static final String ESCAPE_SEQUENCE = "$$";
@@ -307,10 +323,14 @@ private static final class EnvComposer extends Composer {
private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$';
private EnvComposer(
- LoadSettings settings, ParserImpl parser, Map environmentVariables) {
+ LoadSettings settings,
+ ParserImpl parser,
+ Map environmentVariables,
+ Map systemProperties) {
super(settings, parser);
this.load = new Load(settings);
this.environmentVariables = environmentVariables;
+ this.systemProperties = systemProperties;
this.scalarResolver = settings.getSchema().getScalarResolver();
}
@@ -397,12 +417,23 @@ private StringBuilder envVarSubstitution(
int offset = 0;
do {
MatchResult matchResult = matcher.toMatchResult();
- String envVarKey = matcher.group(1);
+ String prefix = matcher.group(1); // "env", "sys", or null
+ String key = matcher.group(2); // variable/property name
String defaultValue = matcher.group(3);
if (defaultValue == null) {
defaultValue = "";
}
- String replacement = environmentVariables.getOrDefault(envVarKey, defaultValue);
+
+ String replacement;
+ if ("sys".equals(prefix)) {
+ // System property substitution
+ Object sysProp = systemProperties.get(key);
+ replacement = sysProp != null ? sysProp.toString() : defaultValue;
+ } else {
+ // Environment variable substitution (default or explicit "env:" prefix)
+ replacement = environmentVariables.getOrDefault(key, defaultValue);
+ }
+
newVal.append(val, offset, matchResult.start()).append(replacement);
offset = matchResult.end();
} while (matcher.find());
diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationParseTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationParseTest.java
index ffc1322bcb1..6889f9b27a4 100644
--- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationParseTest.java
+++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationParseTest.java
@@ -111,6 +111,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
+import java.util.stream.Stream;
import javax.annotation.Nullable;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -999,6 +1000,7 @@ void coreSchemaValues(String rawYaml, Object expectedYamlResult) {
Object yaml =
DeclarativeConfiguration.loadYaml(
new ByteArrayInputStream(rawYaml.getBytes(StandardCharsets.UTF_8)),
+ Collections.emptyMap(),
Collections.emptyMap());
assertThat(yaml).isEqualTo(expectedYamlResult);
}
@@ -1029,7 +1031,8 @@ void envSubstituteAndLoadYaml(String rawYaml, Object expectedYamlResult) {
Object yaml =
DeclarativeConfiguration.loadYaml(
new ByteArrayInputStream(rawYaml.getBytes(StandardCharsets.UTF_8)),
- environmentVariables);
+ environmentVariables,
+ Collections.emptyMap());
assertThat(yaml).isEqualTo(expectedYamlResult);
}
@@ -1131,6 +1134,64 @@ private static Map mapOf(Map.Entry... entries) {
return result;
}
+ @ParameterizedTest
+ @MethodSource("sysPropertySubstitutionArgs")
+ void sysPropertySubstituteAndLoadYaml(String rawYaml, Object expectedYamlResult) {
+ Map systemProperties = new HashMap<>();
+ systemProperties.put("foo.bar", "BAR");
+ systemProperties.put("str.1", "value1");
+ systemProperties.put("str.2", "value2");
+ systemProperties.put("value.with.escape", "value$$");
+ systemProperties.put("empty.str", "");
+ systemProperties.put("bool.prop", "true");
+ systemProperties.put("int.prop", "1");
+ systemProperties.put("float.prop", "1.1");
+ systemProperties.put("hex.prop", "0xdeadbeef");
+
+ Object yaml =
+ DeclarativeConfiguration.loadYaml(
+ new ByteArrayInputStream(rawYaml.getBytes(StandardCharsets.UTF_8)),
+ Collections.emptyMap(),
+ systemProperties);
+ assertThat(yaml).isEqualTo(expectedYamlResult);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static Stream sysPropertySubstitutionArgs() {
+ return java.util.stream.Stream.of(
+ // Simple cases with sys: prefix
+ Arguments.of("key1: ${sys:str.1}\n", mapOf(entry("key1", "value1"))),
+ Arguments.of("key1: ${sys:bool.prop}\n", mapOf(entry("key1", true))),
+ Arguments.of("key1: ${sys:int.prop}\n", mapOf(entry("key1", 1))),
+ Arguments.of("key1: ${sys:float.prop}\n", mapOf(entry("key1", 1.1))),
+ Arguments.of("key1: ${sys:hex.prop}\n", mapOf(entry("key1", 3735928559L))),
+ // Default values
+ Arguments.of("key1: ${sys:not.set:-value1}\n", mapOf(entry("key1", "value1"))),
+ Arguments.of("key1: ${sys:not.set:-true}\n", mapOf(entry("key1", true))),
+ Arguments.of("key1: ${sys:not.set:-1}\n", mapOf(entry("key1", 1))),
+ // Multiple property references
+ Arguments.of("key1: ${sys:str.1}${sys:str.2}\n", mapOf(entry("key1", "value1value2"))),
+ Arguments.of("key1: ${sys:str.1} ${sys:str.2}\n", mapOf(entry("key1", "value1 value2"))),
+ Arguments.of(
+ "key1: ${sys:str.1} ${sys:not.set:-default} ${sys:str.2}\n",
+ mapOf(entry("key1", "value1 default value2"))),
+ // Undefined / empty system property
+ Arguments.of("key1: ${sys:empty.str}\n", mapOf(entry("key1", null))),
+ Arguments.of("key1: ${sys:str.3}\n", mapOf(entry("key1", null))),
+ Arguments.of("key1: ${sys:str.1} ${sys:str.3}\n", mapOf(entry("key1", "value1 "))),
+ // Quoted system properties
+ Arguments.of("key1: \"${sys:hex.prop}\"\n", mapOf(entry("key1", "0xdeadbeef"))),
+ Arguments.of("key1: \"${sys:str.1}\"\n", mapOf(entry("key1", "value1"))),
+ Arguments.of("key1: '${sys:str.1}'\n", mapOf(entry("key1", "value1"))),
+ // Escaped
+ Arguments.of("key1: ${sys:foo.bar}\n", mapOf(entry("key1", "BAR"))),
+ Arguments.of("key1: $${sys:foo.bar}\n", mapOf(entry("key1", "${sys:foo.bar}"))),
+ Arguments.of("key1: $$${sys:foo.bar}\n", mapOf(entry("key1", "$BAR"))),
+ Arguments.of("key1: $$$${sys:foo.bar}\n", mapOf(entry("key1", "$${sys:foo.bar}"))),
+ // Mixed env and sys
+ Arguments.of("key1: ${sys:value.with.escape}\n", mapOf(entry("key1", "value$$"))));
+ }
+
@Test
void read_WithEnvironmentVariables() {
String yaml =
@@ -1149,7 +1210,9 @@ void read_WithEnvironmentVariables() {
envVars.put("OTEL_EXPORTER_OTLP_ENDPOINT", "http://collector:4317");
OpenTelemetryConfigurationModel model =
DeclarativeConfiguration.parse(
- new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)), envVars);
+ new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)),
+ envVars,
+ Collections.emptyMap());
assertThat(model)
.isEqualTo(
new OpenTelemetryConfigurationModel()
@@ -1175,4 +1238,89 @@ void read_WithEnvironmentVariables() {
.withOtlpHttp(
new OtlpHttpExporterModel())))))));
}
+
+ @Test
+ void read_WithSystemProperties() {
+ String yaml =
+ "file_format: \"1.0-rc.1\"\n"
+ + "tracer_provider:\n"
+ + " processors:\n"
+ + " - batch:\n"
+ + " exporter:\n"
+ + " otlp_http:\n"
+ + " endpoint: ${sys:otel.exporter.otlp.endpoint}\n"
+ + " - batch:\n"
+ + " exporter:\n"
+ + " otlp_http:\n"
+ + " endpoint: ${sys:unset.sys.prop}\n";
+ Map sysProps = new HashMap<>();
+ sysProps.put("otel.exporter.otlp.endpoint", "http://collector:4318");
+ OpenTelemetryConfigurationModel model =
+ DeclarativeConfiguration.parse(
+ new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)),
+ Collections.emptyMap(),
+ sysProps);
+ assertThat(model)
+ .isEqualTo(
+ new OpenTelemetryConfigurationModel()
+ .withFileFormat("1.0-rc.1")
+ .withTracerProvider(
+ new TracerProviderModel()
+ .withProcessors(
+ Arrays.asList(
+ new SpanProcessorModel()
+ .withBatch(
+ new BatchSpanProcessorModel()
+ .withExporter(
+ new SpanExporterModel()
+ .withOtlpHttp(
+ new OtlpHttpExporterModel()
+ .withEndpoint(
+ "http://collector:4318")))),
+ new SpanProcessorModel()
+ .withBatch(
+ new BatchSpanProcessorModel()
+ .withExporter(
+ new SpanExporterModel()
+ .withOtlpHttp(
+ new OtlpHttpExporterModel())))))));
+ }
+
+ @Test
+ void read_WithMixedEnvVarsAndSystemProperties() {
+ String yaml =
+ "file_format: \"1.0-rc.1\"\n"
+ + "resource:\n"
+ + " attributes:\n"
+ + " - name: service.name\n"
+ + " value: ${SERVICE_NAME}\n"
+ + " - name: service.version\n"
+ + " value: ${sys:app.version}\n"
+ + " - name: deployment.environment\n"
+ + " value: ${env:DEPLOYMENT_ENV:-production}\n";
+ Map envVars = new HashMap<>();
+ envVars.put("SERVICE_NAME", "my-service");
+ Map sysProps = new HashMap<>();
+ sysProps.put("app.version", "1.2.3");
+ OpenTelemetryConfigurationModel model =
+ DeclarativeConfiguration.parse(
+ new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)), envVars, sysProps);
+ assertThat(model)
+ .isEqualTo(
+ new OpenTelemetryConfigurationModel()
+ .withFileFormat("1.0-rc.1")
+ .withResource(
+ new ResourceModel()
+ .withAttributes(
+ Arrays.asList(
+ new AttributeNameValueModel()
+ .withName("service.name")
+ .withValue("my-service"),
+ new AttributeNameValueModel()
+ .withName("service.version")
+ .withValue("1.2.3"),
+ new AttributeNameValueModel()
+ .withName("deployment.environment")
+ .withValue("production")))));
+ }
}