Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4559fdf
Add InstrumentationDefaults helper to declarative-config-bridge
zeitlinger Apr 10, 2026
0572994
fix: add @CanIgnoreReturnValue to InstrumentationDefaults.setDefault
zeitlinger Apr 13, 2026
deef7d8
fix: add @CanIgnoreReturnValue to applyToModel
zeitlinger Apr 13, 2026
97ab9a4
docs: add InstrumentationDefaults usage to declarative-config-bridge …
zeitlinger Apr 13, 2026
1e43af3
docs(declarative-config-bridge): restructure InstrumentationDefaults …
zeitlinger Apr 17, 2026
425d9c2
refactor(declarative-config-bridge): nest InstrumentationDefaults API
zeitlinger Apr 18, 2026
ea1616d
docs(declarative-config-bridge): use get instead of getStructured in …
zeitlinger Apr 18, 2026
a370c6a
Merge branch 'main' into instrumentation-defaults
trask May 4, 2026
a7d2e54
fix: mirror development key translation in InstrumentationDefaults
zeitlinger May 4, 2026
0f27445
test: use parameterized InstrumentationDefaults translation cases
zeitlinger May 4, 2026
5b0f8fc
test: add bridge roundtrip coverage for InstrumentationDefaults
zeitlinger May 4, 2026
fdadd88
Use HashMap for InstrumentationDefaults storage; iteration order is n…
zeitlinger May 8, 2026
ef6c451
fix: isolate declarative model support in defaults helper
zeitlinger May 12, 2026
93b37ee
fix: export incubator model dependency from config bridge
zeitlinger May 12, 2026
b4a4100
Merge remote-tracking branch 'origin/main' into lane-pr17816-fix
zeitlinger May 12, 2026
256f42b
fix: align config bridge defaults helper with declarative config API
zeitlinger May 12, 2026
57a940b
Preserve typed instrumentation defaults in declarative bridge
zeitlinger Jul 14, 2026
622aa8e
Merge remote-tracking branch 'origin/main' into instrumentation-defaults
zeitlinger Jul 14, 2026
bd0483d
Align declarative bridge applier with current SDK model packages
zeitlinger Jul 14, 2026
3dfcb3c
format
zeitlinger Jul 14, 2026
0eb826d
Add instance applyToModel convenience
zeitlinger Jul 15, 2026
a6342ac
Apply spotless formatting
zeitlinger Jul 15, 2026
dbcae20
make package protected
zeitlinger Jul 16, 2026
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
63 changes: 63 additions & 0 deletions declarative-config-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,66 @@ public class InferredSpansComponentProvider implements ComponentProvider {
}
}
```

Comment thread
zeitlinger marked this conversation as resolved.
## InstrumentationDefaults

`InstrumentationDefaults` lets distribution authors define instrumentation property defaults once
and have them work in both configuration modes.
First, there is a single defaults object that is unaware of the source of the configuration:

```java
InstrumentationDefaults defaults = new InstrumentationDefaults();
defaults.get("micrometer").setDefault("base_time_unit", "s");
defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
```

Navigation mirrors `DeclarativeConfigProperties` — reading uses
`config.get("micrometer").getString("base_time_unit")`; writing defaults uses
`defaults.get("micrometer").setDefault("base_time_unit", "s")`.

Keys use the same declarative config shape as `DeclarativeConfigProperties`. When producing system
property keys, underscores are translated to hyphens, and keys ending in `/development` are
translated using the bridge's `experimental.` convention.

The auto configuration **without declarative config** registers the defaults as a properties
supplier, translating them to `otel.instrumentation.*` keys:

```java
@AutoService(AutoConfigurationCustomizerProvider.class)
public class MyDistroAutoConfig implements AutoConfigurationCustomizerProvider {
@Override
public void customize(AutoConfigurationCustomizer autoConfiguration) {
autoConfiguration.addPropertiesSupplier(defaults::toConfigProperties);
}
}
Comment thread
zeitlinger marked this conversation as resolved.
```

The auto configuration **with declarative config** registers the defaults as a model customizer,
injecting them under `instrumentation/development.java`.

Let's first look at the yaml file that the defaults effectively merge into:

```yaml
file_format: 1.0
instrumentation/development:
java:
micrometer:
base_time_unit: s
log4j_appender:
experimental_log_attributes/development: "true"
```

And now the customizer that applies the defaults to the model:

```java
@AutoService(DeclarativeConfigurationCustomizerProvider.class)
public class MyDistroDeclarativeConfig implements DeclarativeConfigurationCustomizerProvider {
@Override
public void customize(DeclarativeConfigurationCustomizer customizer) {
customizer.addModelCustomizer(model -> defaults.applyToModel(model));
}
}
```

Explicit user configuration always takes precedence — defaults are only applied for properties not
already present (`putIfAbsent`).
1 change: 1 addition & 0 deletions declarative-config-bridge/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ group = "io.opentelemetry.instrumentation"

dependencies {
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
Comment thread
zeitlinger marked this conversation as resolved.
Outdated
implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
implementation("io.opentelemetry:opentelemetry-api-incubator")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.config.bridge;

import com.google.errorprone.annotations.CanIgnoreReturnValue;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalInstrumentationModel;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalLanguageSpecificInstrumentationModel;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalLanguageSpecificInstrumentationPropertyModel;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel;
import java.util.HashMap;
import java.util.Map;

/**
* Defines instrumentation defaults that work with both traditional property-based configuration and
* declarative configuration.
*
* <p>Navigation mirrors {@link io.opentelemetry.api.incubator.config.DeclarativeConfigProperties}:
* read-side uses {@code config.get(name).getString(key)}; write-side uses {@code
* defaults.get(name).setDefault(key, value)}.
*
* <p>Usage:
*
* <pre>{@code
* InstrumentationDefaults defaults = new InstrumentationDefaults();
* defaults.get("micrometer").setDefault("base_time_unit", "s");
* defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
*
* // Declarative config mode: inject into model
* customizer.addModelCustomizer(model -> defaults.applyToModel(model));
*
* // Traditional mode: translate to ConfigProperties
* autoConfiguration.addPropertiesSupplier(defaults::toConfigProperties);
* }</pre>
*/
public final class InstrumentationDefaults {

private final Map<String, InstrumentationProperties> instrumentations = new HashMap<>();

/**
* Returns the defaults builder for the given instrumentation, creating it if absent. Mirrors
Comment thread
zeitlinger marked this conversation as resolved.
Outdated
* {@code DeclarativeConfigProperties.get(name)} on the read side.
*/
public InstrumentationProperties get(String instrumentation) {
return instrumentations.computeIfAbsent(instrumentation, k -> new InstrumentationProperties());
}

/** Translates defaults to {@code otel.instrumentation.*} keys for auto-configuration. */
public Map<String, String> toConfigProperties() {
HashMap<String, String> map = new HashMap<>();
instrumentations.forEach(
(instrumentation, properties) ->
properties.properties.forEach(
Comment thread
zeitlinger marked this conversation as resolved.
Outdated
(key, value) ->
map.put(
"otel.instrumentation."
Comment thread
zeitlinger marked this conversation as resolved.
Outdated
+ translateName(instrumentation)
+ "."
+ translateName(key),
value)));
return map;
}

/**
* Applies defaults to the declarative configuration model under {@code
* instrumentation/development.java}. Existing values in the model take precedence; defaults are
* only set for properties not already present.
*/
@CanIgnoreReturnValue
public OpenTelemetryConfigurationModel applyToModel(OpenTelemetryConfigurationModel model) {
if (instrumentations.isEmpty()) {
return model;
}
Comment thread
zeitlinger marked this conversation as resolved.
Outdated

ExperimentalInstrumentationModel instrumentation = model.getInstrumentationDevelopment();
if (instrumentation == null) {
instrumentation = new ExperimentalInstrumentationModel();
model.withInstrumentationDevelopment(instrumentation);
}
ExperimentalLanguageSpecificInstrumentationModel java = instrumentation.getJava();
if (java == null) {
java = new ExperimentalLanguageSpecificInstrumentationModel();
instrumentation.withJava(java);
}

Map<String, ExperimentalLanguageSpecificInstrumentationPropertyModel> props =
java.getAdditionalProperties();

for (Map.Entry<String, InstrumentationProperties> entry : instrumentations.entrySet()) {
String name = entry.getKey();
Map<String, String> defaults = entry.getValue().properties;

ExperimentalLanguageSpecificInstrumentationPropertyModel propModel = props.get(name);
if (propModel == null) {
propModel = new ExperimentalLanguageSpecificInstrumentationPropertyModel();
props.put(name, propModel);
}

for (Map.Entry<String, String> defaultEntry : defaults.entrySet()) {
propModel
.getAdditionalProperties()
.putIfAbsent(defaultEntry.getKey(), defaultEntry.getValue());
}
}

return model;
}

/** Defaults for a single instrumentation. Keys use underscore notation. */
public static final class InstrumentationProperties {
Comment thread
zeitlinger marked this conversation as resolved.
Outdated

private final Map<String, String> properties = new HashMap<>();

private InstrumentationProperties() {}

/**
* Sets a default value for a property. Keys use underscore notation (e.g. {@code
* base_time_unit}); they are translated to hyphen notation when producing {@code
* otel.instrumentation.*} keys. Keys ending in {@code /development} follow the same {@code
* experimental.} translation as {@link ConfigPropertiesBackedDeclarativeConfigProperties}.
*
* @return {@code this} for chaining
*/
@CanIgnoreReturnValue
public InstrumentationProperties setDefault(String key, String value) {
properties.put(key, value);
return this;
}
}

private static String translateName(String name) {
if (name.endsWith("/development")) {
name = name.substring(0, name.length() - "/development".length());
if (!name.contains("experimental")) {
name = "experimental." + name;
}
}
return name.replace('_', '-');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.config.bridge;

import static org.assertj.core.api.Assertions.assertThat;

import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.internal.DefaultConfigProperties;
import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;

class InstrumentationDefaultsTest {

private static Stream<Arguments> configPropertyDefaults() {
return Stream.of(
Arguments.of(
"micrometer", "base_time_unit", "s", "otel.instrumentation.micrometer.base-time-unit"),
Arguments.of(
"log4j_appender",
"experimental_log_attributes/development",
"true",
"otel.instrumentation.log4j-appender.experimental-log-attributes"),
Arguments.of(
"spring_scheduling",
"controller_telemetry/development",
"false",
"otel.instrumentation.spring-scheduling.experimental.controller-telemetry"),
Arguments.of(
"grpc",
"experimental_span_attributes/development",
"true",
"otel.instrumentation.grpc.experimental-span-attributes"));
}

@ParameterizedTest
@MethodSource("configPropertyDefaults")
void toConfigProperties(
String instrumentation, String key, String value, String expectedPropertyKey) {
InstrumentationDefaults defaults = new InstrumentationDefaults();
defaults.get(instrumentation).setDefault(key, value);

Map<String, String> props = defaults.toConfigProperties();

assertThat(props).containsEntry(expectedPropertyKey, value).hasSize(1);
}

@ParameterizedTest
@MethodSource("configPropertyDefaults")
void toConfigPropertiesRoundTripsThroughBridge(
String instrumentation, String key, String value, String expectedPropertyKey) {
InstrumentationDefaults defaults = new InstrumentationDefaults();
defaults.get(instrumentation).setDefault(key, value);

DeclarativeConfigProperties config =
ConfigPropertiesBackedDeclarativeConfigProperties.createInstrumentationConfig(
DefaultConfigProperties.createFromMap(defaults.toConfigProperties()));

assertThat(config.getStructured("java").getStructured(instrumentation).getString(key))
.isEqualTo(value);
}

@Test
void applyToModel() {
InstrumentationDefaults defaults = new InstrumentationDefaults();
defaults.get("micrometer").setDefault("base_time_unit", "s");
defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");

OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel();
defaults.applyToModel(model);

assertThat(
model
.getInstrumentationDevelopment()
.getJava()
.getAdditionalProperties()
.get("micrometer")
.getAdditionalProperties())
.containsEntry("base_time_unit", "s");
assertThat(
model
.getInstrumentationDevelopment()
.getJava()
.getAdditionalProperties()
.get("log4j_appender")
.getAdditionalProperties())
.containsEntry("experimental_log_attributes/development", "true");
}

@Test
void applyToModelDoesNotOverrideExisting() {
// Pre-populate model with a different value
OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel();
InstrumentationDefaults seed = new InstrumentationDefaults();
seed.get("micrometer").setDefault("base_time_unit", "ms");
seed.applyToModel(model);

// Apply a conflicting default — should not override
InstrumentationDefaults defaults = new InstrumentationDefaults();
defaults.get("micrometer").setDefault("base_time_unit", "s");
defaults.applyToModel(model);

assertThat(
model
.getInstrumentationDevelopment()
.getJava()
.getAdditionalProperties()
.get("micrometer")
.getAdditionalProperties())
.containsEntry("base_time_unit", "ms");
}
}
Loading