diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigPropertiesUtil.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigPropertiesUtil.java new file mode 100644 index 00000000000..7022e0cc118 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigPropertiesUtil.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import io.opentelemetry.api.incubator.config.ConfigProvider; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; + +public class ConfigPropertiesUtil { + private ConfigPropertiesUtil() {} + + /** Resolve {@link ConfigProperties} from the {@code autoConfiguredOpenTelemetrySdk}. */ + public static ConfigProperties resolveConfigProperties( + AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) { + ConfigProperties sdkConfigProperties = + AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk); + if (sdkConfigProperties != null) { + return sdkConfigProperties; + } + ConfigProvider configProvider = + AutoConfigureUtil.getConfigProvider(autoConfiguredOpenTelemetrySdk); + if (configProvider != null) { + DeclarativeConfigProperties instrumentationConfig = configProvider.getInstrumentationConfig(); + + if (instrumentationConfig == null) { + instrumentationConfig = DeclarativeConfigProperties.empty(); + } + + return new DeclarativeConfigPropertiesBridge(instrumentationConfig); + } + // Should never happen + throw new IllegalStateException( + "AutoConfiguredOpenTelemetrySdk does not have ConfigProperties or DeclarativeConfigProperties. This is likely a programming error in opentelemetry-java"); + } +} diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigPropertiesBridge.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigPropertiesBridge.java new file mode 100644 index 00000000000..6284e877d55 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigPropertiesBridge.java @@ -0,0 +1,157 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import static io.opentelemetry.api.incubator.config.DeclarativeConfigProperties.empty; + +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import java.time.Duration; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import javax.annotation.Nullable; + +/** + * A {@link ConfigProperties} which resolves properties based on {@link + * DeclarativeConfigProperties}. + * + *

Only properties starting with "otel.instrumentation." are resolved. Others return null (or + * default value if provided). + * + *

To resolve: + * + *

+ * + *

For example, given the following YAML, asking for {@code + * ConfigProperties#getString("otel.instrumentation.common.string_key")} yields "value": + * + *

+ *   instrumentation:
+ *     java:
+ *       common:
+ *         string_key: value
+ * 
+ */ +final class DeclarativeConfigPropertiesBridge implements ConfigProperties { + + private static final String OTEL_INSTRUMENTATION_PREFIX = "otel.instrumentation."; + + // The node at .instrumentation.java + private final DeclarativeConfigProperties instrumentationJavaNode; + + DeclarativeConfigPropertiesBridge(DeclarativeConfigProperties instrumentationNode) { + instrumentationJavaNode = instrumentationNode.getStructured("java", empty()); + } + + @Nullable + @Override + public String getString(String propertyName) { + return getPropertyValue(propertyName, DeclarativeConfigProperties::getString); + } + + @Nullable + @Override + public Boolean getBoolean(String propertyName) { + return getPropertyValue(propertyName, DeclarativeConfigProperties::getBoolean); + } + + @Nullable + @Override + public Integer getInt(String propertyName) { + return getPropertyValue(propertyName, DeclarativeConfigProperties::getInt); + } + + @Nullable + @Override + public Long getLong(String propertyName) { + return getPropertyValue(propertyName, DeclarativeConfigProperties::getLong); + } + + @Nullable + @Override + public Double getDouble(String propertyName) { + return getPropertyValue(propertyName, DeclarativeConfigProperties::getDouble); + } + + @Nullable + @Override + public Duration getDuration(String propertyName) { + Long millis = getPropertyValue(propertyName, DeclarativeConfigProperties::getLong); + if (millis == null) { + return null; + } + return Duration.ofMillis(millis); + } + + @Override + public List getList(String propertyName) { + List propertyValue = + getPropertyValue( + propertyName, + (properties, lastPart) -> properties.getScalarList(lastPart, String.class)); + return propertyValue == null ? Collections.emptyList() : propertyValue; + } + + @Override + public Map getMap(String propertyName) { + DeclarativeConfigProperties propertyValue = + getPropertyValue(propertyName, DeclarativeConfigProperties::getStructured); + if (propertyValue == null) { + return Collections.emptyMap(); + } + Map result = new HashMap<>(); + propertyValue + .getPropertyKeys() + .forEach( + key -> { + String value = propertyValue.getString(key); + if (value == null) { + return; + } + result.put(key, value); + }); + return Collections.unmodifiableMap(result); + } + + @Nullable + private T getPropertyValue( + String property, BiFunction extractor) { + if (instrumentationJavaNode == null) { + return null; + } + + if (property.startsWith(OTEL_INSTRUMENTATION_PREFIX)) { + property = property.substring(OTEL_INSTRUMENTATION_PREFIX.length()); + } + // Split the remainder of the property on "." + String[] segments = property.split("\\."); + if (segments.length == 0) { + return null; + } + + // Extract the value by walking to the N-1 entry + DeclarativeConfigProperties target = instrumentationJavaNode; + if (segments.length > 1) { + for (int i = 0; i < segments.length - 1; i++) { + target = target.getStructured(segments[i], empty()); + } + } + String lastPart = segments[segments.length - 1]; + + return extractor.apply(target, lastPart); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigPropertiesUtilTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigPropertiesUtilTest.java new file mode 100644 index 00000000000..14e4ce7a5a3 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/ConfigPropertiesUtilTest.java @@ -0,0 +1,87 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.opentelemetry.api.incubator.config.ConfigProvider; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +@SuppressWarnings("DoNotMockAutoValue") +class ConfigPropertiesUtilTest { + @Test + void shouldUseConfigPropertiesForAutoConfiguration() { + ConfigProperties configPropertiesMock = mock(ConfigProperties.class); + AutoConfiguredOpenTelemetrySdk sdkMock = mock(AutoConfiguredOpenTelemetrySdk.class); + try (MockedStatic autoConfigureUtilMock = + Mockito.mockStatic(AutoConfigureUtil.class)) { + autoConfigureUtilMock + .when(() -> AutoConfigureUtil.getConfig(sdkMock)) + .thenReturn(configPropertiesMock); + + ConfigProperties configProperties = ConfigPropertiesUtil.resolveConfigProperties(sdkMock); + + assertThat(configProperties).isSameAs(configPropertiesMock); + } + } + + @Test + void shouldUseConfigProviderForDeclarativeConfiguration() { + String propertyName = "testProperty"; + String expectedValue = "the value"; + DeclarativeConfigProperties javaNodeMock = mock(DeclarativeConfigProperties.class); + when(javaNodeMock.getString(propertyName)).thenReturn(expectedValue); + + DeclarativeConfigProperties instrumentationConfigMock = mock(DeclarativeConfigProperties.class); + when(instrumentationConfigMock.getStructured(eq("java"), any())).thenReturn(javaNodeMock); + + ConfigProvider configProviderMock = mock(ConfigProvider.class); + when(configProviderMock.getInstrumentationConfig()).thenReturn(instrumentationConfigMock); + + AutoConfiguredOpenTelemetrySdk sdkMock = mock(AutoConfiguredOpenTelemetrySdk.class); + + try (MockedStatic autoConfigureUtilMock = + Mockito.mockStatic(AutoConfigureUtil.class)) { + autoConfigureUtilMock.when(() -> AutoConfigureUtil.getConfig(sdkMock)).thenReturn(null); + autoConfigureUtilMock + .when(() -> AutoConfigureUtil.getConfigProvider(sdkMock)) + .thenReturn(configProviderMock); + + ConfigProperties configProperties = ConfigPropertiesUtil.resolveConfigProperties(sdkMock); + + assertThat(configProperties.getString(propertyName)).isEqualTo(expectedValue); + } + } + + @Test + void shouldUseConfigProviderForDeclarativeConfiguration_noInstrumentationConfig() { + AutoConfiguredOpenTelemetrySdk sdkMock = mock(AutoConfiguredOpenTelemetrySdk.class); + ConfigProvider configProviderMock = mock(ConfigProvider.class); + when(configProviderMock.getInstrumentationConfig()).thenReturn(null); + + try (MockedStatic autoConfigureUtilMock = + Mockito.mockStatic(AutoConfigureUtil.class)) { + autoConfigureUtilMock.when(() -> AutoConfigureUtil.getConfig(sdkMock)).thenReturn(null); + autoConfigureUtilMock + .when(() -> AutoConfigureUtil.getConfigProvider(sdkMock)) + .thenReturn(configProviderMock); + + ConfigProperties configProperties = ConfigPropertiesUtil.resolveConfigProperties(sdkMock); + + assertThat(configProperties.getString("testProperty")).isEqualTo(null); + } + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigPropertiesBridgeTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigPropertiesBridgeTest.java new file mode 100644 index 00000000000..d72840e474c --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigPropertiesBridgeTest.java @@ -0,0 +1,141 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.InstrumentationModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class DeclarativeConfigPropertiesBridgeTest { + + private static final String YAML = + "file_format: 0.4\n" + + "instrumentation/development:\n" + + " java:\n" + + " common:\n" + + " default-enabled: true\n" + + " runtime-telemetry:\n" + + " enabled: false\n" + + " example-instrumentation:\n" + + " string_key: value\n" + + " bool_key: true\n" + + " int_key: 1\n" + + " double_key: 1.1\n" + + " list_key:\n" + + " - value1\n" + + " - value2\n" + + " - true\n" + + " map_key:\n" + + " string_key1: value1\n" + + " string_key2: value2\n" + + " bool_key: true\n" + + " acme:\n" + + " full_name:\n" + + " preserved: true"; + + private ConfigProperties bridge; + private ConfigProperties emptyBridge; + + @BeforeEach + void setup() { + OpenTelemetryConfigurationModel model = + DeclarativeConfiguration.parse( + new ByteArrayInputStream(YAML.getBytes(StandardCharsets.UTF_8))); + SdkConfigProvider configProvider = SdkConfigProvider.create(model); + bridge = + new DeclarativeConfigPropertiesBridge( + Objects.requireNonNull(configProvider.getInstrumentationConfig())); + + OpenTelemetryConfigurationModel emptyModel = + new OpenTelemetryConfigurationModel() + .withAdditionalProperty("instrumentation/development", new InstrumentationModel()); + SdkConfigProvider emptyConfigProvider = SdkConfigProvider.create(emptyModel); + emptyBridge = + new DeclarativeConfigPropertiesBridge( + Objects.requireNonNull(emptyConfigProvider.getInstrumentationConfig())); + } + + @Test + void getProperties() { + // only properties starting with "otel.instrumentation." are resolved + // asking for properties which don't exist or inaccessible shouldn't result in an error + assertThat(bridge.getString("file_format")).isNull(); + assertThat(bridge.getString("file_format", "foo")).isEqualTo("foo"); + assertThat(emptyBridge.getBoolean("otel.instrumentation.common.default-enabled")).isNull(); + assertThat(emptyBridge.getBoolean("otel.instrumentation.common.default-enabled", true)) + .isTrue(); + + // common cases + assertThat(bridge.getBoolean("otel.instrumentation.common.default-enabled")).isTrue(); + assertThat(bridge.getBoolean("otel.instrumentation.runtime-telemetry.enabled")).isFalse(); + + // check all the types + Map expectedMap = new HashMap<>(); + expectedMap.put("string_key1", "value1"); + expectedMap.put("string_key2", "value2"); + assertThat(bridge.getString("otel.instrumentation.example-instrumentation.string_key")) + .isEqualTo("value"); + assertThat(bridge.getBoolean("otel.instrumentation.example-instrumentation.bool_key")).isTrue(); + assertThat(bridge.getInt("otel.instrumentation.example-instrumentation.int_key")).isEqualTo(1); + assertThat(bridge.getLong("otel.instrumentation.example-instrumentation.int_key")) + .isEqualTo(1L); + assertThat(bridge.getDuration("otel.instrumentation.example-instrumentation.int_key")) + .isEqualTo(Duration.ofMillis(1)); + assertThat(bridge.getDouble("otel.instrumentation.example-instrumentation.double_key")) + .isEqualTo(1.1); + assertThat(bridge.getList("otel.instrumentation.example-instrumentation.list_key")) + .isEqualTo(Arrays.asList("value1", "value2")); + assertThat(bridge.getMap("otel.instrumentation.example-instrumentation.map_key")) + .isEqualTo(expectedMap); + + // asking for properties with the wrong type returns null + assertThat(bridge.getBoolean("otel.instrumentation.example-instrumentation.string_key")) + .isNull(); + assertThat(bridge.getString("otel.instrumentation.example-instrumentation.bool_key")).isNull(); + assertThat(bridge.getString("otel.instrumentation.example-instrumentation.int_key")).isNull(); + assertThat(bridge.getString("otel.instrumentation.example-instrumentation.double_key")) + .isNull(); + assertThat(bridge.getString("otel.instrumentation.example-instrumentation.list_key")).isNull(); + assertThat(bridge.getString("otel.instrumentation.example-instrumentation.map_key")).isNull(); + + // check all the types + assertThat(bridge.getString("otel.instrumentation.other-instrumentation.string_key", "value")) + .isEqualTo("value"); + assertThat(bridge.getBoolean("otel.instrumentation.other-instrumentation.bool_key", true)) + .isTrue(); + assertThat(bridge.getInt("otel.instrumentation.other-instrumentation.int_key", 1)).isEqualTo(1); + assertThat(bridge.getLong("otel.instrumentation.other-instrumentation.int_key", 1L)) + .isEqualTo(1L); + assertThat( + bridge.getDuration( + "otel.instrumentation.other-instrumentation.int_key", Duration.ofMillis(1))) + .isEqualTo(Duration.ofMillis(1)); + assertThat(bridge.getDouble("otel.instrumentation.other-instrumentation.double_key", 1.1)) + .isEqualTo(1.1); + assertThat( + bridge.getList( + "otel.instrumentation.other-instrumentation.list_key", + Arrays.asList("value1", "value2"))) + .isEqualTo(Arrays.asList("value1", "value2")); + assertThat(bridge.getMap("otel.instrumentation.other-instrumentation.map_key", expectedMap)) + .isEqualTo(expectedMap); + + // verify vendor specific property names are preserved in unchanged form (prefix is not stripped + // as for otel.instrumentation.*) + assertThat(bridge.getBoolean("acme.full_name.preserved")).isTrue(); + } +}