diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ebd56e3531..ef9f61140a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### SDK + +#### Extensions + +* 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) ### API diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 9a4dac688f0..92fccff98da 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -285,7 +285,7 @@ val autoconfigureSuiteTask = registerOsgiSuite( } // Suite: autoconfigure + declarative-config. Same as above but with declarative-config on the -// classpath, additionally exercising declarative-config bundle OSGi metadata. +// classpath, additionally exercising declarative-config and its Jackson mapper 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", diff --git a/sdk-extensions/declarative-config-jackson/README.md b/sdk-extensions/declarative-config-jackson/README.md new file mode 100644 index 00000000000..ffd9da02767 --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/README.md @@ -0,0 +1,8 @@ +# Declarative Configuration Jackson Mapper + +This extension provides a Jackson 2 implementation of the `DeclarativeConfigurationMapper` SPI. +It is included as a runtime dependency by `opentelemetry-sdk-extension-declarative-config`. + +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 new file mode 100644 index 00000000000..1223b4edd08 --- /dev/null +++ b/sdk-extensions/declarative-config-jackson/build.gradle.kts @@ -0,0 +1,16 @@ +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.spi.DeclarativeConfigurationMapper", +)) + +dependencies { + api(project(":sdk-extensions:declarative-config-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..5995eeb48df --- /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.spi.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.spi.DeclarativeConfigurationMapper b/sdk-extensions/declarative-config-jackson/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.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.spi.DeclarativeConfigurationMapper @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal.JacksonDeclarativeConfigurationMapper 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-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationMapper.java b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationMapper.java new file mode 100644 index 00000000000..14acb1f3119 --- /dev/null +++ b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationMapper.java @@ -0,0 +1,42 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi; + +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 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 { + + /** + * 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-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationYamlParser.java b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationYamlParser.java new file mode 100644 index 00000000000..957fe53ed73 --- /dev/null +++ b/sdk-extensions/declarative-config-spi/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/spi/DeclarativeConfigurationYamlParser.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi; + +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/README.md b/sdk-extensions/declarative-config/README.md index 99ed7553c00..5c6f947485c 100644 --- a/sdk-extensions/declarative-config/README.md +++ b/sdk-extensions/declarative-config/README.md @@ -16,6 +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 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` @@ -26,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 2390d633d1b..d4a803c0ebe 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -13,19 +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.spi.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.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")) @@ -33,10 +37,8 @@ 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:autoconfigure-spi")) testImplementation(project(":exporters:logging")) testImplementation(project(":exporters:logging-otlp")) testImplementation(project(":exporters:otlp:all")) @@ -46,6 +48,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 +111,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..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 @@ -5,15 +5,17 @@ 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.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; @@ -26,23 +28,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 +133,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 +143,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 +169,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 +190,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 +231,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/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..375c593700b --- /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.spi.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..527d3eadff3 --- /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.spi.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.spi.DeclarativeConfigurationYamlParser b/sdk-extensions/declarative-config/src/main/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.spi.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.spi.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..a69ca0a4be9 --- /dev/null +++ b/sdk-extensions/declarative-config/src/test/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigurationSpiTest.java @@ -0,0 +1,105 @@ +/* + * 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 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; +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..fa9165b25f4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -69,6 +69,8 @@ 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:declarative-config-spi") include(":sdk-extensions:incubator") include(":sdk-extensions:jaeger-remote-sampler") include(":testing-internal")