overrideValues = new HashMap<>();
+
+ public DeclarativeConfigPropertiesBridgeBuilder() {}
+
+ /**
+ * Adds a mapping from a property prefix to a YAML path.
+ *
+ * For example, if the property prefix is "otel.javaagent" and the YAML path is "agent", then
+ * any property starting with "otel.javaagent." will be resolved against the "agent" node in the
+ * instrumentation/java section of the YAML configuration.
+ *
+ * @param propertyPrefix the prefix of the property to translate
+ * @param yamlPath the YAML path to resolve the property against
+ */
+ @CanIgnoreReturnValue
+ public DeclarativeConfigPropertiesBridgeBuilder addMapping(
+ String propertyPrefix, String yamlPath) {
+ mappings.put(propertyPrefix, yamlPath);
+ return this;
+ }
+
+ /**
+ * Adds a fixed override value for a property.
+ *
+ * @param propertyName the name of the property to override
+ * @param value the value to return when the property is requested
+ */
+ @CanIgnoreReturnValue
+ public DeclarativeConfigPropertiesBridgeBuilder addOverride(String propertyName, Object value) {
+ overrideValues.put(propertyName, value);
+ return this;
+ }
+
+ /** Build {@link ConfigProperties} from the {@code autoConfiguredOpenTelemetrySdk}. */
+ public ConfigProperties build(AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk) {
+ ConfigProperties sdkConfigProperties =
+ AutoConfigureUtil.getConfig(autoConfiguredOpenTelemetrySdk);
+ if (sdkConfigProperties != null) {
+ return sdkConfigProperties;
+ }
+ ConfigProvider configProvider =
+ AutoConfigureUtil.getConfigProvider(autoConfiguredOpenTelemetrySdk);
+ if (configProvider != null) {
+ return buildFromInstrumentationConfig(configProvider.getInstrumentationConfig());
+ }
+ // Should never happen
+ throw new IllegalStateException(
+ "AutoConfiguredOpenTelemetrySdk does not have ConfigProperties or DeclarativeConfigProperties. This is likely a programming error in opentelemetry-java");
+ }
+
+ /**
+ * Build {@link ConfigProperties} from the {@link DeclarativeConfigProperties} provided by the
+ * instrumentation configuration.
+ *
+ *
If the provided {@code instrumentationConfig} is null, an empty {@link
+ * DeclarativeConfigProperties} will be used.
+ *
+ * @param instrumentationConfig the instrumentation configuration to build from
+ * @return a new instance of {@link ConfigProperties}
+ */
+ public ConfigProperties buildFromInstrumentationConfig(
+ @Nullable DeclarativeConfigProperties instrumentationConfig) {
+ // leave the name "build" for a future method that builds from a DeclarativeConfigProperties
+ // instance that doesn't come from the top-level instrumentation config
+ if (instrumentationConfig == null) {
+ instrumentationConfig = DeclarativeConfigProperties.empty();
+ }
+ return new DeclarativeConfigPropertiesBridge(
+ instrumentationConfig.getStructured("java", empty()), mappings, overrideValues);
+ }
+}
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..f28f943d4
--- /dev/null
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/ConfigPropertiesUtilTest.java
@@ -0,0 +1,19 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+class ConfigPropertiesUtilTest {
+ @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/DeclarativeConfigPropertiesBridgeBuilderTest.java b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeBuilderTest.java
new file mode 100644
index 000000000..e07a7fbf7
--- /dev/null
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeBuilderTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 DeclarativeConfigPropertiesBridgeBuilderTest {
+ @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 =
+ new DeclarativeConfigPropertiesBridgeBuilder().build(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 =
+ new DeclarativeConfigPropertiesBridgeBuilder().build(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 =
+ new DeclarativeConfigPropertiesBridgeBuilder().build(sdkMock);
+
+ assertThat(configProperties.getString("testProperty")).isNull();
+ }
+ }
+}
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..405cecc1c
--- /dev/null
+++ b/declarative-config-bridge/src/test/java/io/opentelemetry/contrib/sdk/autoconfigure/DeclarativeConfigPropertiesBridgeTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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 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.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 ConfigProperties bridge;
+ private ConfigProperties emptyBridge;
+
+ @BeforeEach
+ void setup() {
+ bridge = create(new DeclarativeConfigPropertiesBridgeBuilder());
+
+ OpenTelemetryConfigurationModel emptyModel =
+ new OpenTelemetryConfigurationModel()
+ .withAdditionalProperty("instrumentation/development", new InstrumentationModel());
+ SdkConfigProvider emptyConfigProvider = SdkConfigProvider.create(emptyModel);
+ emptyBridge =
+ new DeclarativeConfigPropertiesBridgeBuilder()
+ .buildFromInstrumentationConfig(
+ Objects.requireNonNull(emptyConfigProvider.getInstrumentationConfig()));
+ }
+
+ private static ConfigProperties create(DeclarativeConfigPropertiesBridgeBuilder builder) {
+ OpenTelemetryConfigurationModel model =
+ DeclarativeConfiguration.parse(
+ DeclarativeConfigPropertiesBridgeTest.class
+ .getClassLoader()
+ .getResourceAsStream("config.yaml"));
+ return builder.buildFromInstrumentationConfig(
+ SdkConfigProvider.create(model).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.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 vendorTranslation() {
+ ConfigProperties propertiesBridge =
+ create(new DeclarativeConfigPropertiesBridgeBuilder().addMapping("acme", "acme.full_name"));
+ assertThat(propertiesBridge.getBoolean("acme.preserved")).isTrue();
+ }
+
+ @Test
+ void agentTranslation() {
+ ConfigProperties bridge =
+ create(
+ new DeclarativeConfigPropertiesBridgeBuilder()
+ .addMapping("otel.javaagent", "agent")
+ .addOverride("otel.javaagent.debug", true)
+ .addOverride("otel.javaagent.logging", "application"));
+
+ assertThat(bridge.getBoolean("otel.javaagent.debug")).isTrue();
+ assertThat(bridge.getBoolean("otel.javaagent.experimental.indy")).isTrue();
+ assertThat(bridge.getString("otel.javaagent.logging")).isEqualTo("application");
+ }
+}
diff --git a/declarative-config-bridge/src/test/resources/config.yaml b/declarative-config-bridge/src/test/resources/config.yaml
new file mode 100644
index 000000000..ef0ce9a8c
--- /dev/null
+++ b/declarative-config-bridge/src/test/resources/config.yaml
@@ -0,0 +1,27 @@
+file_format: 0.4
+instrumentation/development:
+ java:
+ acme:
+ full_name:
+ preserved: true
+ agent:
+ experimental:
+ indy: true
+ common:
+ default:
+ enabled: false
+ runtime_telemetry:
+ enabled: false
+ example_instrumentation:
+ string_key: value
+ bool_key: true
+ int_key: 1
+ double_key: 1.1
+ list_key:
+ - value1
+ - value2
+ - true
+ map_key:
+ string_key1: value1
+ string_key2: value2
+ bool_key: true
diff --git a/gcp-auth-extension/build.gradle.kts b/gcp-auth-extension/build.gradle.kts
index f81e5e521..093864c61 100644
--- a/gcp-auth-extension/build.gradle.kts
+++ b/gcp-auth-extension/build.gradle.kts
@@ -15,12 +15,15 @@ val agent: Configuration by configurations.creating {
dependencies {
implementation(platform("org.springframework.boot:spring-boot-dependencies:2.7.18"))
+ 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
@@ -36,6 +39,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..572baa8b8 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
@@ -8,8 +8,8 @@
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException;
import java.util.Locale;
-import java.util.Optional;
-import java.util.function.Supplier;
+import java.util.function.BiFunction;
+import javax.annotation.Nullable;
/**
* An enum representing configurable options for a GCP Authentication Extension. Each option has a
@@ -92,57 +92,41 @@ String getUserReadableName() {
}
/**
- * Retrieves the configured value for this option. This method checks the environment variable
- * first and then the system property.
+ * Retrieves the configured value for this option.
*
* @return The configured value as a string, or throws an exception if not configured.
* @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()) {
- return configuredValue;
- } else {
+ T getRequiredConfiguredValue(
+ ConfigProperties configProperties, BiFunction extractor) {
+ T configuredValue = getConfiguredValue(configProperties, extractor);
+ if (configuredValue == null) {
throw new ConfigurationException(
String.format(
- "GCP Authentication Extension not configured properly: %s not configured. Configure it by exporting environment variable %s or system property %s",
+ "GCP Authentication Extension not configured properly: %s not configured. "
+ + "Configure it by exporting environment variable %s or system property %s",
this.userReadableName, this.getEnvironmentVariable(), this.getSystemProperty()));
}
+ return configuredValue;
}
/**
- * Retrieves the value for this option, prioritizing environment variables and system properties.
- * If neither an environment variable nor a system property is set for this option, the provided
- * fallback function is used to determine the value.
+ * Retrieves the configured value for this option.
*
- * @param fallback A {@link Supplier} that provides the default value for the option when it is
- * not explicitly configured via an environment variable or system property.
- * @return The configured value for the option, obtained from the environment variable, system
- * property, or the fallback function, in that order of precedence.
+ * @return The configured value as a string, or {@code null} if not configured.
*/
- String getConfiguredValueWithFallback(
- ConfigProperties configProperties, Supplier fallback) {
- try {
- return this.getConfiguredValue(configProperties);
- } catch (ConfigurationException e) {
- return fallback.get();
+ @Nullable
+ 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
+ }
}
- }
- /**
- * Retrieves the value for this option, prioritizing environment variables before system
- * properties. If neither an environment variable nor a system property is set for this option,
- * then an empty {@link Optional} is returned.
- *
- * @return The configured value for the option, if set, obtained from the environment variable,
- * system property, or empty {@link Optional}, in that order of precedence.
- */
- Optional getConfiguredValueAsOptional(ConfigProperties configProperties) {
- try {
- return Optional.of(this.getConfiguredValue(configProperties));
- } catch (ConfigurationException e) {
- return Optional.empty();
- }
+ return configuredValue;
}
}
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..5b3cc925d 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
@@ -7,6 +7,7 @@
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auto.service.AutoService;
+import com.google.common.annotations.VisibleForTesting;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.contrib.gcp.auth.GoogleAuthException.Reason;
@@ -25,7 +26,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 +85,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 +97,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 +108,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 +125,63 @@ public int order() {
private static SpanExporter customizeSpanExporter(
SpanExporter exporter, GoogleCredentials credentials, ConfigProperties configProperties) {
- if (isSignalTargeted(SIGNAL_TYPE_TRACES, configProperties)) {
+ if (shouldCustomizeExporter(
+ 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 (shouldCustomizeExporter(
+ SIGNAL_TYPE_METRICS, SIGNAL_TARGET_WARNING_FIX_SUGGESTION, configProperties)) {
return addAuthorizationHeaders(exporter, credentials, configProperties);
+ }
+ return exporter;
+ }
+
+ /**
+ * Utility method to check whether OTLP exporters should be customized for the given target
+ * signal.
+ *
+ * @param signal The target signal to check against. Could be one of {@value SIGNAL_TYPE_TRACES},
+ * {@value SIGNAL_TYPE_METRICS} or {@value SIGNAL_TYPE_ALL}.
+ * @param fixSuggestion A warning to alert the user that auth extension is not configured for the
+ * provided target signal.
+ * @param configProperties The {@link ConfigProperties} object used to configure the extension.
+ * @return A boolean indicating whether the OTLP exporters should be customized or not.
+ */
+ static boolean shouldCustomizeExporter(
+ 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));
}
+ @VisibleForTesting
+ static List targetSignals(ConfigProperties configProperties) {
+ return Objects.requireNonNull(
+ ConfigurableOption.GOOGLE_OTEL_AUTH_TARGET_SIGNALS.getConfiguredValue(
+ configProperties,
+ (properties, name) ->
+ properties.getList(name, Collections.singletonList(SIGNAL_TYPE_ALL))));
+ }
+
// Adds authorization headers to the calls made by the OtlpGrpcSpanExporter and
// OtlpHttpSpanExporter.
private static SpanExporter addAuthorizationHeaders(
@@ -193,7 +218,7 @@ private static MetricExporter addAuthorizationHeaders(
return exporter;
}
- private static Map getRequiredHeaderMap(
+ static Map getRequiredHeaderMap(
GoogleCredentials credentials, ConfigProperties configProperties) {
Map> gcpHeaders;
try {
@@ -216,23 +241,31 @@ 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 Optional.ofNullable(
+ ConfigurableOption.GOOGLE_CLOUD_QUOTA_PROJECT.getConfiguredValue(
+ configProperties, ConfigProperties::getString));
+ }
+
// 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.getRequiredConfiguredValue(
+ configProperties, ConfigProperties::getString);
+ }
}
diff --git a/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthDeclarativeConfigurationCustomizerProvider.java b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthDeclarativeConfigurationCustomizerProvider.java
new file mode 100644
index 000000000..0883750d6
--- /dev/null
+++ b/gcp-auth-extension/src/main/java/io/opentelemetry/contrib/gcp/auth/GcpAuthDeclarativeConfigurationCustomizerProvider.java
@@ -0,0 +1,172 @@
+/*
+ * 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.shouldCustomizeExporter;
+
+import com.google.auth.oauth2.GoogleCredentials;
+import com.google.auto.service.AutoService;
+import com.google.common.annotations.VisibleForTesting;
+import io.opentelemetry.contrib.sdk.autoconfigure.ConfigPropertiesUtil;
+import io.opentelemetry.contrib.sdk.autoconfigure.DeclarativeConfigPropertiesBridgeBuilder;
+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.SdkConfigProvider;
+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 GcpAuthDeclarativeConfigurationCustomizerProvider
+ 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(),
+ getConfigProperties(model));
+
+ return model;
+ });
+ }
+
+ @VisibleForTesting
+ static void customize(
+ OpenTelemetryConfigurationModel model,
+ GoogleCredentials credentials,
+ ConfigProperties configProperties) {
+ Map headerMap =
+ GcpAuthAutoConfigurationCustomizerProvider.getRequiredHeaderMap(
+ credentials, configProperties);
+ customizeMeter(model, headerMap, configProperties);
+ customizeTracer(model, headerMap, configProperties);
+ }
+
+ static ConfigProperties getConfigProperties(OpenTelemetryConfigurationModel model) {
+ return new DeclarativeConfigPropertiesBridgeBuilder()
+ .buildFromInstrumentationConfig(SdkConfigProvider.create(model).getInstrumentationConfig());
+ }
+
+ private static void customizeMeter(
+ OpenTelemetryConfigurationModel model,
+ Map headerMap,
+ ConfigProperties configProperties) {
+ MeterProviderModel meterProvider = model.getMeterProvider();
+ if (meterProvider == null) {
+ return;
+ }
+
+ if (shouldCustomizeExporter(
+ 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 (shouldCustomizeExporter(
+ 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/GcpAuthDeclarativeConfigurationCustomizerProviderTest.java b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthDeclarativeConfigurationCustomizerProviderTest.java
new file mode 100644
index 000000000..ca234a0e3
--- /dev/null
+++ b/gcp-auth-extension/src/test/java/io/opentelemetry/contrib/gcp/auth/GcpAuthDeclarativeConfigurationCustomizerProviderTest.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.GcpAuthDeclarativeConfigurationCustomizerProvider.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.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 GcpAuthDeclarativeConfigurationCustomizerProviderTest {
+
+ @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 =
+ GcpAuthDeclarativeConfigurationCustomizerProvider.getConfigProperties(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")));
+
+ GcpAuthDeclarativeConfigurationCustomizerProvider.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/settings.gradle.kts b/settings.gradle.kts
index 82364bf2a..4d2bc0bb6 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")
diff --git a/span-stacktrace/build.gradle.kts b/span-stacktrace/build.gradle.kts
index 4033b0177..a9fa79e77 100644
--- a/span-stacktrace/build.gradle.kts
+++ b/span-stacktrace/build.gradle.kts
@@ -7,6 +7,8 @@ description = "OpenTelemetry Java span stacktrace capture module"
otelJava.moduleName.set("io.opentelemetry.contrib.stacktrace")
dependencies {
+ implementation(project(":declarative-config-bridge"))
+
annotationProcessor("com.google.auto.service:auto-service")
compileOnly("com.google.auto.service:auto-service-annotations")
@@ -15,8 +17,10 @@ dependencies {
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
+ compileOnly("io.opentelemetry:opentelemetry-sdk-extension-incubator")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure")
testImplementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi")
+ testImplementation("io.opentelemetry:opentelemetry-sdk-extension-incubator")
compileOnly("io.opentelemetry.semconv:opentelemetry-semconv")
testImplementation("io.opentelemetry.semconv:opentelemetry-semconv")
diff --git a/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceAutoConfig.java b/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceAutoConfig.java
index 2315d2a10..189934689 100644
--- a/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceAutoConfig.java
+++ b/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceAutoConfig.java
@@ -23,25 +23,26 @@ public class StackTraceAutoConfig implements AutoConfigurationCustomizerProvider
private static final Logger log = Logger.getLogger(StackTraceAutoConfig.class.getName());
- private static final String CONFIG_MIN_DURATION =
- "otel.java.experimental.span-stacktrace.min.duration";
+ static final String PREFIX = "otel.java.experimental.span-stacktrace.";
+ private static final String CONFIG_MIN_DURATION = PREFIX + "min.duration";
private static final Duration CONFIG_MIN_DURATION_DEFAULT = Duration.ofMillis(5);
-
- private static final String CONFIG_FILTER = "otel.java.experimental.span-stacktrace.filter";
+ private static final String CONFIG_FILTER = PREFIX + "filter";
@Override
public void customize(AutoConfigurationCustomizer config) {
config.addTracerProviderCustomizer(
(providerBuilder, properties) -> {
- long minDuration = getMinDuration(properties);
- if (minDuration >= 0) {
- Predicate filter = getFilterPredicate(properties);
- providerBuilder.addSpanProcessor(new StackTraceSpanProcessor(minDuration, filter));
+ if (getMinDuration(properties) >= 0) {
+ providerBuilder.addSpanProcessor(create(properties));
}
return providerBuilder;
});
}
+ static StackTraceSpanProcessor create(ConfigProperties properties) {
+ return new StackTraceSpanProcessor(getMinDuration(properties), getFilterPredicate(properties));
+ }
+
// package-private for testing
static long getMinDuration(ConfigProperties properties) {
long minDuration =
diff --git a/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceComponentProvider.java b/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceComponentProvider.java
new file mode 100644
index 000000000..cfa36ae6d
--- /dev/null
+++ b/span-stacktrace/src/main/java/io/opentelemetry/contrib/stacktrace/StackTraceComponentProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.stacktrace;
+
+import com.google.auto.service.AutoService;
+import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties;
+import io.opentelemetry.contrib.sdk.autoconfigure.DeclarativeConfigPropertiesBridgeBuilder;
+import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider;
+import io.opentelemetry.sdk.trace.SpanProcessor;
+
+@SuppressWarnings("rawtypes")
+@AutoService(ComponentProvider.class)
+public class StackTraceComponentProvider implements ComponentProvider {
+ @Override
+ public String getName() {
+ return "experimental-stacktrace";
+ }
+
+ @Override
+ public SpanProcessor create(DeclarativeConfigProperties config) {
+ return StackTraceAutoConfig.create(
+ new DeclarativeConfigPropertiesBridgeBuilder()
+ .addTranslation(StackTraceAutoConfig.PREFIX, "")
+ .resolveConfig(config));
+ }
+
+ @Override
+ public Class getType() {
+ return SpanProcessor.class;
+ }
+}
diff --git a/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceComponentProviderTest.java b/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceComponentProviderTest.java
new file mode 100644
index 000000000..a2b4c4212
--- /dev/null
+++ b/span-stacktrace/src/test/java/io/opentelemetry/contrib/stacktrace/StackTraceComponentProviderTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.contrib.stacktrace;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import io.opentelemetry.sdk.OpenTelemetrySdk;
+import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration;
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import org.junit.jupiter.api.Test;
+
+class StackTraceComponentProviderTest {
+ @Test
+ void endToEnd() {
+ String yaml =
+ "file_format: 0.4\n"
+ + "tracer_provider:\n"
+ + " processors:\n"
+ + " - experimental-stacktrace:\n";
+
+ OpenTelemetrySdk openTelemetrySdk =
+ DeclarativeConfiguration.parseAndCreate(
+ new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8)));
+
+ assertThat(openTelemetrySdk.getSdkTracerProvider().toString())
+ .contains("StackTraceSpanProcessor");
+ }
+}