From 4559fdf00c161085de08b62f8b6dc082f34c12ff Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 10 Apr 2026 17:56:39 +0000 Subject: [PATCH 01/14] Add InstrumentationDefaults helper to declarative-config-bridge Provides a utility for distributions to define instrumentation property defaults once and have them work with both traditional property-based configuration (otel.instrumentation.*) and declarative configuration (YAML model under instrumentation/development.java). Extracted from grafana/grafana-opentelemetry-java#1226. Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/build.gradle.kts | 1 + .../bridge/InstrumentationDefaults.java | 108 ++++++++++++++++++ .../bridge/InstrumentationDefaultsTest.java | 71 ++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java create mode 100644 declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java diff --git a/declarative-config-bridge/build.gradle.kts b/declarative-config-bridge/build.gradle.kts index b5ac9ec9c1c3..7da52f11f4fd 100644 --- a/declarative-config-bridge/build.gradle.kts +++ b/declarative-config-bridge/build.gradle.kts @@ -8,6 +8,7 @@ group = "io.opentelemetry.instrumentation" dependencies { compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") implementation("io.opentelemetry:opentelemetry-api-incubator") diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java new file mode 100644 index 000000000000..0153a03cb0dd --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.config.bridge; + +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.LinkedHashMap; +import java.util.Map; + +/** + * Defines instrumentation defaults that work with both traditional property-based configuration and + * declarative configuration. + * + *

Usage: + * + *

