From 188acb41cc5b9ed3885b6d07a603c7b1780739d5 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 21 Nov 2025 19:16:57 +0100 Subject: [PATCH 1/3] fix env var override for spring starter declarative config --- .../build.gradle.kts | 1 + .../autoconfigure/EmbeddedConfigFile.java | 19 +++++++++++++++++++ .../autoconfigure/DeclarativeConfigTest.java | 19 +++++++++++++++++++ .../resources/application.yaml | 7 +++++++ 4 files changed, 46 insertions(+) diff --git a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts index 5752ad25d42f..8ce5e4f8cfdb 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts +++ b/instrumentation/spring/spring-boot-autoconfigure/build.gradle.kts @@ -180,6 +180,7 @@ testing { dependencies { implementation(project()) implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-exporter-otlp") implementation("org.springframework.boot:spring-boot-starter-test:$springBootVersion") { exclude("org.junit.vintage", "junit-vintage-engine") } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java index ffbb11b6b9c6..b2222baf489f 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java @@ -11,6 +11,7 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; import java.util.ArrayList; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.regex.Matcher; @@ -62,6 +63,24 @@ private static Map extractSpringProperties(ConfigurableEnvironme if (Objects.equals(property, "")) { property = null; // spring returns empty string for yaml null } + if (propertyName.contains("[")) { + // fix override via env var or system property + // see + // https://docs.spring.io/spring-boot/reference/features/external-config.html#features.external-config.typesafe-configuration-properties.relaxed-binding + // For example, the configuration property my.service[0].other would use an + // environment variable named MY_SERVICE_0_OTHER. + String envVarName = + propertyName + .replace("[", "_") + .replace("]", "") + .replace(".", "_") + .toUpperCase(Locale.ROOT); + String envVarValue = environment.getProperty(envVarName); + if (envVarValue != null) { + property = envVarValue; + } + } + props.put(propertyName.substring("otel.".length()), property); } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java index d15468a17944..ee298ca07da6 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java @@ -131,4 +131,23 @@ void shouldNotLoadInstrumentationWhenExplicitlyDisabled() { "otel.instrumentation/development.java.spring_web.enabled=false") .run(context -> assertThat(context).doesNotHaveBean("otelRestTemplateBeanPostProcessor")); } + + @Test + void envVarOverride() { + this.contextRunner + // this is typically set via env var + // OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT + .withSystemProperties( + "OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT=http://custom:4318/v1/traces") + .run( + context -> + assertThat(context) + .getBean(OpenTelemetry.class) + .isNotNull() + .satisfies( + c -> + assertThat(c.toString()) + .contains( + "OtlpHttpSpanExporter{endpoint=http://custom:4318/v1/traces"))); + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml index 8f263bc8ff50..efa0b340ae82 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml @@ -2,6 +2,13 @@ otel: # "file_format" serves as opt-in to the new file format file_format: "1.0-rc.1" + tracer_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: http://localhost:4318/v1/traces # to test that we can use env vars in declarative config + # very lightweight test to make sure the declarative config is loaded # the full config is tested in smoke-tests-otel-starter/spring-boot-2/src/testDeclarativeConfig instrumentation/development: From 981cd4edb470163b40efda86e5588cd6d2b6d573 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sun, 23 Nov 2025 15:56:28 +0100 Subject: [PATCH 2/3] support otle style env vars --- .../autoconfigure/EmbeddedConfigFile.java | 85 ++++++++++++++++++- .../OpenTelemetryAutoConfiguration.java | 2 +- .../autoconfigure/DeclarativeConfigTest.java | 20 ++++- .../resources/application.yaml | 2 +- 4 files changed, 101 insertions(+), 8 deletions(-) diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java index b2222baf489f..f0247ff1cb23 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java @@ -14,8 +14,10 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.annotation.Nullable; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MutablePropertySources; @@ -30,6 +32,13 @@ class EmbeddedConfigFile { // which is not public private static final ObjectMapper MAPPER; + private static final Pattern ENV_VARIABLE_REFERENCE = + Pattern.compile("\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)(:-([^\n}]*))?}"); + + private static final String ESCAPE_SEQUENCE = "$$"; + private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE.length(); + private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$'; + static { MAPPER = new ObjectMapper() @@ -43,14 +52,18 @@ class EmbeddedConfigFile { MAPPER.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); } - private EmbeddedConfigFile() {} + private final ConfigurableEnvironment environment; + + EmbeddedConfigFile(ConfigurableEnvironment environment) { + this.environment = environment; + } - static OpenTelemetryConfigurationModel extractModel(ConfigurableEnvironment environment) { + OpenTelemetryConfigurationModel extractModel(ConfigurableEnvironment environment) { Map props = extractSpringProperties(environment); return convertToOpenTelemetryConfigurationModel(props); } - private static Map extractSpringProperties(ConfigurableEnvironment environment) { + private Map extractSpringProperties(ConfigurableEnvironment environment) { MutablePropertySources propertySources = environment.getPropertySources(); Map props = new HashMap<>(); @@ -80,7 +93,9 @@ private static Map extractSpringProperties(ConfigurableEnvironme property = envVarValue; } } - + if (property != null) { + property = envSubstitution(property); + } props.put(propertyName.substring("otel.".length()), property); } } @@ -168,4 +183,66 @@ static Map convertFlatPropsToNested(Map flatProp } return result; } + + /** + * Copy of envSubstitution + */ + private String envSubstitution(String val) { + // Iterate through val left to right, search for escape sequence "$$" + // For the substring of val between the last escape sequence and the next found, perform + // environment variable substitution + // Add the escape replacement character '$' in place of each escape sequence found + + int lastEscapeIndexEnd = 0; + StringBuilder newVal = null; + while (true) { + int escapeIndex = val.indexOf(ESCAPE_SEQUENCE, lastEscapeIndexEnd); + int substitutionEndIndex = escapeIndex == -1 ? val.length() : escapeIndex; + newVal = envVarSubstitution(newVal, val, lastEscapeIndexEnd, substitutionEndIndex); + if (escapeIndex == -1) { + break; + } else { + newVal.append(ESCAPE_SEQUENCE_REPLACEMENT); + } + lastEscapeIndexEnd = escapeIndex + ESCAPE_SEQUENCE_LENGTH; + if (lastEscapeIndexEnd >= val.length()) { + break; + } + } + + return newVal.toString(); + } + + private StringBuilder envVarSubstitution( + @Nullable StringBuilder newVal, String source, int startIndex, int endIndex) { + String val = source.substring(startIndex, endIndex); + Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val); + + if (!matcher.find()) { + return newVal == null ? new StringBuilder(val) : newVal.append(val); + } + + if (newVal == null) { + newVal = new StringBuilder(); + } + + int offset = 0; + do { + MatchResult matchResult = matcher.toMatchResult(); + String envVarKey = matcher.group(1); + String defaultValue = matcher.group(3); + if (defaultValue == null) { + defaultValue = ""; + } + String replacement = environment.getProperty(envVarKey, defaultValue); + newVal.append(val, offset, matchResult.start()).append(replacement); + offset = matchResult.end(); + } while (matcher.find()); + if (offset != val.length()) { + newVal.append(val, offset, val.length()); + } + + return newVal; + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java index 0be0df45ca60..ea28c773cc03 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java @@ -158,7 +158,7 @@ static class EmbeddedConfigFileConfig { @Bean public OpenTelemetryConfigurationModel openTelemetryConfigurationModel( ConfigurableEnvironment environment) throws IOException { - return EmbeddedConfigFile.extractModel(environment); + return new EmbeddedConfigFile(environment).extractModel(environment); } @Bean diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java index ee298ca07da6..7bb5b53f5832 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/java/io/opentelemetry/instrumentation/spring/autoconfigure/DeclarativeConfigTest.java @@ -133,10 +133,9 @@ void shouldNotLoadInstrumentationWhenExplicitlyDisabled() { } @Test - void envVarOverride() { + void envVarOverrideSpringStyle() { this.contextRunner // this is typically set via env var - // OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT .withSystemProperties( "OTEL_TRACER_PROVIDER_PROCESSORS_0_BATCH_EXPORTER_OTLP_HTTP_ENDPOINT=http://custom:4318/v1/traces") .run( @@ -150,4 +149,21 @@ void envVarOverride() { .contains( "OtlpHttpSpanExporter{endpoint=http://custom:4318/v1/traces"))); } + + @Test + void envVarOverrideOtelStyle() { + this.contextRunner + // this is typically set via env var + .withSystemProperties("OTEL_EXPORTER_OTLP_ENDPOINT=http://custom:4318") + .run( + context -> + assertThat(context) + .getBean(OpenTelemetry.class) + .isNotNull() + .satisfies( + c -> + assertThat(c.toString()) + .contains( + "OtlpHttpSpanExporter{endpoint=http://custom:4318/v1/traces"))); + } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml index efa0b340ae82..52e8573cba98 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml @@ -7,7 +7,7 @@ otel: - batch: exporter: otlp_http: - endpoint: http://localhost:4318/v1/traces # to test that we can use env vars in declarative config + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces # to test that we can use env vars in declarative config # very lightweight test to make sure the declarative config is loaded # the full config is tested in smoke-tests-otel-starter/spring-boot-2/src/testDeclarativeConfig From 322e4febeee1996f9976d4a21dfa00e2ad146996 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 24 Nov 2025 08:22:30 +0100 Subject: [PATCH 3/3] spring already has env var substitution --- .../autoconfigure/EmbeddedConfigFile.java | 85 +------------------ .../OpenTelemetryAutoConfiguration.java | 5 +- .../resources/application.yaml | 2 +- 3 files changed, 7 insertions(+), 85 deletions(-) diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java index f0247ff1cb23..b2222baf489f 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/EmbeddedConfigFile.java @@ -14,10 +14,8 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.regex.MatchResult; import java.util.regex.Matcher; import java.util.regex.Pattern; -import javax.annotation.Nullable; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.MutablePropertySources; @@ -32,13 +30,6 @@ class EmbeddedConfigFile { // which is not public private static final ObjectMapper MAPPER; - private static final Pattern ENV_VARIABLE_REFERENCE = - Pattern.compile("\\$\\{([a-zA-Z_][a-zA-Z0-9_]*)(:-([^\n}]*))?}"); - - private static final String ESCAPE_SEQUENCE = "$$"; - private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE.length(); - private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$'; - static { MAPPER = new ObjectMapper() @@ -52,18 +43,14 @@ class EmbeddedConfigFile { MAPPER.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); } - private final ConfigurableEnvironment environment; - - EmbeddedConfigFile(ConfigurableEnvironment environment) { - this.environment = environment; - } + private EmbeddedConfigFile() {} - OpenTelemetryConfigurationModel extractModel(ConfigurableEnvironment environment) { + static OpenTelemetryConfigurationModel extractModel(ConfigurableEnvironment environment) { Map props = extractSpringProperties(environment); return convertToOpenTelemetryConfigurationModel(props); } - private Map extractSpringProperties(ConfigurableEnvironment environment) { + private static Map extractSpringProperties(ConfigurableEnvironment environment) { MutablePropertySources propertySources = environment.getPropertySources(); Map props = new HashMap<>(); @@ -93,9 +80,7 @@ private Map extractSpringProperties(ConfigurableEnvironment envi property = envVarValue; } } - if (property != null) { - property = envSubstitution(property); - } + props.put(propertyName.substring("otel.".length()), property); } } @@ -183,66 +168,4 @@ static Map convertFlatPropsToNested(Map flatProp } return result; } - - /** - * Copy of envSubstitution - */ - private String envSubstitution(String val) { - // Iterate through val left to right, search for escape sequence "$$" - // For the substring of val between the last escape sequence and the next found, perform - // environment variable substitution - // Add the escape replacement character '$' in place of each escape sequence found - - int lastEscapeIndexEnd = 0; - StringBuilder newVal = null; - while (true) { - int escapeIndex = val.indexOf(ESCAPE_SEQUENCE, lastEscapeIndexEnd); - int substitutionEndIndex = escapeIndex == -1 ? val.length() : escapeIndex; - newVal = envVarSubstitution(newVal, val, lastEscapeIndexEnd, substitutionEndIndex); - if (escapeIndex == -1) { - break; - } else { - newVal.append(ESCAPE_SEQUENCE_REPLACEMENT); - } - lastEscapeIndexEnd = escapeIndex + ESCAPE_SEQUENCE_LENGTH; - if (lastEscapeIndexEnd >= val.length()) { - break; - } - } - - return newVal.toString(); - } - - private StringBuilder envVarSubstitution( - @Nullable StringBuilder newVal, String source, int startIndex, int endIndex) { - String val = source.substring(startIndex, endIndex); - Matcher matcher = ENV_VARIABLE_REFERENCE.matcher(val); - - if (!matcher.find()) { - return newVal == null ? new StringBuilder(val) : newVal.append(val); - } - - if (newVal == null) { - newVal = new StringBuilder(); - } - - int offset = 0; - do { - MatchResult matchResult = matcher.toMatchResult(); - String envVarKey = matcher.group(1); - String defaultValue = matcher.group(3); - if (defaultValue == null) { - defaultValue = ""; - } - String replacement = environment.getProperty(envVarKey, defaultValue); - newVal.append(val, offset, matchResult.start()).append(replacement); - offset = matchResult.end(); - } while (matcher.find()); - if (offset != val.length()) { - newVal.append(val, offset, val.length()); - } - - return newVal; - } } diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java index ea28c773cc03..bbb40a59a2cb 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java +++ b/instrumentation/spring/spring-boot-autoconfigure/src/main/java/io/opentelemetry/instrumentation/spring/autoconfigure/OpenTelemetryAutoConfiguration.java @@ -39,7 +39,6 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizerProvider; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; -import java.io.IOException; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -157,8 +156,8 @@ static class EmbeddedConfigFileConfig { @Bean public OpenTelemetryConfigurationModel openTelemetryConfigurationModel( - ConfigurableEnvironment environment) throws IOException { - return new EmbeddedConfigFile(environment).extractModel(environment); + ConfigurableEnvironment environment) { + return EmbeddedConfigFile.extractModel(environment); } @Bean diff --git a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml index 52e8573cba98..455ea6b8d568 100644 --- a/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml +++ b/instrumentation/spring/spring-boot-autoconfigure/src/testDeclarativeConfig/resources/application.yaml @@ -7,7 +7,7 @@ otel: - batch: exporter: otlp_http: - endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://localhost:4318}/v1/traces # to test that we can use env vars in declarative config + endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:http://localhost:4318}/v1/traces # to test that we can use env vars in declarative config # very lightweight test to make sure the declarative config is loaded # the full config is tested in smoke-tests-otel-starter/spring-boot-2/src/testDeclarativeConfig