Skip to content
62 changes: 62 additions & 0 deletions declarative-config-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>.<key>`) 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`).
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")
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,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.
*
* <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", "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 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<String, String> toConfigProperties() {
HashMap<String, String> 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<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 {

private final Map<String, String> 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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> 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");
}
}
Loading