Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for changelog entries. These are generated as part of release notes.


## Version 1.67.0 (2026-10-09)

### API
Expand Down
3 changes: 2 additions & 1 deletion integration-tests/osgi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -285,14 +285,15 @@ 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(
"opentelemetry-exporter-sender-jdk",
"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",
Expand Down
8 changes: 8 additions & 0 deletions sdk-extensions/declarative-config-jackson/README.md
Original file line number Diff line number Diff line change
@@ -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`.
16 changes: 16 additions & 0 deletions sdk-extensions/declarative-config-jackson/build.gradle.kts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to include the jackson major version number in the module name. I.e. declarative-config-jackson2. We learned the hard was with okhttp sender that people have a real problem with major version updates with these kind of load bearing dependencies. Embedding the major version gives us flexibility to maintain two versions at the same time to keep everyone happy. Can of course start without the version in the name and rename to declarative-config-jackson3 if/when needed, but its a nice visual queue to start with.

Also put the major version in the package name, i.e. io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson2.

Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
id("otel.java-conventions")
id("otel.publish-conventions")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
id("otel.publish-conventions")
id("otel.publish-conventions")
id("otel.animalsniffer-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")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
otel.release=alpha
Original file line number Diff line number Diff line change
@@ -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> T convert(Object value, Class<T> targetType) {
return JacksonModelMapper.MAPPER.convertValue(value, targetType);
}

@Nullable
@Override
public Map<String, Object> toMap(Object value) {
return JacksonModelMapper.MAPPER.convertValue(
value, new TypeReference<Map<String, Object>>() {});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
io.opentelemetry.sdk.autoconfigure.declarativeconfig.jackson.internal.JacksonDeclarativeConfigurationMapper
12 changes: 12 additions & 0 deletions sdk-extensions/declarative-config-spi/build.gradle.kts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey let's put these in sdk-extensions/autoconfigure-spi in the io.opentelemetry.sdk.autoconfigure.spi.internal package. That's where ComponentProvider lives, which is another key SPI for declarative config. In #8981 we're promoting ComponentProvider to be stable and it will live in io.opentelemetry.sdk.autoconfigure.spi.declarativeconfig as its permanent home.

Original file line number Diff line number Diff line change
@@ -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"))
}
1 change: 1 addition & 0 deletions sdk-extensions/declarative-config-spi/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
otel.release=alpha
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typical pattern we use is:

  • If exactly one is on the class path, use it
  • If two are on the class path, and the user hasn't selected one via env var / system property, log a warning and use the first we find
  • If two are on the class path, and the user has selected one via env var / system property, use it

See https://github.com/open-telemetry/opentelemetry-java/blob/main/exporters/common/src/main/java/io/opentelemetry/exporter/internal/SenderUtil.java

* 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 <T> the target type
* @return the converted value
*/
<T> T convert(Object value, Class<T> 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<String, Object> toMap(Object value);
}
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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.
*
* <p>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<String, String> environmentVariables,
Map<Object, Object> systemProperties);
}
42 changes: 42 additions & 0 deletions sdk-extensions/declarative-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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<T> 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.
17 changes: 10 additions & 7 deletions sdk-extensions/declarative-config/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -13,30 +13,32 @@ 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"))

annotationProcessor("com.google.auto.value:auto-value")

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"))
Expand 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")
Expand Down Expand Up @@ -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(") }
Expand Down
Loading
Loading