diff --git a/declarative-config-bridge/README.md b/declarative-config-bridge/README.md index 0e6b7154bb15..ef1e6d0c452a 100644 --- a/declarative-config-bridge/README.md +++ b/declarative-config-bridge/README.md @@ -82,3 +82,65 @@ public class InferredSpansComponentProvider implements ComponentProvider { } } ``` + +## 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", "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. + +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); + } +} +``` + +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" +``` + +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`). 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..6d2fa7256659 --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaults.java @@ -0,0 +1,132 @@ +/* + * 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.LinkedHashMap; +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", "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<>(); + + /** + * 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." + + 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. + */ + @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 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 new file mode 100644 index 000000000000..cc5efceef3a0 --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/instrumentation/config/bridge/InstrumentationDefaultsTest.java @@ -0,0 +1,70 @@ +/* + * 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.get("micrometer").setDefault("base_time_unit", "s"); + defaults.get("log4j_appender").setDefault("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.get("micrometer").setDefault("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() { + // 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"); + } +}