{@code
+ * InstrumentationDefaults defaults = new InstrumentationDefaults();
+ * defaults.setDefault("micrometer", "base_time_unit", "s");
+ * defaults.setDefault("log4j_appender", "experimental_log_attributes", "true");
+ *
+ * // Declarative config mode: inject into model
+ * customizer.addModelCustomizer(model -> defaults.applyToModel(model));
+ *
+ * // Traditional mode: translate to ConfigProperties
+ * autoConfiguration.addPropertiesSupplier(defaults::toConfigProperties);
+ * }
+ */ +public final class InstrumentationDefaults { + + private final Map> instrumentations = new LinkedHashMap<>(); + + /** + * Sets a default value for an instrumentation property. Keys use underscore notation (e.g. {@code + * base_time_unit}); they are translated to hyphen notation when producing property keys. + * + * @return {@code this} for chaining + */ + public InstrumentationDefaults setDefault(String instrumentation, String key, String value) { + instrumentations.computeIfAbsent(instrumentation, k -> new LinkedHashMap<>()).put(key, value); + return this; + } + + /** Translates defaults to {@code otel.instrumentation.*} keys for auto-configuration. */ + public Map toConfigProperties() { + HashMap map = new HashMap<>(); + instrumentations.forEach( + (instrumentation, properties) -> + properties.forEach( + (key, value) -> + map.put( + "otel.instrumentation." + + instrumentation.replace('_', '-') + + "." + + key.replace('_', '-'), + 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. + */ + public OpenTelemetryConfigurationModel applyToModel(OpenTelemetryConfigurationModel model) { + if (instrumentations.isEmpty()) { + return model; + } + + 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 props = + java.getAdditionalProperties(); + + for (Map.Entry> entry : instrumentations.entrySet()) { + String name = entry.getKey(); + Map defaults = entry.getValue(); + + ExperimentalLanguageSpecificInstrumentationPropertyModel propModel = props.get(name); + if (propModel == null) { + propModel = new ExperimentalLanguageSpecificInstrumentationPropertyModel(); + props.put(name, propModel); + } + + for (Map.Entry defaultEntry : defaults.entrySet()) { + propModel + .getAdditionalProperties() + .putIfAbsent(defaultEntry.getKey(), defaultEntry.getValue()); + } + } + + return model; + } +} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java new file mode 100644 index 000000000000..7e89f17aafab --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java @@ -0,0 +1,71 @@ +/* + * 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.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import java.util.Map; +import org.junit.jupiter.api.Test; + +class InstrumentationDefaultsTest { + + @Test + void toConfigProperties() { + InstrumentationDefaults defaults = new InstrumentationDefaults(); + defaults.setDefault("micrometer", "base_time_unit", "s"); + defaults.setDefault("log4j_appender", "experimental_log_attributes", "true"); + + Map props = defaults.toConfigProperties(); + + assertThat(props) + .containsEntry("otel.instrumentation.micrometer.base-time-unit", "s") + .containsEntry("otel.instrumentation.log4j-appender.experimental-log-attributes", "true") + .hasSize(2); + } + + @Test + void applyToModel() { + InstrumentationDefaults defaults = new InstrumentationDefaults(); + defaults.setDefault("micrometer", "base_time_unit", "s"); + + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + defaults.applyToModel(model); + + assertThat( + model + .getInstrumentationDevelopment() + .getJava() + .getAdditionalProperties() + .get("micrometer") + .getAdditionalProperties()) + .containsEntry("base_time_unit", "s"); + } + + @Test + void applyToModelDoesNotOverrideExisting() { + InstrumentationDefaults defaults = new InstrumentationDefaults(); + defaults.setDefault("micrometer", "base_time_unit", "s"); + + // Pre-populate model with a different value + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + new InstrumentationDefaults() + .setDefault("micrometer", "base_time_unit", "ms") + .applyToModel(model); + + // Apply defaults — should not override + defaults.applyToModel(model); + + assertThat( + model + .getInstrumentationDevelopment() + .getJava() + .getAdditionalProperties() + .get("micrometer") + .getAdditionalProperties()) + .containsEntry("base_time_unit", "ms"); + } +} From 0572994339b673ad4f48fe48558e54a18c3709bc Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 13 Apr 2026 13:45:28 +0000 Subject: [PATCH 02/14] fix: add @CanIgnoreReturnValue to InstrumentationDefaults.setDefault Signed-off-by: Gregor Zeitlinger --- .../instrumentation/config/bridge/InstrumentationDefaults.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java index 0153a03cb0dd..bdb0ce02b01f 100644 --- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java @@ -5,6 +5,7 @@ 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; @@ -41,6 +42,7 @@ public final class InstrumentationDefaults { * * @return {@code this} for chaining */ + @CanIgnoreReturnValue public InstrumentationDefaults setDefault(String instrumentation, String key, String value) { instrumentations.computeIfAbsent(instrumentation, k -> new LinkedHashMap<>()).put(key, value); return this; From deef7d881e5c27a14ca40765e32e6871e2e79389 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 13 Apr 2026 14:49:18 +0000 Subject: [PATCH 03/14] fix: add @CanIgnoreReturnValue to applyToModel Signed-off-by: Gregor Zeitlinger --- .../instrumentation/config/bridge/InstrumentationDefaults.java | 1 + 1 file changed, 1 insertion(+) diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java index bdb0ce02b01f..9f5e70811482 100644 --- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java @@ -69,6 +69,7 @@ public Map toConfigProperties() { * 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; From 97ab9a4c917586710aecae5c67b612e42c2716f3 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 13 Apr 2026 17:32:15 +0000 Subject: [PATCH 04/14] docs: add InstrumentationDefaults usage to declarative-config-bridge README Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/README.md | 49 +++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md index 0e6b7154bb15..c6210764ffff 100644 --- a/declarative-config-bridge/README.md +++ b/declarative-config-bridge/README.md @@ -82,3 +82,52 @@ public class InferredSpansComponentProvider implements ComponentProvider { } } ``` + +## InstrumentationDefaults + +`InstrumentationDefaults` lets distribution authors define instrumentation property defaults once +and have them work in both configuration modes — traditional auto-configuration and declarative +configuration. + +### Usage + +```java +InstrumentationDefaults defaults = new InstrumentationDefaults(); +defaults.setDefault("micrometer", "base_time_unit", "s"); +defaults.setDefault("log4j_appender", "experimental_log_attributes", "true"); +``` + +Keys use underscore notation (matching the declarative config model). They are translated to +hyphen notation (`otel.instrumentation..`) when producing system property keys. + +### Auto-configuration (traditional) + +Register the defaults as a properties supplier: + +```java +@AutoService(AutoConfigurationCustomizerProvider.class) +public class MyDistroAutoConfig implements AutoConfigurationCustomizerProvider { + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + autoConfiguration.addPropertiesSupplier(defaults::toConfigProperties); + } +} +``` + +### Declarative configuration + +Register the defaults as a model customizer: + +```java +@AutoService(DeclarativeConfigurationCustomizerProvider.class) +public class MyDistroDeclarativeConfig implements DeclarativeConfigurationCustomizerProvider { + @Override + public void customize(DeclarativeConfigurationCustomizer customizer) { + customizer.addModelCustomizer(model -> defaults.applyToModel(model)); + } +} +``` + +Defaults are injected under `instrumentation/development.java` in the model. Explicit user +configuration always takes precedence — defaults are only applied for properties not already +present. From 1e43af3dabaf200a98e242e825bca33c86b91ba4 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Fri, 17 Apr 2026 12:48:22 +0000 Subject: [PATCH 05/14] docs(declarative-config-bridge): restructure InstrumentationDefaults readme to match DC API pattern Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/README.md | 33 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md index c6210764ffff..a70ec933dcc7 100644 --- a/declarative-config-bridge/README.md +++ b/declarative-config-bridge/README.md @@ -86,10 +86,8 @@ public class InferredSpansComponentProvider implements ComponentProvider { ## InstrumentationDefaults `InstrumentationDefaults` lets distribution authors define instrumentation property defaults once -and have them work in both configuration modes — traditional auto-configuration and declarative -configuration. - -### Usage +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(); @@ -100,9 +98,8 @@ defaults.setDefault("log4j_appender", "experimental_log_attributes", "true"); Keys use underscore notation (matching the declarative config model). They are translated to hyphen notation (`otel.instrumentation..`) when producing system property keys. -### Auto-configuration (traditional) - -Register the defaults as a properties supplier: +The auto configuration **without declarative config** registers the defaults as a properties +supplier, translating them to `otel.instrumentation.*` keys: ```java @AutoService(AutoConfigurationCustomizerProvider.class) @@ -114,9 +111,22 @@ public class MyDistroAutoConfig implements AutoConfigurationCustomizerProvider { } ``` -### Declarative configuration +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: "true" +``` -Register the defaults as a model customizer: +And now the customizer that applies the defaults to the model: ```java @AutoService(DeclarativeConfigurationCustomizerProvider.class) @@ -128,6 +138,5 @@ public class MyDistroDeclarativeConfig implements DeclarativeConfigurationCustom } ``` -Defaults are injected under `instrumentation/development.java` in the model. Explicit user -configuration always takes precedence — defaults are only applied for properties not already -present. +Explicit user configuration always takes precedence — defaults are only applied for properties not +already present (`putIfAbsent`). From 425d9c20c9fa27b43ad090d3490eea0f0b234531 Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Sat, 18 Apr 2026 08:27:34 +0000 Subject: [PATCH 06/14] refactor(declarative-config-bridge): nest InstrumentationDefaults API Replace vararg setDefault(instr, key, value) with nested defaults.get(name).setDefault(key, value). Mirrors DeclarativeConfigProperties.getStructured(name).getString(key) on the read side. README notes the symmetry. Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/README.md | 8 ++- .../bridge/InstrumentationDefaults.java | 49 +++++++++++++------ .../bridge/InstrumentationDefaultsTest.java | 19 ++++--- 3 files changed, 50 insertions(+), 26 deletions(-) diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md index a70ec933dcc7..973f89961808 100644 --- a/declarative-config-bridge/README.md +++ b/declarative-config-bridge/README.md @@ -91,10 +91,14 @@ First, there is a single defaults object that is unaware of the source of the co ```java InstrumentationDefaults defaults = new InstrumentationDefaults(); -defaults.setDefault("micrometer", "base_time_unit", "s"); -defaults.setDefault("log4j_appender", "experimental_log_attributes", "true"); +defaults.get("micrometer").setDefault("base_time_unit", "s"); +defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true"); ``` +Navigation mirrors `DeclarativeConfigProperties` — reading uses +`config.getStructured("micrometer").getString("base_time_unit")`; writing defaults uses +`defaults.get("micrometer").setDefault("base_time_unit", "s")`. + Keys use underscore notation (matching the declarative config model). They are translated to hyphen notation (`otel.instrumentation..`) when producing system property keys. diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java index 9f5e70811482..516d44fc602c 100644 --- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java @@ -18,12 +18,16 @@ * Defines instrumentation defaults that work with both traditional property-based configuration and * declarative configuration. * + *

Navigation mirrors {@link io.opentelemetry.api.incubator.config.DeclarativeConfigProperties}: + * read-side uses {@code config.getStructured(name).getString(key)}; write-side uses {@code + * defaults.get(name).setDefault(key, value)}. + * *

Usage: * *

{@code
  * InstrumentationDefaults defaults = new InstrumentationDefaults();
- * defaults.setDefault("micrometer", "base_time_unit", "s");
- * defaults.setDefault("log4j_appender", "experimental_log_attributes", "true");
+ * defaults.get("micrometer").setDefault("base_time_unit", "s");
+ * defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true");
  *
  * // Declarative config mode: inject into model
  * customizer.addModelCustomizer(model -> defaults.applyToModel(model));
@@ -34,18 +38,14 @@
  */
 public final class InstrumentationDefaults {
 
-  private final Map> instrumentations = new LinkedHashMap<>();
+  private final Map instrumentations = new LinkedHashMap<>();
 
   /**
-   * Sets a default value for an instrumentation property. Keys use underscore notation (e.g. {@code
-   * base_time_unit}); they are translated to hyphen notation when producing property keys.
-   *
-   * @return {@code this} for chaining
+   * Returns the defaults builder for the given instrumentation, creating it if absent. Mirrors
+   * {@code DeclarativeConfigProperties.getStructured(name)} on the read side.
    */
-  @CanIgnoreReturnValue
-  public InstrumentationDefaults setDefault(String instrumentation, String key, String value) {
-    instrumentations.computeIfAbsent(instrumentation, k -> new LinkedHashMap<>()).put(key, value);
-    return this;
+  public InstrumentationProperties get(String instrumentation) {
+    return instrumentations.computeIfAbsent(instrumentation, k -> new InstrumentationProperties());
   }
 
   /** Translates defaults to {@code otel.instrumentation.*} keys for auto-configuration. */
@@ -53,7 +53,7 @@ public Map toConfigProperties() {
     HashMap map = new HashMap<>();
     instrumentations.forEach(
         (instrumentation, properties) ->
-            properties.forEach(
+            properties.properties.forEach(
                 (key, value) ->
                     map.put(
                         "otel.instrumentation."
@@ -89,9 +89,9 @@ public OpenTelemetryConfigurationModel applyToModel(OpenTelemetryConfigurationMo
     Map props =
         java.getAdditionalProperties();
 
-    for (Map.Entry> entry : instrumentations.entrySet()) {
+    for (Map.Entry entry : instrumentations.entrySet()) {
       String name = entry.getKey();
-      Map defaults = entry.getValue();
+      Map defaults = entry.getValue().properties;
 
       ExperimentalLanguageSpecificInstrumentationPropertyModel propModel = props.get(name);
       if (propModel == null) {
@@ -108,4 +108,25 @@ public OpenTelemetryConfigurationModel applyToModel(OpenTelemetryConfigurationMo
 
     return model;
   }
+
+  /** Defaults for a single instrumentation. Keys use underscore notation. */
+  public static final class InstrumentationProperties {
+
+    private final Map properties = new LinkedHashMap<>();
+
+    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.
+     *
+     * @return {@code this} for chaining
+     */
+    @CanIgnoreReturnValue
+    public InstrumentationProperties setDefault(String key, String value) {
+      properties.put(key, value);
+      return this;
+    }
+  }
 }
diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
index 7e89f17aafab..cc5efceef3a0 100644
--- a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
@@ -16,8 +16,8 @@ class InstrumentationDefaultsTest {
   @Test
   void toConfigProperties() {
     InstrumentationDefaults defaults = new InstrumentationDefaults();
-    defaults.setDefault("micrometer", "base_time_unit", "s");
-    defaults.setDefault("log4j_appender", "experimental_log_attributes", "true");
+    defaults.get("micrometer").setDefault("base_time_unit", "s");
+    defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true");
 
     Map props = defaults.toConfigProperties();
 
@@ -30,7 +30,7 @@ void toConfigProperties() {
   @Test
   void applyToModel() {
     InstrumentationDefaults defaults = new InstrumentationDefaults();
-    defaults.setDefault("micrometer", "base_time_unit", "s");
+    defaults.get("micrometer").setDefault("base_time_unit", "s");
 
     OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel();
     defaults.applyToModel(model);
@@ -47,16 +47,15 @@ void applyToModel() {
 
   @Test
   void applyToModelDoesNotOverrideExisting() {
-    InstrumentationDefaults defaults = new InstrumentationDefaults();
-    defaults.setDefault("micrometer", "base_time_unit", "s");
-
     // Pre-populate model with a different value
     OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel();
-    new InstrumentationDefaults()
-        .setDefault("micrometer", "base_time_unit", "ms")
-        .applyToModel(model);
+    InstrumentationDefaults seed = new InstrumentationDefaults();
+    seed.get("micrometer").setDefault("base_time_unit", "ms");
+    seed.applyToModel(model);
 
-    // Apply defaults — should not override
+    // Apply a conflicting default — should not override
+    InstrumentationDefaults defaults = new InstrumentationDefaults();
+    defaults.get("micrometer").setDefault("base_time_unit", "s");
     defaults.applyToModel(model);
 
     assertThat(

From ea1616d625b3f0af7266a1d4d1dac21c4be3c183 Mon Sep 17 00:00:00 2001
From: Gregor Zeitlinger 
Date: Sat, 18 Apr 2026 08:33:43 +0000
Subject: [PATCH 07/14] docs(declarative-config-bridge): use get instead of
 getStructured in symmetry note

DeclarativeConfigProperties.get(name) (added in open-telemetry/opentelemetry-java#7923)
is the standard read-side API; update InstrumentationDefaults javadoc and README to
match.

Signed-off-by: Gregor Zeitlinger 
---
 declarative-config-bridge/README.md                           | 2 +-
 .../config/bridge/InstrumentationDefaults.java                | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md
index 973f89961808..ef1e6d0c452a 100644
--- a/declarative-config-bridge/README.md
+++ b/declarative-config-bridge/README.md
@@ -96,7 +96,7 @@ defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true")
 ```
 
 Navigation mirrors `DeclarativeConfigProperties` — reading uses
-`config.getStructured("micrometer").getString("base_time_unit")`; writing defaults uses
+`config.get("micrometer").getString("base_time_unit")`; writing defaults uses
 `defaults.get("micrometer").setDefault("base_time_unit", "s")`.
 
 Keys use underscore notation (matching the declarative config model). They are translated to
diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
index 516d44fc602c..6d2fa7256659 100644
--- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
+++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
@@ -19,7 +19,7 @@
  * declarative configuration.
  *
  * 

Navigation mirrors {@link io.opentelemetry.api.incubator.config.DeclarativeConfigProperties}: - * read-side uses {@code config.getStructured(name).getString(key)}; write-side uses {@code + * read-side uses {@code config.get(name).getString(key)}; write-side uses {@code * defaults.get(name).setDefault(key, value)}. * *

Usage: @@ -42,7 +42,7 @@ public final class InstrumentationDefaults { /** * Returns the defaults builder for the given instrumentation, creating it if absent. Mirrors - * {@code DeclarativeConfigProperties.getStructured(name)} on the read side. + * {@code DeclarativeConfigProperties.get(name)} on the read side. */ public InstrumentationProperties get(String instrumentation) { return instrumentations.computeIfAbsent(instrumentation, k -> new InstrumentationProperties()); From a7d2e54cd74dd7423f5a39cc34a0a62a62345fcd Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Mon, 4 May 2026 11:55:15 +0000 Subject: [PATCH 08/14] fix: mirror development key translation in InstrumentationDefaults Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/README.md | 9 ++++---- .../bridge/InstrumentationDefaults.java | 19 ++++++++++++---- .../bridge/InstrumentationDefaultsTest.java | 22 +++++++++++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md index ef1e6d0c452a..44a86c923a50 100644 --- a/declarative-config-bridge/README.md +++ b/declarative-config-bridge/README.md @@ -92,15 +92,16 @@ First, there is a single defaults object that is unaware of the source of the co ```java InstrumentationDefaults defaults = new InstrumentationDefaults(); defaults.get("micrometer").setDefault("base_time_unit", "s"); -defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true"); +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 underscore notation (matching the declarative config model). They are translated to -hyphen notation (`otel.instrumentation..`) when producing system property keys. +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: @@ -127,7 +128,7 @@ instrumentation/development: micrometer: base_time_unit: s log4j_appender: - experimental_log_attributes: "true" + experimental_log_attributes/development: "true" ``` And now the customizer that applies the defaults to the model: diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java index 6d2fa7256659..0affd8203b9c 100644 --- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java @@ -27,7 +27,7 @@ *

{@code
  * InstrumentationDefaults defaults = new InstrumentationDefaults();
  * defaults.get("micrometer").setDefault("base_time_unit", "s");
- * defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true");
+ * defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
  *
  * // Declarative config mode: inject into model
  * customizer.addModelCustomizer(model -> defaults.applyToModel(model));
@@ -57,9 +57,9 @@ public Map toConfigProperties() {
                 (key, value) ->
                     map.put(
                         "otel.instrumentation."
-                            + instrumentation.replace('_', '-')
+                            + translateName(instrumentation)
                             + "."
-                            + key.replace('_', '-'),
+                            + translateName(key),
                         value)));
     return map;
   }
@@ -119,7 +119,8 @@ 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.
+     * otel.instrumentation.*} keys. Keys ending in {@code /development} follow the same {@code
+     * experimental.} translation as {@link ConfigPropertiesBackedDeclarativeConfigProperties}.
      *
      * @return {@code this} for chaining
      */
@@ -129,4 +130,14 @@ public InstrumentationProperties setDefault(String key, String 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('_', '-');
+  }
 }
diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
index cc5efceef3a0..77dc9aa0cde6 100644
--- a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
@@ -17,20 +17,30 @@ class InstrumentationDefaultsTest {
   void toConfigProperties() {
     InstrumentationDefaults defaults = new InstrumentationDefaults();
     defaults.get("micrometer").setDefault("base_time_unit", "s");
-    defaults.get("log4j_appender").setDefault("experimental_log_attributes", "true");
+    defaults
+        .get("log4j_appender")
+        .setDefault("experimental_log_attributes/development", "true");
+    defaults.get("spring_scheduling").setDefault("controller_telemetry/development", "false");
+    defaults.get("grpc").setDefault("experimental_span_attributes/development", "true");
 
     Map props = defaults.toConfigProperties();
 
     assertThat(props)
         .containsEntry("otel.instrumentation.micrometer.base-time-unit", "s")
         .containsEntry("otel.instrumentation.log4j-appender.experimental-log-attributes", "true")
-        .hasSize(2);
+        .containsEntry(
+            "otel.instrumentation.spring-scheduling.experimental.controller-telemetry", "false")
+        .containsEntry("otel.instrumentation.grpc.experimental-span-attributes", "true")
+        .hasSize(4);
   }
 
   @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);
@@ -43,6 +53,14 @@ void applyToModel() {
                 .get("micrometer")
                 .getAdditionalProperties())
         .containsEntry("base_time_unit", "s");
+    assertThat(
+            model
+                .getInstrumentationDevelopment()
+                .getJava()
+                .getAdditionalProperties()
+                .get("log4j_appender")
+                .getAdditionalProperties())
+        .containsEntry("experimental_log_attributes/development", "true");
   }
 
   @Test

From 0f2744503b69a16fa95515523bef81ba4c7fb069 Mon Sep 17 00:00:00 2001
From: Gregor Zeitlinger 
Date: Mon, 4 May 2026 12:08:40 +0000
Subject: [PATCH 09/14] test: use parameterized InstrumentationDefaults
 translation cases Signed-off-by: Gregor Zeitlinger
 

---
 .../bridge/InstrumentationDefaultsTest.java   | 50 ++++++++++++-------
 1 file changed, 32 insertions(+), 18 deletions(-)

diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
index 77dc9aa0cde6..ebca5d5f6013 100644
--- a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
@@ -9,38 +9,52 @@
 
 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 {
 
-  @Test
-  void toConfigProperties() {
+  private static Stream 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("micrometer").setDefault("base_time_unit", "s");
-    defaults
-        .get("log4j_appender")
-        .setDefault("experimental_log_attributes/development", "true");
-    defaults.get("spring_scheduling").setDefault("controller_telemetry/development", "false");
-    defaults.get("grpc").setDefault("experimental_span_attributes/development", "true");
+    defaults.get(instrumentation).setDefault(key, value);
 
     Map props = defaults.toConfigProperties();
 
-    assertThat(props)
-        .containsEntry("otel.instrumentation.micrometer.base-time-unit", "s")
-        .containsEntry("otel.instrumentation.log4j-appender.experimental-log-attributes", "true")
-        .containsEntry(
-            "otel.instrumentation.spring-scheduling.experimental.controller-telemetry", "false")
-        .containsEntry("otel.instrumentation.grpc.experimental-span-attributes", "true")
-        .hasSize(4);
+    assertThat(props).containsEntry(expectedPropertyKey, value).hasSize(1);
   }
 
   @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");
+    defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
 
     OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel();
     defaults.applyToModel(model);

From 5b0f8fcc175e4059a90d7d3ad32f171611b1da32 Mon Sep 17 00:00:00 2001
From: Gregor Zeitlinger 
Date: Mon, 4 May 2026 12:33:28 +0000
Subject: [PATCH 10/14] test: add bridge roundtrip coverage for
 InstrumentationDefaults Signed-off-by: Gregor Zeitlinger
 

---
 .../bridge/InstrumentationDefaultsTest.java     | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
index ebca5d5f6013..cbbb12125b63 100644
--- a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java
@@ -7,6 +7,8 @@
 
 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;
@@ -50,6 +52,21 @@ void 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();

From fdadd88b8a3d458afac52f0d343cebc6c96a2ebc Mon Sep 17 00:00:00 2001
From: Gregor Zeitlinger 
Date: Fri, 8 May 2026 13:16:21 +0000
Subject: [PATCH 11/14] Use HashMap for InstrumentationDefaults storage;
 iteration order is not observable

Signed-off-by: Gregor Zeitlinger 
---
 .../config/bridge/InstrumentationDefaults.java               | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
index 0affd8203b9c..b767e40842b4 100644
--- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
+++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
@@ -11,7 +11,6 @@
 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.LinkedHashMap;
 import java.util.Map;
 
 /**
@@ -38,7 +37,7 @@
  */
 public final class InstrumentationDefaults {
 
-  private final Map instrumentations = new LinkedHashMap<>();
+  private final Map instrumentations = new HashMap<>();
 
   /**
    * Returns the defaults builder for the given instrumentation, creating it if absent. Mirrors
@@ -112,7 +111,7 @@ public OpenTelemetryConfigurationModel applyToModel(OpenTelemetryConfigurationMo
   /** Defaults for a single instrumentation. Keys use underscore notation. */
   public static final class InstrumentationProperties {
 
-    private final Map properties = new LinkedHashMap<>();
+    private final Map properties = new HashMap<>();
 
     private InstrumentationProperties() {}
 

From ef6c45156a03423a67a7fbef91a7697923334809 Mon Sep 17 00:00:00 2001
From: Gregor Zeitlinger 
Date: Tue, 12 May 2026 07:19:59 +0000
Subject: [PATCH 12/14] fix: isolate declarative model support in defaults
 helper Signed-off-by: Gregor Zeitlinger 

---
 declarative-config-bridge/README.md           |  58 +++++-
 declarative-config-bridge/build.gradle.kts    |   1 +
 .../bridge/DefaultInstrumentationConfig.java  | 172 ++++++++++++++++++
 .../DefaultInstrumentationConfigApplier.java  |  81 +++++++++
 .../bridge/InstrumentationDefaults.java       | 142 ---------------
 ...faultInstrumentationConfigApplierTest.java | 102 +++++++++++
 ... => DefaultInstrumentationConfigTest.java} |  58 +-----
 7 files changed, 413 insertions(+), 201 deletions(-)
 create mode 100644 declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfig.java
 create mode 100644 declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java
 delete mode 100644 declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java
 create mode 100644 declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java
 rename declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/{InstrumentationDefaultsTest.java => DefaultInstrumentationConfigTest.java} (53%)

diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md
index 44a86c923a50..f7362a24191c 100644
--- a/declarative-config-bridge/README.md
+++ b/declarative-config-bridge/README.md
@@ -83,25 +83,29 @@ public class InferredSpansComponentProvider implements ComponentProvider {
 }
 ```
 
-## InstrumentationDefaults
+## DefaultInstrumentationConfig
 
-`InstrumentationDefaults` lets distribution authors define instrumentation property defaults once
-and have them work in both configuration modes.
+`DefaultInstrumentationConfig` 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();
+DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig();
 defaults.get("micrometer").setDefault("base_time_unit", "s");
 defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
+defaults.addMapping("acme", "acme.full_name");
+defaults.get("acme").get("full_name").setDefault("preserved", "true");
 ```
 
 Navigation mirrors `DeclarativeConfigProperties` — reading uses
 `config.get("micrometer").getString("base_time_unit")`; writing defaults uses
-`defaults.get("micrometer").setDefault("base_time_unit", "s")`.
+`defaults.get("micrometer").setDefault("base_time_unit", "s")`, and deeper nested paths can chain
+`get(...)` the same way.
 
 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.
+translated using the bridge's `experimental.` convention. Custom property prefixes can be aligned
+with `DeclarativeConfigPropertiesBridgeBuilder` mappings via `defaults.addMapping(...)`.
 
 The auto configuration **without declarative config** registers the defaults as a properties
 supplier, translating them to `otel.instrumentation.*` keys:
@@ -109,15 +113,36 @@ supplier, translating them to `otel.instrumentation.*` keys:
 ```java
 @AutoService(AutoConfigurationCustomizerProvider.class)
 public class MyDistroAutoConfig implements AutoConfigurationCustomizerProvider {
+  private static final DefaultInstrumentationConfig DEFAULTS = createDefaults();
+
   @Override
   public void customize(AutoConfigurationCustomizer autoConfiguration) {
-    autoConfiguration.addPropertiesSupplier(defaults::toConfigProperties);
+    autoConfiguration.addPropertiesSupplier(DEFAULTS::toConfigProperties);
+  }
+
+  private static DefaultInstrumentationConfig createDefaults() {
+    DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig();
+    defaults.get("micrometer").setDefault("base_time_unit", "s");
+    defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
+    defaults.addMapping("acme", "acme.full_name");
+    defaults.get("acme").get("full_name").setDefault("preserved", "true");
+    return defaults;
   }
 }
 ```
 
+With the `acme` mapping above, the generated properties include:
+
+```properties
+otel.instrumentation.micrometer.base-time-unit=s
+otel.instrumentation.log4j-appender.experimental-log-attributes=true
+acme.preserved=true
+```
+
 The auto configuration **with declarative config** registers the defaults as a model customizer,
-injecting them under `instrumentation/development.java`.
+injecting them under `instrumentation/development.java`. This optional path uses
+`DefaultInstrumentationConfigApplier`, so only declarative-config users need the incubator
+file-config dependency on their classpath.
 
 Let's first look at the yaml file that the defaults effectively merge into:
 
@@ -129,6 +154,9 @@ instrumentation/development:
       base_time_unit: s
     log4j_appender:
       experimental_log_attributes/development: "true"
+    acme:
+      full_name:
+        preserved: "true"
 ```
 
 And now the customizer that applies the defaults to the model:
@@ -136,9 +164,21 @@ And now the customizer that applies the defaults to the model:
 ```java
 @AutoService(DeclarativeConfigurationCustomizerProvider.class)
 public class MyDistroDeclarativeConfig implements DeclarativeConfigurationCustomizerProvider {
+  private static final DefaultInstrumentationConfig DEFAULTS = createDefaults();
+
   @Override
   public void customize(DeclarativeConfigurationCustomizer customizer) {
-    customizer.addModelCustomizer(model -> defaults.applyToModel(model));
+    customizer.addModelCustomizer(
+        model -> DefaultInstrumentationConfigApplier.applyToModel(DEFAULTS, model));
+  }
+
+  private static DefaultInstrumentationConfig createDefaults() {
+    DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig();
+    defaults.get("micrometer").setDefault("base_time_unit", "s");
+    defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
+    defaults.addMapping("acme", "acme.full_name");
+    defaults.get("acme").get("full_name").setDefault("preserved", "true");
+    return defaults;
   }
 }
 ```
diff --git a/declarative-config-bridge/build.gradle.kts b/declarative-config-bridge/build.gradle.kts
index 7da52f11f4fd..b12d08030ff7 100644
--- a/declarative-config-bridge/build.gradle.kts
+++ b/declarative-config-bridge/build.gradle.kts
@@ -8,6 +8,7 @@ group = "io.opentelemetry.instrumentation"
 
 dependencies {
   compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
+  // Only declarative-config users need the incubator model classes at runtime.
   compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
   implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
   implementation("io.opentelemetry:opentelemetry-api-incubator")
diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfig.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfig.java
new file mode 100644
index 000000000000..684a60e402fb
--- /dev/null
+++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfig.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.config.bridge;
+
+import static java.util.Collections.emptyList;
+
+import com.google.errorprone.annotations.CanIgnoreReturnValue;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Defines instrumentation defaults that work with both traditional property-based configuration and
+ * declarative configuration.
+ *
+ * 

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)}. + * + *

Usage: + * + *

{@code
+ * DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig();
+ * defaults.get("micrometer").setDefault("base_time_unit", "s");
+ * defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true");
+ * defaults.addMapping("acme", "acme.full_name");
+ * defaults.get("acme").get("full_name").setDefault("preserved", "true");
+ *
+ * autoConfiguration.addPropertiesSupplier(defaults::toConfigProperties);
+ * }
+ * + *

For declarative-config model integration, use the optional {@link + * DefaultInstrumentationConfigApplier} helper so the base defaults type stays usable without the + * incubator model classes on the runtime classpath. + */ +public final class DefaultInstrumentationConfig { + + private final Map defaults; + private final List path; + private final Map propertyMappings; + + public DefaultInstrumentationConfig() { + this(new HashMap<>(), emptyList(), new HashMap<>()); + } + + private DefaultInstrumentationConfig( + Map defaults, List path, Map propertyMappings) { + this.defaults = defaults; + this.path = path; + this.propertyMappings = propertyMappings; + } + + /** + * Returns the defaults node for the given child, mirroring {@code + * DeclarativeConfigProperties.get(name)} on the read side. + */ + public DefaultInstrumentationConfig get(String name) { + List newPath = new ArrayList<>(path); + newPath.add(name); + return new DefaultInstrumentationConfig(defaults, newPath, propertyMappings); + } + + /** + * Adds a property prefix mapping, mirroring {@link DeclarativeConfigPropertiesBridgeBuilder + * #addMapping(String, String)} in the opposite direction. + * + *

For example, mapping {@code acme} to {@code acme.full_name} makes {@code + * defaults.get("acme").get("full_name").setDefault("preserved", "true")} produce {@code + * acme.preserved=true}. + */ + @CanIgnoreReturnValue + public DefaultInstrumentationConfig addMapping(String propertyPrefix, String declarativePath) { + propertyMappings.put(propertyPrefix, declarativePath); + return this; + } + + /** + * Sets a default value for a property on the current node. Keys use the declarative config shape + * (e.g. {@code base_time_unit}); when producing config property keys, underscores are translated + * to hyphens and keys ending in {@code /development} follow the same {@code experimental.} + * translation as {@link ConfigPropertiesBackedDeclarativeConfigProperties}. + * + * @return {@code this} for chaining + */ + @CanIgnoreReturnValue + public DefaultInstrumentationConfig setDefault(String key, String value) { + defaults.put(pathWithName(key), value); + return this; + } + + /** + * Translates defaults to config properties for auto-configuration. + * + *

Defaults use {@code otel.instrumentation.*} keys unless a custom mapping overrides the + * property prefix for a declarative path subtree. + */ + public Map toConfigProperties() { + HashMap map = new HashMap<>(); + defaults.forEach((declarativePath, value) -> map.put(toConfigProperty(declarativePath), value)); + return map; + } + + Map getDefaults() { + return defaults; + } + + private String pathWithName(String name) { + if (path.isEmpty()) { + return name; + } + return String.join(".", path) + "." + name; + } + + private String toConfigProperty(String declarativePath) { + String propertyPrefix = null; + String declarativePrefix = null; + for (Map.Entry entry : propertyMappings.entrySet()) { + String candidate = entry.getValue(); + if (!matchesPrefix(declarativePath, candidate)) { + continue; + } + if (declarativePrefix == null || candidate.length() > declarativePrefix.length()) { + declarativePrefix = candidate; + propertyPrefix = entry.getKey(); + } + } + + if (propertyPrefix == null) { + return "otel.instrumentation." + translatePath(declarativePath); + } + if (declarativePrefix == null) { + throw new IllegalStateException("missing declarative prefix for property mapping"); + } + + if (declarativePath.equals(declarativePrefix)) { + return propertyPrefix; + } + + int matchedPrefixLength = declarativePrefix.length(); + return propertyPrefix + "." + translatePath(declarativePath.substring(matchedPrefixLength + 1)); + } + + private static boolean matchesPrefix(String path, String prefix) { + return path.equals(prefix) || path.startsWith(prefix + "."); + } + + private static String translatePath(String path) { + String[] segments = path.split("\\."); + StringBuilder translated = new StringBuilder(); + for (int i = 0; i < segments.length; i++) { + if (i > 0) { + translated.append("."); + } + translated.append(translateName(segments[i])); + } + return translated.toString(); + } + + 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('_', '-'); + } +} diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java new file mode 100644 index 000000000000..7188d3e88e1d --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java @@ -0,0 +1,81 @@ +/* + * 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; + +/** Utility that applies {@link DefaultInstrumentationConfig} defaults to the declarative model. */ +public final class DefaultInstrumentationConfigApplier { + + private DefaultInstrumentationConfigApplier() {} + + /** + * 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 static OpenTelemetryConfigurationModel applyToModel( + DefaultInstrumentationConfig defaults, OpenTelemetryConfigurationModel model) { + if (defaults.getDefaults().isEmpty()) { + return model; + } + + 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 props = + java.getAdditionalProperties(); + + for (Map.Entry entry : defaults.getDefaults().entrySet()) { + applyDefault(props, entry.getKey(), entry.getValue()); + } + + return model; + } + + private static void applyDefault( + Map props, + String declarativePath, + String value) { + String[] segments = declarativePath.split("\\."); + ExperimentalLanguageSpecificInstrumentationPropertyModel propertyModel = + props.computeIfAbsent( + segments[0], key -> new ExperimentalLanguageSpecificInstrumentationPropertyModel()); + Map target = propertyModel.getAdditionalProperties(); + for (int i = 1; i < segments.length - 1; i++) { + Object child = target.get(segments[i]); + if (child == null) { + Map nested = new HashMap<>(); + target.put(segments[i], nested); + target = nested; + continue; + } + if (!(child instanceof Map)) { + return; + } + // Nested defaults only create string-keyed maps, so this cast is safe here. + @SuppressWarnings("unchecked") + Map nested = (Map) child; + target = nested; + } + target.putIfAbsent(segments[segments.length - 1], value); + } +} diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java deleted file mode 100644 index b767e40842b4..000000000000 --- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * 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. - * - *

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)}. - * - *

Usage: - * - *

{@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);
- * }
- */ -public final class InstrumentationDefaults { - - private final Map instrumentations = new HashMap<>(); - - /** - * Returns the defaults builder for the given instrumentation, creating it if absent. Mirrors - * {@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 toConfigProperties() { - HashMap map = new HashMap<>(); - instrumentations.forEach( - (instrumentation, properties) -> - properties.properties.forEach( - (key, value) -> - map.put( - "otel.instrumentation." - + 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; - } - - 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 props = - java.getAdditionalProperties(); - - for (Map.Entry entry : instrumentations.entrySet()) { - String name = entry.getKey(); - Map defaults = entry.getValue().properties; - - ExperimentalLanguageSpecificInstrumentationPropertyModel propModel = props.get(name); - if (propModel == null) { - propModel = new ExperimentalLanguageSpecificInstrumentationPropertyModel(); - props.put(name, propModel); - } - - for (Map.Entry 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 { - - private final Map 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('_', '-'); - } -} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java new file mode 100644 index 000000000000..7593562cf025 --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java @@ -0,0 +1,102 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.config.bridge; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import org.junit.jupiter.api.Test; + +class DefaultInstrumentationConfigApplierTest { + + @Test + void applyToModel() { + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); + defaults.get("micrometer").setDefault("base_time_unit", "s"); + defaults.get("log4j_appender").setDefault("experimental_log_attributes/development", "true"); + + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + DefaultInstrumentationConfigApplier.applyToModel(defaults, 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 applyToModelSupportsNestedPaths() { + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); + defaults.get("acme").get("full_name").setDefault("preserved", "true"); + + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + DefaultInstrumentationConfigApplier.applyToModel(defaults, model); + + assertThat( + model + .getInstrumentationDevelopment() + .getJava() + .getAdditionalProperties() + .get("acme") + .getAdditionalProperties()) + .containsEntry("full_name", singletonMap("preserved", "true")); + } + + @Test + void applyToModelDoesNotOverrideExisting() { + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + DefaultInstrumentationConfig seed = new DefaultInstrumentationConfig(); + seed.get("micrometer").setDefault("base_time_unit", "ms"); + DefaultInstrumentationConfigApplier.applyToModel(seed, model); + + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); + defaults.get("micrometer").setDefault("base_time_unit", "s"); + DefaultInstrumentationConfigApplier.applyToModel(defaults, model); + + assertThat( + model + .getInstrumentationDevelopment() + .getJava() + .getAdditionalProperties() + .get("micrometer") + .getAdditionalProperties()) + .containsEntry("base_time_unit", "ms"); + } + + @Test + void applyToModelDoesNotOverrideExistingNestedValues() { + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + DefaultInstrumentationConfig seed = new DefaultInstrumentationConfig(); + seed.get("acme").get("full_name").setDefault("preserved", "true"); + DefaultInstrumentationConfigApplier.applyToModel(seed, model); + + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); + defaults.get("acme").get("full_name").setDefault("preserved", "false"); + DefaultInstrumentationConfigApplier.applyToModel(defaults, model); + + assertThat( + model + .getInstrumentationDevelopment() + .getJava() + .getAdditionalProperties() + .get("acme") + .getAdditionalProperties()) + .containsEntry("full_name", singletonMap("preserved", "true")); + } +} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigTest.java similarity index 53% rename from declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java rename to declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigTest.java index cbbb12125b63..1697093aed8e 100644 --- a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigTest.java @@ -9,7 +9,6 @@ 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; @@ -17,7 +16,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -class InstrumentationDefaultsTest { +class DefaultInstrumentationConfigTest { private static Stream configPropertyDefaults() { return Stream.of( @@ -44,7 +43,7 @@ private static Stream configPropertyDefaults() { @MethodSource("configPropertyDefaults") void toConfigProperties( String instrumentation, String key, String value, String expectedPropertyKey) { - InstrumentationDefaults defaults = new InstrumentationDefaults(); + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); defaults.get(instrumentation).setDefault(key, value); Map props = defaults.toConfigProperties(); @@ -56,7 +55,7 @@ void toConfigProperties( @MethodSource("configPropertyDefaults") void toConfigPropertiesRoundTripsThroughBridge( String instrumentation, String key, String value, String expectedPropertyKey) { - InstrumentationDefaults defaults = new InstrumentationDefaults(); + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); defaults.get(instrumentation).setDefault(key, value); DeclarativeConfigProperties config = @@ -68,52 +67,11 @@ void toConfigPropertiesRoundTripsThroughBridge( } @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"); + void toConfigPropertiesWithCustomMapping() { + DefaultInstrumentationConfig defaults = new DefaultInstrumentationConfig(); + defaults.addMapping("acme", "acme.full_name"); + defaults.get("acme").get("full_name").setDefault("preserved", "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"); + assertThat(defaults.toConfigProperties()).containsEntry("acme.preserved", "true").hasSize(1); } } From 93b37ee309aa1fb4bbef874c4c137ca321c3b51b Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 12 May 2026 08:41:45 +0000 Subject: [PATCH 13/14] fix: export incubator model dependency from config bridge Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/declarative-config-bridge/build.gradle.kts b/declarative-config-bridge/build.gradle.kts index b12d08030ff7..6feb37208d5f 100644 --- a/declarative-config-bridge/build.gradle.kts +++ b/declarative-config-bridge/build.gradle.kts @@ -8,8 +8,8 @@ group = "io.opentelemetry.instrumentation" dependencies { compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") - // Only declarative-config users need the incubator model classes at runtime. - compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") + // DefaultInstrumentationConfigApplier exposes incubator model types in the public API. + api("io.opentelemetry:opentelemetry-sdk-extension-incubator") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") implementation("io.opentelemetry:opentelemetry-api-incubator") From 256f42b8ab2ac87e5142384c9bfb2710b1744f7d Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 12 May 2026 09:15:21 +0000 Subject: [PATCH 14/14] fix: align config bridge defaults helper with declarative config API Signed-off-by: Gregor Zeitlinger --- declarative-config-bridge/build.gradle.kts | 4 ++-- .../bridge/DefaultInstrumentationConfigApplier.java | 8 ++++---- .../bridge/DefaultInstrumentationConfigApplierTest.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/declarative-config-bridge/build.gradle.kts b/declarative-config-bridge/build.gradle.kts index 2f7ff687f6d1..3138c2997ff4 100644 --- a/declarative-config-bridge/build.gradle.kts +++ b/declarative-config-bridge/build.gradle.kts @@ -8,8 +8,8 @@ group = "io.opentelemetry.instrumentation" dependencies { compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") - // DefaultInstrumentationConfigApplier exposes incubator model types in the public API. - api("io.opentelemetry:opentelemetry-sdk-extension-incubator") + // DefaultInstrumentationConfigApplier exposes declarative config model types in the public API. + api("io.opentelemetry:opentelemetry-sdk-extension-declarative-config") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") implementation("io.opentelemetry:opentelemetry-api-incubator") diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java index 7188d3e88e1d..5a2f53d36197 100644 --- a/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplier.java @@ -6,10 +6,10 @@ 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 io.opentelemetry.sdk.declarativeconfig.internal.model.ExperimentalInstrumentationModel; +import io.opentelemetry.sdk.declarativeconfig.internal.model.ExperimentalLanguageSpecificInstrumentationModel; +import io.opentelemetry.sdk.declarativeconfig.internal.model.ExperimentalLanguageSpecificInstrumentationPropertyModel; +import io.opentelemetry.sdk.declarativeconfig.internal.model.OpenTelemetryConfigurationModel; import java.util.HashMap; import java.util.Map; diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java index 7593562cf025..677767e0a11f 100644 --- a/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/DefaultInstrumentationConfigApplierTest.java @@ -8,7 +8,7 @@ import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; -import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.declarativeconfig.internal.model.OpenTelemetryConfigurationModel; import org.junit.jupiter.api.Test; class DefaultInstrumentationConfigApplierTest {