From 1b413005a67fac23a682176e837e75a57073088f Mon Sep 17 00:00:00 2001 From: Ashish Thakur Date: Wed, 28 Oct 2026 17:26:09 +0530 Subject: [PATCH 1/3] feat: Make declarative config parsing and mapping pluggable Add parser and mapper SPIs, and move the Jackson 2 mapper into an optional adapter module. Signed-off-by: Ashish Thakur --- .../declarative-config-jackson/README.md | 8 + .../build.gradle.kts | 17 ++ .../gradle.properties | 1 + ...JacksonDeclarativeConfigurationMapper.java | 35 +++ .../jackson/internal/JacksonModelMapper.java} | 19 +- ...ativeconfig.DeclarativeConfigurationMapper | 1 + sdk-extensions/declarative-config/README.md | 1 + .../declarative-config/build.gradle.kts | 12 +- .../DeclarativeConfiguration.java | 241 ++---------------- .../DeclarativeConfigurationMapper.java | 41 +++ .../DeclarativeConfigurationYamlParser.java | 38 +++ .../YamlDeclarativeConfigProperties.java | 5 +- .../DeclarativeConfigProviderLoader.java | 42 +++ .../DeclarativeConfigurationMapperUtil.java | 68 +++++ ...akeYamlDeclarativeConfigurationParser.java | 219 ++++++++++++++++ .../model/internal/ExtensionPropertyUtil.java | 11 +- ...econfig.DeclarativeConfigurationYamlParser | 1 + .../DeclarativeConfigurationSpiTest.java | 103 ++++++++ .../ExtensionPropertyUtilTest.java | 12 +- .../DeclarativeConfigProviderLoaderTest.java | 95 +++++++ .../model/ModelPackageBoundaryTest.java | 1 - settings.gradle.kts | 1 + 22 files changed, 721 insertions(+), 251 deletions(-) create mode 100644 sdk-extensions/declarative-config-jackson/README.md create mode 100644 sdk-extensions/declarative-config-jackson/build.gradle.kts create mode 100644 sdk-extensions/declarative-config-jackson/gradle.properties create mode 100644 sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java rename sdk-extensions/{declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ModelMapper.java => declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonModelMapper.java} (61%) create mode 100644 sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper create mode 100644 sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java create mode 100644 sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java create mode 100644 sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoader.java create mode 100644 sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java create mode 100644 sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java create mode 100644 sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser create mode 100644 sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java create mode 100644 sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoaderTest.java diff --git a/sdk-extensions/declarative-config-jackson/README.md b/sdk-extensions/declarative-config-jackson/README.md new file mode 100644 index 00000000000..6d4a22f4371 --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/README.md @@ -0,0 +1,8 @@ +# Declarative Configuration Jackson Mapper + +This optional extension provides a Jackson 2 implementation of the +`DeclarativeConfigurationMapper` SPI for `opentelemetry-sdk-extension-declarative-config`. + +Add this artifact when using the default Jackson 2 mapper. Projects using another mapper can +exclude this artifact and register their own `DeclarativeConfigurationMapper` provider. For +example, a project using Jackson 3 can implement the SPI with its Jackson 3 `ObjectMapper`. diff --git a/sdk-extensions/declarative-config-jackson/build.gradle.kts b/sdk-extensions/declarative-config-jackson/build.gradle.kts new file mode 100644 index 00000000000..ee4fe87ae81 --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry SDK Declarative Config Jackson Mapper" +otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson") +otelJava.requireSuppressWarningsExplanation.set(false) +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper", +)) + +dependencies { + api(project(":sdk-extensions:declarative-config")) + compileOnly(project(":sdk-extensions:autoconfigure-spi")) + implementation("com.fasterxml.jackson.core:jackson-databind") +} diff --git a/sdk-extensions/declarative-config-jackson/gradle.properties b/sdk-extensions/declarative-config-jackson/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java b/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java new file mode 100644 index 00000000000..af51505275f --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java @@ -0,0 +1,35 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class JacksonDeclarativeConfigurationMapper implements DeclarativeConfigurationMapper { + + @Override + public int order() { + return Integer.MIN_VALUE; + } + + @Override + public T convert(Object value, Class targetType) { + return JacksonModelMapper.MAPPER.convertValue(value, targetType); + } + + @Nullable + @Override + public Map toMap(Object value) { + return JacksonModelMapper.MAPPER.convertValue( + value, new TypeReference>() {}); + } +} diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ModelMapper.java b/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonModelMapper.java similarity index 61% rename from sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ModelMapper.java rename to sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonModelMapper.java index 722c2e69f82..a5803b7d15a 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ModelMapper.java +++ b/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonModelMapper.java @@ -3,37 +3,32 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal; +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal; import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; import com.fasterxml.jackson.databind.ObjectMapper; /** - * Canonical {@link ObjectMapper} configuration for parsing declarative configuration YAML. {@link - * io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfiguration#MAPPER} delegates - * to this instance. - * - *

