diff --git a/declarative-config-bridge/build.gradle.kts b/declarative-config-bridge/build.gradle.kts new file mode 100644 index 000000000..1da7432d0 --- /dev/null +++ b/declarative-config-bridge/build.gradle.kts @@ -0,0 +1,18 @@ +plugins { + id("otel.java-conventions") + id("otel.publish-conventions") +} + +description = "OpenTelemetry extension that provides a bridge for declarative configuration." +otelJava.moduleName.set("io.opentelemetry.contrib.sdk.declarative.config.bridge") + +dependencies { + // We use `compileOnly` dependency because during runtime all necessary classes are provided by + // javaagent itself. + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") + + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") + testImplementation("org.mockito:mockito-inline") +} diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtil.java b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtil.java new file mode 100644 index 000000000..e83b72e0d --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtil.java @@ -0,0 +1,63 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.autoconfigure; + +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 io.opentelemetry.sdk.extension.incubator.fileconfig.SdkConfigProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import java.util.Collections; +import java.util.Map; + +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, Collections.emptyMap()); + } + // Should never happen + throw new IllegalStateException( + "AutoConfiguredOpenTelemetrySdk does not have ConfigProperties or DeclarativeConfigProperties. This is likely a programming error in opentelemetry-java"); + } + + public static ConfigProperties resolveModel(OpenTelemetryConfigurationModel model) { + return resolveModel(model, Collections.emptyMap()); + } + + public static ConfigProperties resolveModel( + OpenTelemetryConfigurationModel model, Map translationMap) { + SdkConfigProvider configProvider = SdkConfigProvider.create(model); + DeclarativeConfigProperties instrumentationConfig = configProvider.getInstrumentationConfig(); + if (instrumentationConfig == null) { + instrumentationConfig = DeclarativeConfigProperties.empty(); + } + + return new DeclarativeConfigPropertiesBridge(instrumentationConfig, translationMap); + } + + public static String propertyYamlPath(String propertyName) { + return DeclarativeConfigPropertiesBridge.yamlPath(propertyName); + } +} diff --git a/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridge.java b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridge.java new file mode 100644 index 000000000..11a16afbb --- /dev/null +++ b/declarative-config-bridge/src/main/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridge.java @@ -0,0 +1,182 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.autoconfigure; + +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; + private final Map translationMap; + + DeclarativeConfigPropertiesBridge( + DeclarativeConfigProperties instrumentationNode, Map translationMap) { + instrumentationJavaNode = instrumentationNode.getStructured("java", empty()); + this.translationMap = translationMap; + } + + @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; + } + + String[] segments = getSegments(translate(property)); + 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); + } + + private String translate(String property) { + for (Map.Entry entry : translationMap.entrySet()) { + if (property.startsWith(entry.getKey())) { + return entry.getValue() + property.substring(entry.getKey().length()); + } + } + return property; + } + + private static String[] getSegments(String property) { + if (property.startsWith(OTEL_INSTRUMENTATION_PREFIX)) { + property = property.substring(OTEL_INSTRUMENTATION_PREFIX.length()); + } + // Split the remainder of the property on "." + return property.split("\\."); + } + + static String yamlPath(String property) { + String[] segments = getSegments(property); + if (segments.length == 0) { + throw new IllegalArgumentException("Invalid property: " + property); + } + + return "'instrumentation/development' / 'java' / '" + String.join("' / '", segments) + "'"; + } +} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtilTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtilTest.java new file mode 100644 index 000000000..9981ffdb2 --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtilTest.java @@ -0,0 +1,94 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.autoconfigure; + +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); + } + } + + @Test + void propertyYamlPath() { + assertThat(ConfigPropertiesUtil.propertyYamlPath("google.otel.auth.target.signals")) + .isEqualTo( + "'instrumentation/development' / 'java' / 'google' / 'otel' / 'auth' / 'target' / 'signals'"); + } +} diff --git a/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java new file mode 100644 index 000000000..a7722c5b9 --- /dev/null +++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.sdk.autoconfigure; /* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.extension.incubator.fileconfig.SdkConfigProvider; +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.Collections; +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() { + bridge = createBridge(Collections.emptyMap()); + + OpenTelemetryConfigurationModel emptyModel = + new OpenTelemetryConfigurationModel() + .withAdditionalProperty("instrumentation/development", new InstrumentationModel()); + SdkConfigProvider emptyConfigProvider = SdkConfigProvider.create(emptyModel); + emptyBridge = + new DeclarativeConfigPropertiesBridge( + Objects.requireNonNull(emptyConfigProvider.getInstrumentationConfig()), + Collections.emptyMap()); + } + + private static DeclarativeConfigPropertiesBridge createBridge( + Map translationMap) { + OpenTelemetryConfigurationModel model = + DeclarativeConfiguration.parse( + new ByteArrayInputStream(YAML.getBytes(StandardCharsets.UTF_8))); + return new DeclarativeConfigPropertiesBridge( + Objects.requireNonNull(SdkConfigProvider.create(model).getInstrumentationConfig()), + translationMap); + } + + @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); + } + + @Test + void vendor() { + // 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(); + } + + @Test + void translation() { + DeclarativeConfigPropertiesBridge propertiesBridge = + createBridge(Collections.singletonMap("acme", "acme.full_name")); + assertThat(propertiesBridge.getBoolean("acme.preserved")).isTrue(); + } +} diff --git a/gcp-auth-extension/build.gradle.kts b/gcp-auth-extension/build.gradle.kts index fd95a653f..ec5a32ab5 100644 --- a/gcp-auth-extension/build.gradle.kts +++ b/gcp-auth-extension/build.gradle.kts @@ -14,12 +14,15 @@ val agent: Configuration by configurations.creating { } dependencies { + implementation(project(":declarative-config-bridge")) + annotationProcessor("com.google.auto.service:auto-service") // We use `compileOnly` dependency because during runtime all necessary classes are provided by // javaagent itself. compileOnly("com.google.auto.service:auto-service-annotations") compileOnly("io.opentelemetry:opentelemetry-api") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") compileOnly("io.opentelemetry:opentelemetry-exporter-otlp") // Only dependencies added to `implementation` configuration will be picked up by Shadow plugin @@ -35,6 +38,7 @@ dependencies { testImplementation("io.opentelemetry:opentelemetry-exporter-otlp") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") testImplementation("io.opentelemetry.instrumentation:opentelemetry-instrumentation-annotations") testImplementation("org.awaitility:awaitility") diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java index 639207909..5756d1224 100644 --- a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/ConfigurableOption.java @@ -9,6 +9,7 @@ import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; import java.util.Locale; import java.util.Optional; +import java.util.function.BiFunction; import java.util.function.Supplier; /** @@ -99,9 +100,17 @@ String getUserReadableName() { * @throws ConfigurationException if neither the environment variable nor the system property is * set. */ - String getConfiguredValue(ConfigProperties configProperties) { - String configuredValue = configProperties.getString(this.getSystemProperty()); - if (configuredValue != null && !configuredValue.isEmpty()) { + T getConfiguredValue( + ConfigProperties configProperties, BiFunction extractor) { + T configuredValue = extractor.apply(configProperties, this.getSystemProperty()); + if (configuredValue instanceof String) { + String value = (String) configuredValue; + if (value.isEmpty()) { + configuredValue = null; // Treat empty string as not configured + } + } + + if (configuredValue != null) { return configuredValue; } else { throw new ConfigurationException( @@ -121,10 +130,12 @@ String getConfiguredValue(ConfigProperties configProperties) { * @return The configured value for the option, obtained from the environment variable, system * property, or the fallback function, in that order of precedence. */ - String getConfiguredValueWithFallback( - ConfigProperties configProperties, Supplier fallback) { + T getConfiguredValueWithFallback( + ConfigProperties configProperties, + Supplier fallback, + BiFunction extractor) { try { - return this.getConfiguredValue(configProperties); + return this.getConfiguredValue(configProperties, extractor); } catch (ConfigurationException e) { return fallback.get(); } @@ -140,7 +151,7 @@ String getConfiguredValueWithFallback( */ Optional getConfiguredValueAsOptional(ConfigProperties configProperties) { try { - return Optional.of(this.getConfiguredValue(configProperties)); + return Optional.of(this.getConfiguredValue(configProperties, ConfigProperties::getString)); } catch (ConfigurationException e) { return Optional.empty(); } diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java index 1de583029..a54dd677d 100644 --- a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthAutoConfigurationCustomizerProvider.java @@ -25,7 +25,7 @@ import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.IOException; -import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; @@ -84,8 +84,8 @@ public class GcpAuthAutoConfigurationCustomizerProvider * customizes only the signal specific exporter. * * - * The 'customization' performed includes customizing the exporters by adding required headers to - * the export calls made and customizing the resource by adding required resource attributes to + *

The 'customization' performed includes customizing the exporters by adding required headers + * to the export calls made and customizing the resource by adding required resource attributes to * enable GCP integration. * * @param autoConfiguration the AutoConfigurationCustomizer to customize. @@ -96,12 +96,7 @@ public class GcpAuthAutoConfigurationCustomizerProvider */ @Override public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) { - GoogleCredentials credentials; - try { - credentials = GoogleCredentials.getApplicationDefault(); - } catch (IOException e) { - throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); - } + GoogleCredentials credentials = getCredentials(); autoConfiguration .addSpanExporterCustomizer( (spanExporter, configProperties) -> @@ -112,6 +107,16 @@ public void customize(@Nonnull AutoConfigurationCustomizer autoConfiguration) { .addResourceCustomizer(GcpAuthAutoConfigurationCustomizerProvider::customizeResource); } + static GoogleCredentials getCredentials() { + GoogleCredentials credentials; + try { + credentials = GoogleCredentials.getApplicationDefault(); + } catch (IOException e) { + throw new GoogleAuthException(Reason.FAILED_ADC_RETRIEVAL, e); + } + return credentials; + } + @Override public int order() { return Integer.MAX_VALUE - 1; @@ -119,44 +124,50 @@ public int order() { private static SpanExporter customizeSpanExporter( SpanExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) { - if (isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties)) { + if (shouldConfigureExporter( + SIGNAL_TYPE_TRACES, SIGNAL_TARGET_WARNING_FIX_SUGGESTION, configProperties)) { return addAuthorizationHeaders(exporter, credentials, configProperties); - } else { - String[] params = {SIGNAL_TYPE_TRACES, SIGNAL_TARGET_WARNING_FIX_SUGGESTION}; - logger.log( - Level.WARNING, - "GCP Authentication Extension is not configured for signal type: {0}. {1}", - params); } return exporter; } private static MetricExporter customizeMetricExporter( MetricExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) { - if (isSignalTargeted(SIGNAL_TYPE_METRICS, configProperties)) { + if (shouldConfigureExporter( + SIGNAL_TYPE_METRICS, SIGNAL_TARGET_WARNING_FIX_SUGGESTION, configProperties)) { return addAuthorizationHeaders(exporter, credentials, configProperties); + } + return exporter; + } + + static boolean shouldConfigureExporter( + String signal, String fixSuggestion, ConfigProperties configProperties) { + if (isSignalTargeted(signal, configProperties)) { + return true; } else { - String[] params = {SIGNAL_TYPE_METRICS, SIGNAL_TARGET_WARNING_FIX_SUGGESTION}; logger.log( Level.WARNING, "GCP Authentication Extension is not configured for signal type: {0}. {1}", - params); + new String[] {signal, fixSuggestion}); + return false; } - return exporter; } // Checks if the auth extension is configured to target the passed signal for authentication. private static boolean isSignalTargeted(String checkSignal, ConfigProperties configProperties) { - String userSpecifiedTargetedSignals = - ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback( - configProperties, () -> SIGNAL_TYPE_ALL); - return Arrays.stream(userSpecifiedTargetedSignals.split(",")) - .map(String::trim) + return targetSignals(configProperties).stream() .anyMatch( targetedSignal -> targetedSignal.equals(checkSignal) || targetedSignal.equals(SIGNAL_TYPE_ALL)); } + static List targetSignals(ConfigProperties configProperties) { + return ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValueWithFallback( + configProperties, + () -> Collections.singletonList(SIGNAL_TYPE_ALL), + ConfigProperties::getList); + } + // Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and // OtlpHttpSpanExporter. private static SpanExporter addAuthorizationHeaders( @@ -193,7 +204,7 @@ private static MetricExporter addAuthorizationHeaders( return exporter; } - private static Map getRequiredHeaderMap( + static Map getRequiredHeaderMap( GoogleCredentials credentials, ConfigProperties configProperties) { Map> gcpHeaders; try { @@ -216,23 +227,30 @@ private static Map getRequiredHeaderMap( // Add quota user project header if not detected by the auth library and user provided it via // system properties. if (!flattenedHeaders.containsKey(QUOTA_USER_PROJECT_HEADER)) { - Optional maybeConfiguredQuotaProjectId = - ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueAsOptional( - configProperties); - maybeConfiguredQuotaProjectId.ifPresent( - configuredQuotaProjectId -> - flattenedHeaders.put(QUOTA_USER_PROJECT_HEADER, configuredQuotaProjectId)); + getQuotaProjectId(configProperties) + .ifPresent( + configuredQuotaProjectId -> + flattenedHeaders.put(QUOTA_USER_PROJECT_HEADER, configuredQuotaProjectId)); } return flattenedHeaders; } + static Optional getQuotaProjectId(ConfigProperties configProperties) { + return ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValueAsOptional( + configProperties); + } + // Updates the current resource with the attributes required for ingesting OTLP data on GCP. private static Resource customizeResource(Resource resource, ConfigProperties configProperties) { - String gcpProjectId = - ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue(configProperties); Resource res = Resource.create( - Attributes.of(AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), gcpProjectId)); + Attributes.of( + AttributeKey.stringKey(GCP_USER_PROJECT_ID_KEY), getProjectId(configProperties))); return resource.merge(res); } + + static String getProjectId(ConfigProperties configProperties) { + return ConfigurableOption.GOOGLE_CLOUD_PROJECT.getConfiguredValue( + configProperties, ConfigProperties::getString); + } } diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthCustomizerProvider.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthCustomizerProvider.java new file mode 100644 index 000000000..297a47596 --- /dev/null +++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthCustomizerProvider.java @@ -0,0 +1,162 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_METRICS; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.SIGNAL_TYPE_TRACES; +import static io.opentelemetry.contrib.gcp.auth.GcpAuthAutoConfigurationCustomizerProvider.shouldConfigureExporter; + +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auto.service.AutoService; +import io.opentelemetry.contrib.sdk.autoconfigure.ConfigPropertiesUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizer; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizerProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.BatchSpanProcessorModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.MeterProviderModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.MetricReaderModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.NameStringValuePairModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpGrpcExporterModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpGrpcMetricExporterModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpHttpExporterModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpHttpMetricExporterModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.PushMetricExporterModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SimpleSpanProcessorModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanProcessorModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.TracerProviderModel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +@AutoService(DeclarativeConfigurationCustomizerProvider.class) +public class GcpAuthCustomizerProvider implements DeclarativeConfigurationCustomizerProvider { + + static final String SIGNAL_TARGET_WARNING_YAML_FIX_SUGGESTION = + String.format( + "You may safely ignore this warning if it is intentional, otherwise please configure the '%s' by setting %s in the configuration file.", + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getUserReadableName(), + ConfigPropertiesUtil.propertyYamlPath( + ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getSystemProperty())); + + @Override + public void customize(DeclarativeConfigurationCustomizer customizer) { + customizer.addModelCustomizer( + model -> { + customize( + model, + GcpAuthAutoConfigurationCustomizerProvider.getCredentials(), + ConfigPropertiesUtil.resolveModel(model)); + + return model; + }); + } + + static void customize( + OpenTelemetryConfigurationModel model, + GoogleCredentials credentials, + ConfigProperties configProperties) { + Map headerMap = + GcpAuthAutoConfigurationCustomizerProvider.getRequiredHeaderMap( + credentials, configProperties); + customizeMeter(model, headerMap, configProperties); + customizeTracer(model, headerMap, configProperties); + } + + private static void customizeMeter( + OpenTelemetryConfigurationModel model, + Map headerMap, + ConfigProperties configProperties) { + MeterProviderModel meterProvider = model.getMeterProvider(); + if (meterProvider == null) { + return; + } + + if (shouldConfigureExporter( + SIGNAL_TYPE_METRICS, SIGNAL_TARGET_WARNING_YAML_FIX_SUGGESTION, configProperties)) { + for (MetricReaderModel reader : meterProvider.getReaders()) { + if (reader.getPeriodic() != null) { + addAuth(meterModelHeaders(reader.getPeriodic().getExporter()), headerMap); + } + } + } + } + + private static List> meterModelHeaders( + @Nullable PushMetricExporterModel exporter) { + ArrayList> list = new ArrayList<>(); + if (exporter == null) { + return list; + } + OtlpGrpcMetricExporterModel grpc = exporter.getOtlpGrpc(); + if (grpc != null) { + list.add(grpc.getHeaders()); + } + OtlpHttpMetricExporterModel http = exporter.getOtlpHttp(); + if (http != null) { + list.add(http.getHeaders()); + } + return list; + } + + private static void customizeTracer( + OpenTelemetryConfigurationModel model, + Map headerMap, + ConfigProperties configProperties) { + TracerProviderModel tracerProvider = model.getTracerProvider(); + if (tracerProvider == null) { + return; + } + + if (shouldConfigureExporter( + SIGNAL_TYPE_TRACES, SIGNAL_TARGET_WARNING_YAML_FIX_SUGGESTION, configProperties)) { + for (SpanProcessorModel processor : tracerProvider.getProcessors()) { + BatchSpanProcessorModel batch = processor.getBatch(); + if (batch != null) { + addAuth(spanExporterModelHeaders(batch.getExporter()), headerMap); + } + SimpleSpanProcessorModel simple = processor.getSimple(); + if (simple != null) { + addAuth(spanExporterModelHeaders(simple.getExporter()), headerMap); + } + } + } + } + + private static List> spanExporterModelHeaders( + @Nullable SpanExporterModel exporter) { + ArrayList> list = new ArrayList<>(); + + if (exporter == null) { + return list; + } + OtlpGrpcExporterModel grpc = exporter.getOtlpGrpc(); + if (grpc != null) { + list.add(grpc.getHeaders()); + } + OtlpHttpExporterModel http = exporter.getOtlpHttp(); + if (http != null) { + list.add(http.getHeaders()); + } + return list; + } + + private static void addAuth( + List> headerConsumers, Map headerMap) { + headerConsumers.forEach(headers -> addHeaders(headers, headerMap)); + } + + private static void addHeaders(List headers, Map add) { + add.forEach( + (key, value) -> { + if (headers.stream().noneMatch(header -> key.equals(header.getName()))) { + headers.add(new NameStringValuePairModel().withName(key).withValue(value)); + } + }); + } +} diff --git a/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthCustomizerProviderTest.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthCustomizerProviderTest.java new file mode 100644 index 000000000..fd469eae0 --- /dev/null +++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthCustomizerProviderTest.java @@ -0,0 +1,86 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.gcp.auth; + +import static io.opentelemetry.contrib.gcp.auth.GcpAuthCustomizerProvider.SIGNAL_TARGET_WARNING_YAML_FIX_SUGGESTION; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.auth.oauth2.GoogleCredentials; +import io.opentelemetry.contrib.sdk.autoconfigure.ConfigPropertiesUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import org.junit.jupiter.api.Test; + +class GcpAuthCustomizerProviderTest { + + @Test + void declarativeConfig() throws IOException { + String yaml = + "file_format: 0.4\n" + + "tracer_provider:\n" + + " processors:\n" + + " - simple:\n" + + " exporter:\n" + + " otlp_http:\n" + + "meter_provider:\n" + + " readers:\n" + + " - periodic:\n" + + " exporter:\n" + + " otlp_http:\n" + + "instrumentation/development:\n" + + " java:\n" + + " google:\n" + + " cloud:\n" + + " project: p\n" + + " quota:\n" + + " project: qp\n" + + " otel:\n" + + " auth:\n" + + " target:\n" + + " signals: [metrics, traces]\n"; + + OpenTelemetryConfigurationModel model = + DeclarativeConfiguration.parse( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + ConfigProperties properties = ConfigPropertiesUtil.resolveModel(model); + + assertThat(GcpAuthAutoConfigurationCustomizerProvider.targetSignals(properties)) + .containsExactly("metrics", "traces"); + assertThat(GcpAuthAutoConfigurationCustomizerProvider.getProjectId(properties)).isEqualTo("p"); + assertThat(GcpAuthAutoConfigurationCustomizerProvider.getQuotaProjectId(properties)) + .contains("qp"); + + GoogleCredentials credentials = mock(GoogleCredentials.class); + when(credentials.getRequestMetadata()) + .thenReturn( + Collections.singletonMap("x-goog-user-project", Collections.singletonList("qp"))); + + GcpAuthCustomizerProvider.customize(model, credentials, properties); + + String header = + "headers=\\[io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.NameStringValuePairModel@.*\\[name=x-goog-user-project,value=qp]"; + // both metrics and traces should have the header + assertThat(model.toString()).matches(String.format(".*%s.*%s.*", header, header)); + } + + @Test + void fixSuggestion() { + assertThat(SIGNAL_TARGET_WARNING_YAML_FIX_SUGGESTION) + .isEqualTo( + "You may safely ignore this warning if it is intentional, " + + "otherwise please configure the 'Target Signals for Google Authentication Extension' " + + "by setting " + + "'instrumentation/development' / 'java' / 'google' / 'otel' / 'auth' / 'target' / " + + "'signals' in the configuration file."); + } +} diff --git a/inferred-spans/build.gradle.kts b/inferred-spans/build.gradle.kts index 98d5e33a3..bc89d6da9 100644 --- a/inferred-spans/build.gradle.kts +++ b/inferred-spans/build.gradle.kts @@ -9,10 +9,13 @@ description = "OpenTelemetry Java profiling based inferred spans module" otelJava.moduleName.set("io.opentelemetry.contrib.inferredspans") dependencies { + implementation(project(":declarative-config-bridge")) + annotationProcessor("com.google.auto.service:auto-service") compileOnly("com.google.auto.service:auto-service-annotations") compileOnly("io.opentelemetry:opentelemetry-sdk") compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator") compileOnly("io.opentelemetry.semconv:opentelemetry-semconv") implementation("com.lmax:disruptor") implementation("org.jctools:jctools-core") @@ -25,6 +28,7 @@ dependencies { testImplementation("io.opentelemetry.semconv:opentelemetry-semconv") testImplementation("io.opentelemetry:opentelemetry-sdk") testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") + testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator") testImplementation("io.opentelemetry:opentelemetry-sdk-testing") testImplementation("io.opentelemetry:opentelemetry-api-incubator") testImplementation("io.opentelemetry:opentelemetry-exporter-logging") diff --git a/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansCustomizerProvider.java b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansCustomizerProvider.java new file mode 100644 index 000000000..47a3c61a8 --- /dev/null +++ b/inferred-spans/src/main/java/io/opentelemetry/contrib/inferredspans/InferredSpansCustomizerProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.contrib.inferredspans; + +import com.google.auto.service.AutoService; +import io.opentelemetry.contrib.sdk.autoconfigure.ConfigPropertiesUtil; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizer; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationCustomizerProvider; +import java.util.Collections; + +@AutoService(DeclarativeConfigurationCustomizerProvider.class) +public class InferredSpansCustomizerProvider implements DeclarativeConfigurationCustomizerProvider { + + @Override + public void customize(DeclarativeConfigurationCustomizer customizer) { + customizer.addModelCustomizer( + model -> { + ConfigProperties configProperties = + ConfigPropertiesUtil.resolveModel( + model, Collections.singletonMap("otel.inferred.spans", "inferred_spans")); + + return model; + }); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 27db34c41..5c761f370 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,6 +43,7 @@ include(":baggage-processor") include(":compressors:compressor-zstd") include(":cloudfoundry-resources") include(":consistent-sampling") +include(":declarative-config-bridge") include(":dependencyManagement") include(":disk-buffering") include(":example")