This class is internal and experimental. Its APIs are unstable and can change at any time. Its - * APIs (or a version of them) may be promoted to the public stable API in the future, but no - * guarantees are made. + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. */ -public final class ModelMapper { +public final class JacksonModelMapper { public static final ObjectMapper MAPPER; static { MAPPER = new ObjectMapper() - // Create empty object instances for keys which are present but have null values + // Create empty object instances for keys which are present but have null values. .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)); // Boxed primitives which are present but have null values should be set to null, rather than - // empty instances + // empty instances. MAPPER.configOverride(String.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); MAPPER.configOverride(Integer.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); MAPPER.configOverride(Double.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); MAPPER.configOverride(Boolean.class).setSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SET)); } - private ModelMapper() {} + private JacksonModelMapper() {} } diff --git a/sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper b/sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper new file mode 100644 index 00000000000..afde493d993 --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal.JacksonDeclarativeConfigurationMapper diff --git a/sdk-extensions/declarative-config/README.md b/sdk-extensions/declarative-config/README.md index 99ed7553c00..335ec434f7d 100644 --- a/sdk-extensions/declarative-config/README.md +++ b/sdk-extensions/declarative-config/README.md @@ -16,6 +16,7 @@ try (FileInputStream yamlConfigFileInputStream = new FileInputStream("/path/to/c Notes: * Environment variable substitution is supported as [defined in the spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/data-model.md#environment-variable-substitution) * Customization is limited to customizing the in-memory config model, or exporters. See `DeclarativeConfigurationCustomizer` SPI for details. +* YAML parsing and mapping parsed values to configuration models can be replaced with `DeclarativeConfigurationYamlParser` and `DeclarativeConfigurationMapper` SPI implementations. Providers are discovered with `ServiceLoader`; the provider with the highest `order()` is selected. The built-in SnakeYAML parser is included. The Jackson 2 mapper is provided by the optional `opentelemetry-sdk-extension-declarative-config-jackson` artifact. Add that artifact to use the default mapper, or register a mapper provider for another implementation such as Jackson 3. With no mapper provider, operations that need model mapping fail with a `DeclarativeConfigException`. * Custom SDK extension components which reference the [ComponentProvider](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ComponentProvider.java) SPI can be referenced in declarative configuration. Supported types include: * `Resource` * `SpanExporter` diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index 2390d633d1b..5a680f2b431 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -16,10 +16,13 @@ otelJava.requireSuppressWarningsExplanation.set(false) otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.autoconfigure.spi")) otelJava.osgiServiceLoaderProvides.set(listOf( "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser", )) -// declarative-config discovers customizer providers at runtime via ServiceLoader +// declarative-config discovers these providers at runtime via ServiceLoader otelJava.osgiServiceLoaderRequires.set(listOf( "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper", )) dependencies { @@ -33,9 +36,9 @@ dependencies { api("com.fasterxml.jackson.core:jackson-annotations") implementation("org.snakeyaml:snakeyaml-engine") - implementation("com.fasterxml.jackson.core:jackson-databind") testImplementation(project(":sdk:testing")) + testImplementation(project(":sdk-extensions:declarative-config-jackson")) testImplementation(project(":sdk-extensions:autoconfigure-spi")) testImplementation(project(":exporters:logging")) testImplementation(project(":exporters:logging-otlp")) @@ -46,6 +49,7 @@ dependencies { testImplementation("io.opentelemetry.contrib:opentelemetry-aws-xray-propagator") testImplementation("io.opentelemetry.instrumentation:opentelemetry-resources") testImplementation("com.linecorp.armeria:armeria-junit5") + testImplementation("com.fasterxml.jackson.core:jackson-databind") // testImplementation("com.google.guava:guava-testlib") testImplementation("nl.jqno.equalsverifier:equalsverifier") @@ -108,8 +112,8 @@ val syncPojoModelsToSrc = tasks.register("syncPojoModelsToSrc") { val modelSrcDir = File(projectDir, "src/main/java") doLast { val modelDir = File(modelSrcDir, modelPackage.replace('.', '/')) - // Delete only @Generated files so hand-written files (ModelMapper, ExtensionPropertyUtil) - // in model.internal survive the regeneration cycle. + // Delete only @Generated files so hand-written files (ExtensionPropertyUtil) in model.internal + // survive the regeneration cycle. modelDir.walkTopDown() .filter { it.isFile && it.extension == "java" } .filter { it.readText().contains("@Generated(") } diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java index 245936f5efe..821136750e4 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java @@ -5,15 +5,15 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.common.ComponentLoader; import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.DeclarativeConfigProviderLoader; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.DeclarativeConfigurationMapperUtil; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.SnakeYamlDeclarativeConfigurationParser; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.OpenTelemetryConfigurationModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.SamplerModel; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ModelMapper; import io.opentelemetry.sdk.autoconfigure.spi.Ordered; import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener; import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; @@ -26,23 +26,6 @@ import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; -import java.util.regex.MatchResult; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import javax.annotation.Nullable; -import org.snakeyaml.engine.v2.api.Load; -import org.snakeyaml.engine.v2.api.LoadSettings; -import org.snakeyaml.engine.v2.api.YamlUnicodeReader; -import org.snakeyaml.engine.v2.common.ScalarStyle; -import org.snakeyaml.engine.v2.composer.Composer; -import org.snakeyaml.engine.v2.nodes.MappingNode; -import org.snakeyaml.engine.v2.nodes.Node; -import org.snakeyaml.engine.v2.nodes.ScalarNode; -import org.snakeyaml.engine.v2.nodes.Tag; -import org.snakeyaml.engine.v2.parser.ParserImpl; -import org.snakeyaml.engine.v2.resolver.ScalarResolver; -import org.snakeyaml.engine.v2.scanner.StreamReader; -import org.snakeyaml.engine.v2.schema.CoreSchema; /** * Configure {@link OpenTelemetrySdk} using For Implementers * - *

External consumers needing to parse OpenTelemetry YAML configuration files should use the same - * Jackson {@code ObjectMapper} configuration for compatibility. See {@link - * io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ModelMapper#MAPPER}. + *

YAML parsing and model mapping can be replaced by implementations of {@link + * DeclarativeConfigurationYamlParser} and {@link DeclarativeConfigurationMapper}. */ 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("\\$\\{(?:(env|sys):)?([a-zA-Z_][a-zA-Z0-9_.]*)(?::-([^\\n}]*))?}"); private static final ComponentLoader DEFAULT_COMPONENT_LOADER = ComponentLoader.forClassLoader(DeclarativeConfigProperties.class.getClassLoader()); - - // Visible for testing - static final ObjectMapper MAPPER = ModelMapper.MAPPER; + private static final DeclarativeConfigurationYamlParser YAML_PARSER = + DeclarativeConfigProviderLoader.load( + DeclarativeConfigurationYamlParser.class, SnakeYamlDeclarativeConfigurationParser::new); private DeclarativeConfiguration() {} @@ -152,7 +131,9 @@ static OpenTelemetryConfigurationModel parse( Map environmentVariables, Map systemProperties) { Object yamlObj = loadYaml(configuration, environmentVariables, systemProperties); - return MAPPER.convertValue(yamlObj, OpenTelemetryConfigurationModel.class); + OpenTelemetryConfigurationModel model = + DeclarativeConfigurationMapperUtil.convert(yamlObj, OpenTelemetryConfigurationModel.class); + return Objects.requireNonNull(model, "Unable to map configuration to model"); } // Visible for testing @@ -160,9 +141,7 @@ static Object loadYaml( InputStream inputStream, Map environmentVariables, Map systemProperties) { - LoadSettings settings = LoadSettings.builder().setSchema(new CoreSchema()).build(); - Load yaml = new EnvLoad(settings, environmentVariables, systemProperties); - return yaml.loadFromInputStream(inputStream); + return YAML_PARSER.parse(inputStream, environmentVariables, systemProperties); } /** @@ -188,8 +167,7 @@ public static DeclarativeConfigProperties toConfigProperties(InputStream configu static DeclarativeConfigProperties toConfigProperties( Object model, ComponentLoader componentLoader) { - Map configurationMap = - MAPPER.convertValue(model, new TypeReference>() {}); + Map configurationMap = DeclarativeConfigurationMapperUtil.toMap(model); if (configurationMap == null) { configurationMap = Collections.emptyMap(); } @@ -210,8 +188,11 @@ public static Sampler createSampler(DeclarativeConfigProperties genericSamplerMo YamlDeclarativeConfigProperties yamlDeclarativeConfigProperties = requireYamlDeclarativeConfigProperties(genericSamplerModel); SamplerModel samplerModel = - MAPPER.convertValue( - DeclarativeConfigProperties.toMap(yamlDeclarativeConfigProperties), SamplerModel.class); + Objects.requireNonNull( + DeclarativeConfigurationMapperUtil.convert( + DeclarativeConfigProperties.toMap(yamlDeclarativeConfigProperties), + SamplerModel.class), + "Unable to map sampler configuration to model"); return createAndMaybeCleanup( SamplerFactory.getInstance(), new DeclarativeConfigContext(yamlDeclarativeConfigProperties.getComponentLoader()), @@ -248,192 +229,6 @@ static R createAndMaybeCleanup( } } - 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, - Map systemProperties) { - super(settings); - this.settings = settings; - this.environmentVariables = environmentVariables; - this.systemProperties = systemProperties; - } - - @Override - public Object loadFromInputStream(InputStream yamlStream) { - Objects.requireNonNull(yamlStream, "InputStream cannot be null"); - return loadOne( - new EnvComposer( - settings, - new ParserImpl( - settings, new StreamReader(settings, new YamlUnicodeReader(yamlStream))), - environmentVariables, - systemProperties)); - } - } - - /** - * A YAML Composer that performs environment variable and system property substitution according - * to the - * OpenTelemetry Configuration Data Model specification. - * - *

This composer supports: - * - *

    - *
  • 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 $} - *
- * - *

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 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 = "$$"; - private static final int ESCAPE_SEQUENCE_LENGTH = ESCAPE_SEQUENCE.length(); - private static final char ESCAPE_SEQUENCE_REPLACEMENT = '$'; - - private EnvComposer( - 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(); - } - - @Override - protected Node composeValueNode(MappingNode node) { - Node itemValue = super.composeValueNode(node); - if (!(itemValue instanceof ScalarNode)) { - // Only apply environment variable substitution to ScalarNodes - return itemValue; - } - ScalarNode scalarNode = (ScalarNode) itemValue; - String envSubstitution = envSubstitution(scalarNode.getValue()); - - // If the environment variable substitution does not change the value, do not modify the node - if (envSubstitution.equals(scalarNode.getValue())) { - return itemValue; - } - - Object envSubstitutionObj = load.loadFromString(envSubstitution); - Tag tag = itemValue.getTag(); - ScalarStyle scalarStyle = scalarNode.getScalarStyle(); - - Tag resolvedTag = - envSubstitutionObj == null - ? Tag.NULL - : scalarResolver.resolve(envSubstitutionObj.toString(), true); - - // Only non-quoted substituted scalars can have their tag changed - if (!itemValue.getTag().equals(resolvedTag) - && scalarStyle != ScalarStyle.SINGLE_QUOTED - && scalarStyle != ScalarStyle.DOUBLE_QUOTED) { - tag = resolvedTag; - } - - boolean resolved = true; - return new ScalarNode( - tag, - resolved, - envSubstitution, - scalarStyle, - itemValue.getStartMark(), - itemValue.getEndMark()); - } - - 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 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; - 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()); - if (offset != val.length()) { - newVal.append(val, offset, val.length()); - } - - return newVal; - } - } - // Visible for testing static void callAutoConfigureListeners( DeclarativeConfigContext context, OpenTelemetrySdk openTelemetrySdk) { diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java new file mode 100644 index 00000000000..3273af04b0d --- /dev/null +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig; + +import io.opentelemetry.sdk.autoconfigure.spi.Ordered; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * Converts parsed configuration values to declarative configuration models and generic maps. + * + *

Implementations are discovered with {@link java.util.ServiceLoader}. The implementation with + * the highest {@link #order()} is used. The optional {@code + * opentelemetry-sdk-extension-declarative-config-jackson} artifact supplies a Jackson 2 + * implementation. Implementations must preserve the mapping behavior of the declarative + * configuration models, including extension properties and null values. + */ +public interface DeclarativeConfigurationMapper extends Ordered { + + /** + * Convert a value to an instance of {@code targetType}. + * + * @param value the value to convert + * @param targetType the target type + * @param the target type + * @return the converted value + */ + T convert(Object value, Class targetType); + + /** + * Convert a model or parsed configuration value to a map with string keys. + * + * @param value the value to convert + * @return the converted map, or null if the input value is null + */ + @Nullable + Map toMap(Object value); +} diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java new file mode 100644 index 00000000000..c6faeb47650 --- /dev/null +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig; + +import io.opentelemetry.sdk.autoconfigure.spi.Ordered; +import java.io.InputStream; +import java.util.Map; + +/** + * Parses a declarative configuration YAML stream into generic Java objects. + * + *

Implementations are discovered with {@link java.util.ServiceLoader}. The implementation with + * the highest {@link #order()} is used. The built-in parser has the lowest possible order and is + * used when no other implementation is registered. + * + *

Implementations must apply environment variable and system property substitution according to + * the OpenTelemetry Configuration Data Model specification. Substitution is performed on scalar + * values, not mapping keys. For unquoted scalar values, substituted text must be parsed using the + * same YAML scalar rules as the original text. + */ +public interface DeclarativeConfigurationYamlParser extends Ordered { + + /** + * Parse a YAML stream. + * + * @param configuration the YAML input stream + * @param environmentVariables environment variables used for substitution + * @param systemProperties system properties used for substitution + * @return the parsed configuration + */ + Object parse( + InputStream configuration, + Map environmentVariables, + Map systemProperties); +} diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/YamlDeclarativeConfigProperties.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/YamlDeclarativeConfigProperties.java index 43a4bb16ef1..9dce82ac27f 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/YamlDeclarativeConfigProperties.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/YamlDeclarativeConfigProperties.java @@ -67,9 +67,8 @@ private YamlDeclarativeConfigProperties( /** * Create a {@link YamlDeclarativeConfigProperties} from the {@code properties} map. * - *

{@code properties} is expected to be the output of YAML parsing (i.e. with Jackson {@code - * com.fasterxml.jackson.databind.ObjectMapper}), and have values which are scalars, lists of - * scalars, lists of maps, and maps. + *

{@code properties} is expected to have values which are scalars, lists of scalars, lists of + * maps, and maps. * * @see DeclarativeConfiguration#toConfigProperties(Object) */ diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoader.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoader.java new file mode 100644 index 00000000000..8c110fb46af --- /dev/null +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoader.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal; + +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.sdk.autoconfigure.spi.Ordered; +import java.util.List; +import java.util.function.Supplier; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class DeclarativeConfigProviderLoader { + + private static final ComponentLoader COMPONENT_LOADER = + ComponentLoader.forClassLoader(DeclarativeConfigProperties.class.getClassLoader()); + + private DeclarativeConfigProviderLoader() {} + + /** + * Load the highest-ordered implementation of {@code spiClass}, or create its default. + * + * @param spiClass the SPI type + * @param defaultProvider supplies the default provider when none are registered + * @param the SPI type + * @return the selected provider + */ + public static T load(Class spiClass, Supplier defaultProvider) { + return load(spiClass, defaultProvider, COMPONENT_LOADER); + } + + static T load( + Class spiClass, Supplier defaultProvider, ComponentLoader componentLoader) { + List providers = Ordered.loadOrderedList(componentLoader, spiClass); + return providers.isEmpty() ? defaultProvider.get() : providers.get(providers.size() - 1); + } +} diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java new file mode 100644 index 00000000000..0dfd1129e88 --- /dev/null +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java @@ -0,0 +1,68 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal; + +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper; +import java.util.Map; +import javax.annotation.Nullable; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class DeclarativeConfigurationMapperUtil { + + private static final DeclarativeConfigurationMapper MAPPER = + DeclarativeConfigProviderLoader.load( + DeclarativeConfigurationMapper.class, MissingMapper::new); + + private DeclarativeConfigurationMapperUtil() {} + + /** + * Convert a value to an instance of {@code targetType} using the selected mapper. + * + * @param value the value to convert + * @param targetType the target type + * @param the target type + * @return the converted value + */ + public static T convert(Object value, Class targetType) { + return MAPPER.convert(value, targetType); + } + + /** + * Convert a model or parsed configuration value to a generic map using the selected mapper. + * + * @param value the value to convert + * @return the converted map, or null if the mapper returns null + */ + @Nullable + public static Map toMap(Object value) { + return MAPPER.toMap(value); + } + + private static final class MissingMapper implements DeclarativeConfigurationMapper { + + @Override + public T convert(Object value, Class targetType) { + throw missingMapperException(); + } + + @Nullable + @Override + public Map toMap(Object value) { + throw missingMapperException(); + } + + private static DeclarativeConfigException missingMapperException() { + return new DeclarativeConfigException( + "No DeclarativeConfigurationMapper provider is available. Add the optional " + + "opentelemetry-sdk-extension-declarative-config-jackson artifact or register a " + + "custom mapper provider."); + } + } +} diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java new file mode 100644 index 00000000000..0a947c707af --- /dev/null +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java @@ -0,0 +1,219 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal; + +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser; +import java.io.InputStream; +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.snakeyaml.engine.v2.api.Load; +import org.snakeyaml.engine.v2.api.LoadSettings; +import org.snakeyaml.engine.v2.api.YamlUnicodeReader; +import org.snakeyaml.engine.v2.common.ScalarStyle; +import org.snakeyaml.engine.v2.composer.Composer; +import org.snakeyaml.engine.v2.nodes.MappingNode; +import org.snakeyaml.engine.v2.nodes.Node; +import org.snakeyaml.engine.v2.nodes.ScalarNode; +import org.snakeyaml.engine.v2.nodes.Tag; +import org.snakeyaml.engine.v2.parser.ParserImpl; +import org.snakeyaml.engine.v2.resolver.ScalarResolver; +import org.snakeyaml.engine.v2.scanner.StreamReader; +import org.snakeyaml.engine.v2.schema.CoreSchema; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class SnakeYamlDeclarativeConfigurationParser + implements DeclarativeConfigurationYamlParser { + + // Matches ${VAR_NAME}, ${env:VAR_NAME}, or ${sys:property.name} with optional :-default + private static final Pattern ENV_VARIABLE_REFERENCE = + Pattern.compile("\\$\\{(?:(env|sys):)?([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 = '$'; + + @Override + public int order() { + return Integer.MIN_VALUE; + } + + @Override + public Object parse( + InputStream configuration, + Map environmentVariables, + Map systemProperties) { + LoadSettings settings = LoadSettings.builder().setSchema(new CoreSchema()).build(); + Load yaml = new EnvLoad(settings, environmentVariables, systemProperties); + return yaml.loadFromInputStream(configuration); + } + + 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, + Map systemProperties) { + super(settings); + this.settings = settings; + this.environmentVariables = environmentVariables; + this.systemProperties = systemProperties; + } + + @Override + public Object loadFromInputStream(InputStream yamlStream) { + Objects.requireNonNull(yamlStream, "InputStream cannot be null"); + return loadOne( + new EnvComposer( + settings, + new ParserImpl( + settings, new StreamReader(settings, new YamlUnicodeReader(yamlStream))), + environmentVariables, + systemProperties)); + } + } + + /** + * A YAML Composer that performs environment variable and system property substitution according + * to the OpenTelemetry Configuration Data Model specification. + */ + private static final class EnvComposer extends Composer { + + private final Load load; + private final Map environmentVariables; + private final Map systemProperties; + private final ScalarResolver scalarResolver; + + private EnvComposer( + 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(); + } + + @Override + protected Node composeValueNode(MappingNode node) { + Node itemValue = super.composeValueNode(node); + if (!(itemValue instanceof ScalarNode)) { + // Only apply environment variable substitution to ScalarNodes + return itemValue; + } + ScalarNode scalarNode = (ScalarNode) itemValue; + String envSubstitution = envSubstitution(scalarNode.getValue()); + + // If the environment variable substitution does not change the value, do not modify the node + if (envSubstitution.equals(scalarNode.getValue())) { + return itemValue; + } + + Object envSubstitutionObj = load.loadFromString(envSubstitution); + Tag tag = itemValue.getTag(); + ScalarStyle scalarStyle = scalarNode.getScalarStyle(); + + Tag resolvedTag = + envSubstitutionObj == null + ? Tag.NULL + : scalarResolver.resolve(envSubstitutionObj.toString(), true); + + // Only non-quoted substituted scalars can have their tag changed + if (!itemValue.getTag().equals(resolvedTag) + && scalarStyle != ScalarStyle.SINGLE_QUOTED + && scalarStyle != ScalarStyle.DOUBLE_QUOTED) { + tag = resolvedTag; + } + + boolean resolved = true; + return new ScalarNode( + tag, + resolved, + envSubstitution, + scalarStyle, + itemValue.getStartMark(), + itemValue.getEndMark()); + } + + private String envSubstitution(String val) { + // Substitute each portion between escaped dollar signs independently. Each "$$" becomes + // one literal "$" and prevents substitution in the adjacent portion. + 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 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; + 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()); + if (offset != val.length()) { + newVal.append(val, offset, val.length()); + } + + return newVal; + } + } +} diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExtensionPropertyUtil.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExtensionPropertyUtil.java index 1f9f3ce0b44..80d972e2a9c 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExtensionPropertyUtil.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/internal/ExtensionPropertyUtil.java @@ -5,6 +5,7 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.DeclarativeConfigurationMapperUtil; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -48,7 +49,8 @@ public static void handleAnySetter( LOGGER.warning( "Property '" + name + "' has been stabilized. Use '" + candidate + "' instead."); extensionProperties.put( - name, value == null ? null : ModelMapper.MAPPER.convertValue(value, stableType)); + name, + value == null ? null : DeclarativeConfigurationMapperUtil.convert(value, stableType)); return; } } @@ -56,7 +58,10 @@ public static void handleAnySetter( Class experimentalType = experimentalPropertyTypes.get(name); if (experimentalType != null) { extensionProperties.put( - name, value == null ? null : ModelMapper.MAPPER.convertValue(value, experimentalType)); + name, + value == null + ? null + : DeclarativeConfigurationMapperUtil.convert(value, experimentalType)); return; } @@ -72,7 +77,7 @@ public static void handleAnySetter( * Returns the extension property value for {@code key}, cast to {@code type}. * *

Values are guaranteed to be the correct type at store time: {@link #handleAnySetter} - * converts known experimental properties via {@code ModelMapper} before storing, and the + * converts known experimental properties via the configured mapper before storing, and the * generated accessor builders enforce the type at the call site. A {@link ClassCastException} * here indicates a contract violation (e.g. writing directly to the extension properties map with * the wrong type). diff --git a/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser b/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser new file mode 100644 index 00000000000..64e47bdf73d --- /dev/null +++ b/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.SnakeYamlDeclarativeConfigurationParser diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java new file mode 100644 index 00000000000..32d0679399a --- /dev/null +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.DeclarativeConfigProviderLoader; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.SnakeYamlDeclarativeConfigurationParser; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.OpenTelemetryConfigurationModel; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class DeclarativeConfigurationSpiTest { + + @Test + void defaultProvidersAreRegisteredWithServiceLoader() { + assertThat( + DeclarativeConfigProviderLoader.load( + DeclarativeConfigurationYamlParser.class, + SnakeYamlDeclarativeConfigurationParser::new)) + .isInstanceOf(SnakeYamlDeclarativeConfigurationParser.class); + DeclarativeConfigurationMapper mapper = loadMapper(); + assertThat(mapper.getClass().getName()) + .isEqualTo( + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal." + + "JacksonDeclarativeConfigurationMapper"); + assertThat(mapper.order()).isEqualTo(Integer.MIN_VALUE); + } + + @Test + void yamlParserPerformsEnvironmentAndSystemPropertySubstitution() { + String yaml = "boolean: ${BOOL}\nquoted: '${BOOL}'\nproperty: ${sys:port}\n"; + Map environmentVariables = Collections.singletonMap("BOOL", "true"); + Map systemProperties = new HashMap<>(); + systemProperties.put("port", "4317"); + + Object parsed = + new SnakeYamlDeclarativeConfigurationParser() + .parse( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)), + environmentVariables, + systemProperties); + + assertThat(parsed).isEqualTo(mapOf("boolean", true, "quoted", "true", "property", 4317)); + } + + @Test + void mapperConvertsModelsAndGenericMaps() { + Map input = new HashMap<>(); + input.put("file_format", "1.2"); + input.put("disabled", true); + + DeclarativeConfigurationMapper mapper = loadMapper(); + OpenTelemetryConfigurationModel model = + mapper.convert(input, OpenTelemetryConfigurationModel.class); + + assertThat(model.getFileFormat()).isEqualTo("1.2"); + assertThat(model.getDisabled()).isTrue(); + assertThat(mapper.toMap(model)) + .containsEntry("file_format", "1.2") + .containsEntry("disabled", true); + } + + @Test + void topLevelParseAndPropertiesUseRegisteredProviders() { + String yaml = "file_format: '1.2'\ndisabled: true\n"; + ByteArrayInputStream input = new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)); + + OpenTelemetryConfigurationModel model = DeclarativeConfiguration.parse(input); + DeclarativeConfigProperties properties = + DeclarativeConfiguration.toConfigProperties( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + + assertThat(model.getFileFormat()).isEqualTo("1.2"); + assertThat(model.getDisabled()).isTrue(); + assertThat(properties.getString("file_format")).isEqualTo("1.2"); + assertThat(properties.getBoolean("disabled")).isTrue(); + } + + private static Map mapOf(Object... entries) { + Map result = new HashMap<>(); + for (int i = 0; i < entries.length; i += 2) { + result.put((String) entries[i], entries[i + 1]); + } + return result; + } + + private static DeclarativeConfigurationMapper loadMapper() { + return DeclarativeConfigProviderLoader.load( + DeclarativeConfigurationMapper.class, + () -> { + throw new AssertionError("Expected a mapper provider to be registered"); + }); + } +} diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ExtensionPropertyUtilTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ExtensionPropertyUtilTest.java index 6cad1c2a064..078b3fd08e7 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ExtensionPropertyUtilTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/ExtensionPropertyUtilTest.java @@ -7,13 +7,13 @@ import static org.assertj.core.api.Assertions.assertThat; +import com.fasterxml.jackson.databind.ObjectMapper; import io.github.netmikey.logunit.api.LogCapturer; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.LogRecordLimitsModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.LoggerProviderModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.SamplerModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExperimentalComposableSamplerModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ExtensionPropertyUtil; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.internal.ModelMapper; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; @@ -24,6 +24,8 @@ class ExtensionPropertyUtilTest { + private final ObjectMapper mapper = new ObjectMapper(); + @RegisterExtension LogCapturer logs = LogCapturer.create().captureForLogger(ExtensionPropertyUtil.class.getName(), Level.TRACE); @@ -181,20 +183,20 @@ void filterSerializable_dropsGraduatedKeys_preservesOthers() { @Test void stableModel_roundTripsGraduatedKey_emitsStableKeyOnly() throws Exception { String json = "{\"limits/development\":{}}"; - LoggerProviderModel model = ModelMapper.MAPPER.readValue(json, LoggerProviderModel.class); + LoggerProviderModel model = mapper.readValue(json, LoggerProviderModel.class); assertThat(model.getLimits()).isNotNull().isInstanceOf(LogRecordLimitsModel.class); - String reserialized = ModelMapper.MAPPER.writeValueAsString(model); + String reserialized = mapper.writeValueAsString(model); assertThat(reserialized).contains("\"limits\":").doesNotContain("limits/development"); } @Test void stableModel_roundTripsNonGraduatedDevelopmentKey_verbatim() throws Exception { String json = "{\"composite/development\":{}}"; - SamplerModel model = ModelMapper.MAPPER.readValue(json, SamplerModel.class); + SamplerModel model = mapper.readValue(json, SamplerModel.class); - String reserialized = ModelMapper.MAPPER.writeValueAsString(model); + String reserialized = mapper.writeValueAsString(model); assertThat(reserialized).contains("composite/development"); } } diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoaderTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoaderTest.java new file mode 100644 index 00000000000..79d37406000 --- /dev/null +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigProviderLoaderTest.java @@ -0,0 +1,95 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.sdk.autoconfigure.spi.Ordered; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import org.junit.jupiter.api.Test; + +class DeclarativeConfigProviderLoaderTest { + + @Test + void loadSelectsHighestOrderedProvider() { + TestProvider lowerOrder = new TestProvider(1); + TestProvider higherOrder = new TestProvider(2); + ComponentLoader componentLoader = componentLoader(lowerOrder, higherOrder); + + TestSpi selected = + DeclarativeConfigProviderLoader.load( + TestSpi.class, () -> new TestProvider(Integer.MIN_VALUE), componentLoader); + + assertThat(selected).isSameAs(higherOrder); + } + + @Test + void loadUsesDefaultWhenNoProvidersAreRegistered() { + AtomicBoolean defaultProviderCalled = new AtomicBoolean(); + Supplier defaultProvider = + () -> { + defaultProviderCalled.set(true); + return new TestProvider(Integer.MIN_VALUE); + }; + + TestSpi selected = + DeclarativeConfigProviderLoader.load(TestSpi.class, defaultProvider, componentLoader()); + + assertThat(defaultProviderCalled).isTrue(); + assertThat(selected).isInstanceOf(TestProvider.class); + } + + @Test + void loadDoesNotCreateDefaultWhenProviderIsRegistered() { + AtomicBoolean defaultProviderCalled = new AtomicBoolean(); + TestProvider provider = new TestProvider(0); + + TestSpi selected = + DeclarativeConfigProviderLoader.load( + TestSpi.class, + () -> { + defaultProviderCalled.set(true); + return new TestProvider(Integer.MIN_VALUE); + }, + componentLoader(provider)); + + assertThat(selected).isSameAs(provider); + assertThat(defaultProviderCalled).isFalse(); + } + + private static ComponentLoader componentLoader(TestProvider... providers) { + return new ComponentLoader() { + @Override + public Iterable load(Class spiClass) { + List result = new ArrayList<>(); + for (TestProvider provider : providers) { + result.add(spiClass.cast(provider)); + } + return result; + } + }; + } + + private interface TestSpi extends Ordered {} + + private static final class TestProvider implements TestSpi { + + private final int order; + + private TestProvider(int order) { + this.order = order; + } + + @Override + public int order() { + return order; + } + } +} diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/ModelPackageBoundaryTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/ModelPackageBoundaryTest.java index dec71f24aef..ff1f4a37d6f 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/ModelPackageBoundaryTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/model/ModelPackageBoundaryTest.java @@ -56,7 +56,6 @@ void experimentalTypesAreExactlyTheInternalPackage() throws IOException { // Internal utility classes are exempt from the Experimental prefix requirement. boolean isInfrastructure = clazz.getSimpleName().endsWith("Accessor") - || clazz.getSimpleName().equals("ModelMapper") || clazz.getSimpleName().equals("ExtensionPropertyUtil"); assertThat(experimental || isInfrastructure) .as( diff --git a/settings.gradle.kts b/settings.gradle.kts index bf8362e1464..f55eeff5351 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,6 +69,7 @@ include(":sdk:trace-shaded-deps") include(":sdk-extensions:autoconfigure") include(":sdk-extensions:autoconfigure-spi") include(":sdk-extensions:declarative-config") +include(":sdk-extensions:declarative-config-jackson") include(":sdk-extensions:incubator") include(":sdk-extensions:jaeger-remote-sampler") include(":testing-internal") From a103e4d2b95aa51aa4fa480cbcd11412cff96acb Mon Sep 17 00:00:00 2001 From: Ashish Thakur Date: Wed, 28 Oct 2026 19:14:06 +0530 Subject: [PATCH 2/3] feat: dependency fix based on tests run Signed-off-by: Ashish Thakur --- CHANGELOG.md | 9 +++++++++ api/incubator/build.gradle.kts | 3 +++ integration-tests/graal-incubating/build.gradle.kts | 2 ++ integration-tests/osgi/build.gradle.kts | 6 ++++-- sdk-extensions/autoconfigure/build.gradle.kts | 2 ++ 5 files changed, 20 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ebd56e3531..db3fc0e58b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### SDK + +#### Extensions + +* Declarative config: Add parser and mapper SPIs for issue + [#8820](https://github.com/open-telemetry/opentelemetry-java/issues/8820), and move the Jackson 2 + mapper to an optional extension. Add the extension to use the built-in Jackson 2 mapper; register + a custom mapper to use another implementation such as Jackson 3. + ## Version 1.67.0 (2026-10-09) ### API diff --git a/api/incubator/build.gradle.kts b/api/incubator/build.gradle.kts index 56046af9eeb..e4d57df0456 100644 --- a/api/incubator/build.gradle.kts +++ b/api/incubator/build.gradle.kts @@ -21,6 +21,8 @@ dependencies { // To use parsed config file as input for InstrumentationConfigUtilTest testImplementation(project(":sdk-extensions:declarative-config")) + testRuntimeOnly(project(":sdk-extensions:autoconfigure-spi")) + testRuntimeOnly(project(":sdk-extensions:declarative-config-jackson")) testImplementation(project(":sdk:testing")) testImplementation(project(":api:testing-internal")) @@ -36,6 +38,7 @@ testing { dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") implementation(project(":sdk-extensions:declarative-config")) + implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":sdk-extensions:autoconfigure")) implementation("com.google.guava:guava") } diff --git a/integration-tests/graal-incubating/build.gradle.kts b/integration-tests/graal-incubating/build.gradle.kts index d669cb3b773..3b666c3cce9 100644 --- a/integration-tests/graal-incubating/build.gradle.kts +++ b/integration-tests/graal-incubating/build.gradle.kts @@ -25,7 +25,9 @@ dependencies { implementation(project(":sdk:testing")) implementation(project(":exporters:otlp:all")) implementation(project(":api:incubator")) + implementation(project(":sdk-extensions:autoconfigure-spi")) implementation(project(":sdk-extensions:declarative-config")) + implementation(project(":sdk-extensions:declarative-config-jackson")) } // org.graalvm.buildtools.native plugin requires java 17+ as of version 0.11.0 diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 9a4dac688f0..ebdbf61e8eb 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -284,8 +284,8 @@ val autoconfigureSuiteTask = registerOsgiSuite( runtimeOnly(project(":exporters:sender:jdk")) } -// Suite: autoconfigure + declarative-config. Same as above but with declarative-config on the -// classpath, additionally exercising declarative-config bundle OSGi metadata. +// Suite: autoconfigure + declarative-config. Same as above but with declarative-config and its +// Jackson mapper adapter on the classpath, additionally exercising their OSGi metadata. val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( "autoconfigureDeclarativeConfig", extraRunrequires = listOf( @@ -293,6 +293,7 @@ val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( "opentelemetry-exporter-otlp", "opentelemetry-extension-trace-propagators", "opentelemetry-sdk-extension-declarative-config", + "opentelemetry-sdk-extension-declarative-config-jackson", ), serviceLoaderProvides = listOf( "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider", @@ -302,6 +303,7 @@ val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( implementation(project(":sdk:all")) implementation(project(":sdk-extensions:autoconfigure")) implementation(project(":sdk-extensions:declarative-config")) + implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":exporters:otlp:all")) implementation(project(":extensions:trace-propagators")) runtimeOnly(project(":exporters:sender:jdk")) diff --git a/sdk-extensions/autoconfigure/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts index 692bb0c7812..e600915941f 100644 --- a/sdk-extensions/autoconfigure/build.gradle.kts +++ b/sdk-extensions/autoconfigure/build.gradle.kts @@ -107,6 +107,7 @@ testing { register("testIncubating") { dependencies { implementation(project(":sdk-extensions:declarative-config")) + implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":exporters:logging")) implementation(project(":exporters:otlp:all")) implementation(project(":sdk:testing")) @@ -120,6 +121,7 @@ testing { register("testDeclarativeConfigSpi") { dependencies { implementation(project(":sdk-extensions:declarative-config")) + implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":exporters:logging")) implementation(project(":sdk:testing")) } From 93ffc5d92e0551aaa40cc3f3a79b035787bed2a0 Mon Sep 17 00:00:00 2001 From: Ashish Thakur Date: Wed, 28 Oct 2026 20:01:43 +0530 Subject: [PATCH 3/3] feat: few changes for better arch Signed-off-by: Ashish Thakur --- CHANGELOG.md | 8 ++-- api/incubator/build.gradle.kts | 3 -- .../graal-incubating/build.gradle.kts | 2 - integration-tests/osgi/build.gradle.kts | 5 +-- sdk-extensions/autoconfigure/build.gradle.kts | 2 - .../declarative-config-jackson/README.md | 10 ++--- .../build.gradle.kts | 5 +-- ...JacksonDeclarativeConfigurationMapper.java | 2 +- ...config.spi.DeclarativeConfigurationMapper} | 0 .../declarative-config-spi/build.gradle.kts | 12 ++++++ .../declarative-config-spi/gradle.properties | 1 + .../spi}/DeclarativeConfigurationMapper.java | 11 ++--- .../DeclarativeConfigurationYamlParser.java | 2 +- sdk-extensions/declarative-config/README.md | 43 ++++++++++++++++++- .../declarative-config/build.gradle.kts | 13 +++--- .../DeclarativeConfiguration.java | 2 + .../DeclarativeConfigurationMapperUtil.java | 2 +- ...akeYamlDeclarativeConfigurationParser.java | 2 +- ...ig.spi.DeclarativeConfigurationYamlParser} | 0 .../DeclarativeConfigurationSpiTest.java | 2 + settings.gradle.kts | 1 + 21 files changed, 89 insertions(+), 39 deletions(-) rename sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/{io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper => io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper} (100%) create mode 100644 sdk-extensions/declarative-config-spi/build.gradle.kts create mode 100644 sdk-extensions/declarative-config-spi/gradle.properties rename sdk-extensions/{declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig => declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi}/DeclarativeConfigurationMapper.java (67%) rename sdk-extensions/{declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig => declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi}/DeclarativeConfigurationYamlParser.java (95%) rename sdk-extensions/declarative-config/src/main/resources/META-INF/services/{io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser => io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index db3fc0e58b6..ef9f61140a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,10 @@ #### Extensions -* Declarative config: Add parser and mapper SPIs for issue - [#8820](https://github.com/open-telemetry/opentelemetry-java/issues/8820), and move the Jackson 2 - mapper to an optional extension. Add the extension to use the built-in Jackson 2 mapper; register - a custom mapper to use another implementation such as Jackson 3. +* Declarative config: Add parser and mapper SPIs in a dedicated artifact for issue + [#8820](https://github.com/open-telemetry/opentelemetry-java/issues/8820). Include the Jackson 2 + mapper by default through a runtime adapter that can be excluded to register a custom mapper such + as one based on Jackson 3. ## Version 1.67.0 (2026-10-09) diff --git a/api/incubator/build.gradle.kts b/api/incubator/build.gradle.kts index e4d57df0456..56046af9eeb 100644 --- a/api/incubator/build.gradle.kts +++ b/api/incubator/build.gradle.kts @@ -21,8 +21,6 @@ dependencies { // To use parsed config file as input for InstrumentationConfigUtilTest testImplementation(project(":sdk-extensions:declarative-config")) - testRuntimeOnly(project(":sdk-extensions:autoconfigure-spi")) - testRuntimeOnly(project(":sdk-extensions:declarative-config-jackson")) testImplementation(project(":sdk:testing")) testImplementation(project(":api:testing-internal")) @@ -38,7 +36,6 @@ testing { dependencies { implementation("com.fasterxml.jackson.core:jackson-databind") implementation(project(":sdk-extensions:declarative-config")) - implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":sdk-extensions:autoconfigure")) implementation("com.google.guava:guava") } diff --git a/integration-tests/graal-incubating/build.gradle.kts b/integration-tests/graal-incubating/build.gradle.kts index 3b666c3cce9..d669cb3b773 100644 --- a/integration-tests/graal-incubating/build.gradle.kts +++ b/integration-tests/graal-incubating/build.gradle.kts @@ -25,9 +25,7 @@ dependencies { implementation(project(":sdk:testing")) implementation(project(":exporters:otlp:all")) implementation(project(":api:incubator")) - implementation(project(":sdk-extensions:autoconfigure-spi")) implementation(project(":sdk-extensions:declarative-config")) - implementation(project(":sdk-extensions:declarative-config-jackson")) } // org.graalvm.buildtools.native plugin requires java 17+ as of version 0.11.0 diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index ebdbf61e8eb..92fccff98da 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -284,8 +284,8 @@ val autoconfigureSuiteTask = registerOsgiSuite( runtimeOnly(project(":exporters:sender:jdk")) } -// Suite: autoconfigure + declarative-config. Same as above but with declarative-config and its -// Jackson mapper adapter on the classpath, additionally exercising their OSGi metadata. +// Suite: autoconfigure + declarative-config. Same as above but with declarative-config on the +// classpath, additionally exercising declarative-config and its Jackson mapper OSGi metadata. val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( "autoconfigureDeclarativeConfig", extraRunrequires = listOf( @@ -303,7 +303,6 @@ val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( implementation(project(":sdk:all")) implementation(project(":sdk-extensions:autoconfigure")) implementation(project(":sdk-extensions:declarative-config")) - implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":exporters:otlp:all")) implementation(project(":extensions:trace-propagators")) runtimeOnly(project(":exporters:sender:jdk")) diff --git a/sdk-extensions/autoconfigure/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts index e600915941f..692bb0c7812 100644 --- a/sdk-extensions/autoconfigure/build.gradle.kts +++ b/sdk-extensions/autoconfigure/build.gradle.kts @@ -107,7 +107,6 @@ testing { register("testIncubating") { dependencies { implementation(project(":sdk-extensions:declarative-config")) - implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":exporters:logging")) implementation(project(":exporters:otlp:all")) implementation(project(":sdk:testing")) @@ -121,7 +120,6 @@ testing { register("testDeclarativeConfigSpi") { dependencies { implementation(project(":sdk-extensions:declarative-config")) - implementation(project(":sdk-extensions:declarative-config-jackson")) implementation(project(":exporters:logging")) implementation(project(":sdk:testing")) } diff --git a/sdk-extensions/declarative-config-jackson/README.md b/sdk-extensions/declarative-config-jackson/README.md index 6d4a22f4371..ffd9da02767 100644 --- a/sdk-extensions/declarative-config-jackson/README.md +++ b/sdk-extensions/declarative-config-jackson/README.md @@ -1,8 +1,8 @@ # Declarative Configuration Jackson Mapper -This optional extension provides a Jackson 2 implementation of the -`DeclarativeConfigurationMapper` SPI for `opentelemetry-sdk-extension-declarative-config`. +This extension provides a Jackson 2 implementation of the `DeclarativeConfigurationMapper` SPI. +It is included as a runtime dependency by `opentelemetry-sdk-extension-declarative-config`. -Add this artifact when using the default Jackson 2 mapper. Projects using another mapper can -exclude this artifact and register their own `DeclarativeConfigurationMapper` provider. For -example, a project using Jackson 3 can implement the SPI with its Jackson 3 `ObjectMapper`. +Projects using another mapper can exclude this artifact from the declarative-config dependency and +register their own `DeclarativeConfigurationMapper` provider. For example, a project using Jackson 3 +can implement the SPI with its Jackson 3 `ObjectMapper`. diff --git a/sdk-extensions/declarative-config-jackson/build.gradle.kts b/sdk-extensions/declarative-config-jackson/build.gradle.kts index ee4fe87ae81..1223b4edd08 100644 --- a/sdk-extensions/declarative-config-jackson/build.gradle.kts +++ b/sdk-extensions/declarative-config-jackson/build.gradle.kts @@ -7,11 +7,10 @@ description = "OpenTelemetry SDK Declarative Config Jackson Mapper" otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson") otelJava.requireSuppressWarningsExplanation.set(false) otelJava.osgiServiceLoaderProvides.set(listOf( - "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper", )) dependencies { - api(project(":sdk-extensions:declarative-config")) - compileOnly(project(":sdk-extensions:autoconfigure-spi")) + api(project(":sdk-extensions:declarative-config-spi")) implementation("com.fasterxml.jackson.core:jackson-databind") } diff --git a/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java b/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java index af51505275f..5995eeb48df 100644 --- a/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java +++ b/sdk-extensions/declarative-config-jackson/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/jackson/internal/JacksonDeclarativeConfigurationMapper.java @@ -6,7 +6,7 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal; import com.fasterxml.jackson.core.type.TypeReference; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper; import java.util.Map; import javax.annotation.Nullable; diff --git a/sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper b/sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper similarity index 100% rename from sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper rename to sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper diff --git a/sdk-extensions/declarative-config-spi/build.gradle.kts b/sdk-extensions/declarative-config-spi/build.gradle.kts new file mode 100644 index 00000000000..90e1027fefd --- /dev/null +++ b/sdk-extensions/declarative-config-spi/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry SDK Declarative Config SPI" +otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi") +otelJava.requireSuppressWarningsExplanation.set(false) + +dependencies { + api(project(":sdk-extensions:autoconfigure-spi")) +} diff --git a/sdk-extensions/declarative-config-spi/gradle.properties b/sdk-extensions/declarative-config-spi/gradle.properties new file mode 100644 index 00000000000..4476ae57e31 --- /dev/null +++ b/sdk-extensions/declarative-config-spi/gradle.properties @@ -0,0 +1 @@ +otel.release=alpha diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationMapper.java similarity index 67% rename from sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java rename to sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationMapper.java index 3273af04b0d..14acb1f3119 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationMapper.java +++ b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationMapper.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.sdk.autoconfigure.declarativeconfig; +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi; import io.opentelemetry.sdk.autoconfigure.spi.Ordered; import java.util.Map; @@ -13,10 +13,11 @@ * Converts parsed configuration values to declarative configuration models and generic maps. * *

Implementations are discovered with {@link java.util.ServiceLoader}. The implementation with - * the highest {@link #order()} is used. The optional {@code - * opentelemetry-sdk-extension-declarative-config-jackson} artifact supplies a Jackson 2 - * implementation. Implementations must preserve the mapping behavior of the declarative - * configuration models, including extension properties and null values. + * the highest {@link #order()} is used. The Jackson 2 implementation is supplied by the {@code + * opentelemetry-sdk-extension-declarative-config-jackson} runtime adapter, which is included by + * default with declarative config and can be excluded when using a custom mapper. Implementations + * must preserve the mapping behavior of the declarative configuration models, including extension + * properties and null values. */ public interface DeclarativeConfigurationMapper extends Ordered { diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationYamlParser.java similarity index 95% rename from sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java rename to sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationYamlParser.java index c6faeb47650..957fe53ed73 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationYamlParser.java +++ b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationYamlParser.java @@ -3,7 +3,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -package io.opentelemetry.sdk.autoconfigure.declarativeconfig; +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi; import io.opentelemetry.sdk.autoconfigure.spi.Ordered; import java.io.InputStream; diff --git a/sdk-extensions/declarative-config/README.md b/sdk-extensions/declarative-config/README.md index 335ec434f7d..5c6f947485c 100644 --- a/sdk-extensions/declarative-config/README.md +++ b/sdk-extensions/declarative-config/README.md @@ -16,7 +16,8 @@ try (FileInputStream yamlConfigFileInputStream = new FileInputStream("/path/to/c Notes: * Environment variable substitution is supported as [defined in the spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/configuration/data-model.md#environment-variable-substitution) * Customization is limited to customizing the in-memory config model, or exporters. See `DeclarativeConfigurationCustomizer` SPI for details. -* YAML parsing and mapping parsed values to configuration models can be replaced with `DeclarativeConfigurationYamlParser` and `DeclarativeConfigurationMapper` SPI implementations. Providers are discovered with `ServiceLoader`; the provider with the highest `order()` is selected. The built-in SnakeYAML parser is included. The Jackson 2 mapper is provided by the optional `opentelemetry-sdk-extension-declarative-config-jackson` artifact. Add that artifact to use the default mapper, or register a mapper provider for another implementation such as Jackson 3. With no mapper provider, operations that need model mapping fail with a `DeclarativeConfigException`. +* YAML parsing and model mapping can be replaced with `DeclarativeConfigurationYamlParser` and `DeclarativeConfigurationMapper` implementations. Both interfaces are in the `opentelemetry-sdk-extension-declarative-config-spi` artifact and are included transitively. Providers are discovered with `ServiceLoader`; the provider with the highest `order()` is selected. +* The built-in SnakeYAML parser and Jackson 2 mapper work by default. The Jackson 2 mapper is supplied by a runtime adapter. If you exclude that adapter, register a mapper provider of your own; otherwise, operations that need mapping fail with a `DeclarativeConfigException`. * Custom SDK extension components which reference the [ComponentProvider](https://github.com/open-telemetry/opentelemetry-java/blob/main/sdk-extensions/autoconfigure-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/spi/internal/ComponentProvider.java) SPI can be referenced in declarative configuration. Supported types include: * `Resource` * `SpanExporter` @@ -27,3 +28,43 @@ Notes: * `TextMapPropagator` * `Sampler` * You can use declarative configuration with [autoconfigure](https://opentelemetry.io/docs/languages/java/configuration/#declarative-configuration) to specify a configuration file via environment variable, e.g. `OTEL_CONFIG_FILE=/path/to/config.yaml`. + +## Use a custom mapper + +To use a mapper such as Jackson 3, exclude the default Jackson 2 adapter from the declarative config +dependency and add Jackson 3 Databind. This Gradle example uses Jackson 3.2.3: + +```kotlin +dependencies { + implementation("io.opentelemetry:opentelemetry-sdk-extension-declarative-config") { + exclude( + group = "io.opentelemetry", + module = "opentelemetry-sdk-extension-declarative-config-jackson", + ) + } + implementation("tools.jackson.core:jackson-databind:3.2.3") +} +``` + +Implement `DeclarativeConfigurationMapper` and override these methods: + +* `order()` returns the provider priority. The provider with the highest value is selected. +* `convert(Object value, Class targetType)` converts parsed configuration values to a model. +* `toMap(Object value)` converts a model or configuration value to a map with string keys. Return + `null` when the input is `null`. + +Configure your mapper to preserve the null and extension-property behavior expected by the models. + +Register the class with `ServiceLoader` by adding this file to your application or mapper module: + +```text +META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper +``` + +Put your provider's fully qualified class name in that file: + +```text +your.package.YourMapper +``` + +With the service file on the runtime classpath, declarative config discovers and uses your mapper. diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index 5a680f2b431..d4a803c0ebe 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -13,22 +13,23 @@ plugins { description = "OpenTelemetry SDK Declarative Config" otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure.declarativeconfig") otelJava.requireSuppressWarningsExplanation.set(false) -otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.autoconfigure.spi")) otelJava.osgiServiceLoaderProvides.set(listOf( "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", - "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser", )) // declarative-config discovers these providers at runtime via ServiceLoader otelJava.osgiServiceLoaderRequires.set(listOf( "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider", - "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser", - "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper", )) dependencies { api(project(":sdk:all")) api(project(":api:incubator")) - compileOnly(project(":sdk-extensions:autoconfigure-spi")) + api(project(":sdk-extensions:autoconfigure-spi")) + api(project(":sdk-extensions:declarative-config-spi")) + runtimeOnly(project(":sdk-extensions:declarative-config-jackson")) // Needed for composable samplers implementation(project(":sdk-extensions:incubator")) @@ -38,8 +39,6 @@ dependencies { implementation("org.snakeyaml:snakeyaml-engine") testImplementation(project(":sdk:testing")) - testImplementation(project(":sdk-extensions:declarative-config-jackson")) - testImplementation(project(":sdk-extensions:autoconfigure-spi")) testImplementation(project(":exporters:logging")) testImplementation(project(":exporters:logging-otlp")) testImplementation(project(":exporters:otlp:all")) diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java index 821136750e4..ce641409f69 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfiguration.java @@ -14,6 +14,8 @@ import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.SnakeYamlDeclarativeConfigurationParser; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.OpenTelemetryConfigurationModel; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.SamplerModel; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser; import io.opentelemetry.sdk.autoconfigure.spi.Ordered; import io.opentelemetry.sdk.autoconfigure.spi.internal.AutoConfigureListener; import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java index 0dfd1129e88..375c593700b 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/DeclarativeConfigurationMapperUtil.java @@ -6,7 +6,7 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal; import io.opentelemetry.api.incubator.config.DeclarativeConfigException; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationMapper; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper; import java.util.Map; import javax.annotation.Nullable; diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java index 0a947c707af..527d3eadff3 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/internal/SnakeYamlDeclarativeConfigurationParser.java @@ -5,7 +5,7 @@ package io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal; -import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser; import java.io.InputStream; import java.util.Map; import java.util.Objects; diff --git a/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser b/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser similarity index 100% rename from sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationYamlParser rename to sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser diff --git a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java index 32d0679399a..a69ca0a4be9 100644 --- a/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java @@ -11,6 +11,8 @@ import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.DeclarativeConfigProviderLoader; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.internal.SnakeYamlDeclarativeConfigurationParser; import io.opentelemetry.sdk.autoconfigure.declarativeconfig.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationMapper; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.DeclarativeConfigurationYamlParser; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; import java.util.Collections; diff --git a/settings.gradle.kts b/settings.gradle.kts index f55eeff5351..fa9165b25f4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -70,6 +70,7 @@ include(":sdk-extensions:autoconfigure") include(":sdk-extensions:autoconfigure-spi") include(":sdk-extensions:declarative-config") include(":sdk-extensions:declarative-config-jackson") +include(":sdk-extensions:declarative-config-spi") include(":sdk-extensions:incubator") include(":sdk-extensions:jaeger-remote-sampler") include(":testing-internal")