diff --git a/sdk-extensions/autoconfigure/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts index a00685f6e6d..72d6b88bec6 100644 --- a/sdk-extensions/autoconfigure/build.gradle.kts +++ b/sdk-extensions/autoconfigure/build.gradle.kts @@ -86,6 +86,14 @@ testing { implementation(project(":sdk:testing")) } } + + register("testDeclarativeConfigSpi") { + dependencies { + implementation(project(":sdk-extensions:incubator")) + implementation(project(":exporters:logging")) + implementation(project(":sdk:testing")) + } + } } } diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java index 1b0e43c1f12..7f1a5678959 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/AutoConfiguredOpenTelemetrySdkBuilder.java @@ -445,6 +445,21 @@ public AutoConfiguredOpenTelemetrySdk build() { } private AutoConfiguredOpenTelemetrySdk buildImpl() { + AutoConfiguredOpenTelemetrySdk fromFileConfiguration = + maybeConfigureFromFile( + this.config != null + ? this.config + : DefaultConfigProperties.create(Collections.emptyMap(), componentLoader), + componentLoader); + if (fromFileConfiguration != null) { + maybeRegisterShutdownHook(fromFileConfiguration.getOpenTelemetrySdk()); + Object configProvider = fromFileConfiguration.getConfigProvider(); + if (setResultAsGlobal && INCUBATOR_AVAILABLE && configProvider != null) { + IncubatingUtil.setGlobalConfigProvider(configProvider); + } + return fromFileConfiguration; + } + SpiHelper spiHelper = SpiHelper.create(componentLoader); if (!customized) { customized = true; @@ -454,20 +469,8 @@ private AutoConfiguredOpenTelemetrySdk buildImpl() { customizer.customize(this); } } - ConfigProperties config = getConfig(); - AutoConfiguredOpenTelemetrySdk fromFileConfiguration = - maybeConfigureFromFile(config, componentLoader); - if (fromFileConfiguration != null) { - maybeRegisterShutdownHook(fromFileConfiguration.getOpenTelemetrySdk()); - Object configProvider = fromFileConfiguration.getConfigProvider(); - if (setResultAsGlobal && INCUBATOR_AVAILABLE && configProvider != null) { - IncubatingUtil.setGlobalConfigProvider(configProvider); - } - return fromFileConfiguration; - } - Resource resource = ResourceConfiguration.configureResource(config, spiHelper, resourceCustomizer); @@ -571,6 +574,14 @@ void configureSdk( @Nullable private static AutoConfiguredOpenTelemetrySdk maybeConfigureFromFile( ConfigProperties config, ComponentLoader componentLoader) { + if (INCUBATOR_AVAILABLE) { + AutoConfiguredOpenTelemetrySdk sdk = IncubatingUtil.configureFromSpi(componentLoader); + if (sdk != null) { + logger.fine("Autoconfigured from SPI by opentelemetry-sdk-extension-incubator"); + return sdk; + } + } + String otelConfigFile = config.getString("otel.config.file"); if (otelConfigFile != null && !otelConfigFile.isEmpty()) { logger.warning( diff --git a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtil.java b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtil.java index df2686ece55..ce700181c93 100644 --- a/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtil.java +++ b/sdk-extensions/autoconfigure/src/main/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtil.java @@ -5,6 +5,8 @@ package io.opentelemetry.sdk.autoconfigure; +import static java.util.Objects.requireNonNull; + import io.opentelemetry.api.incubator.config.ConfigProvider; import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.api.incubator.config.GlobalConfigProvider; @@ -18,8 +20,8 @@ import java.io.InputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.util.Objects; import java.util.logging.Logger; +import javax.annotation.Nullable; /** * Utilities for interacting with incubating components ({@code @@ -32,54 +34,111 @@ final class IncubatingUtil { private IncubatingUtil() {} + // Visible for testing + interface Factory { + @Nullable + AutoConfiguredOpenTelemetrySdk create() + throws ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException; + } + static AutoConfiguredOpenTelemetrySdk configureFromFile( Logger logger, String configurationFile, ComponentLoader componentLoader) { logger.fine("Autoconfiguring from configuration file: " + configurationFile); try (FileInputStream fis = new FileInputStream(configurationFile)) { - Class declarativeConfiguration = - Class.forName( - "io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration"); - Method parse = declarativeConfiguration.getMethod("parse", InputStream.class); - Object model = parse.invoke(null, fis); - Class openTelemetryConfiguration = - Class.forName( - "io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel"); - Method create = - declarativeConfiguration.getMethod( - "create", openTelemetryConfiguration, ComponentLoader.class); - OpenTelemetrySdk sdk = (OpenTelemetrySdk) create.invoke(null, model, componentLoader); - Class sdkConfigProvider = - Class.forName("io.opentelemetry.sdk.extension.incubator.fileconfig.SdkConfigProvider"); - Method createFileConfigProvider = - sdkConfigProvider.getMethod("create", openTelemetryConfiguration, ComponentLoader.class); - ConfigProvider configProvider = - (ConfigProvider) createFileConfigProvider.invoke(null, model, componentLoader); - // Note: can't access file configuration resource without reflection so setting a dummy - // resource - return AutoConfiguredOpenTelemetrySdk.create( - sdk, Resource.getDefault(), null, configProvider); + return requireNonNull( + createWithFactory( + "file", + () -> + getOpenTelemetrySdk( + Class.forName( + "io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration") + .getMethod("parse", InputStream.class) + .invoke(null, fis), + componentLoader))); } catch (FileNotFoundException e) { throw new ConfigurationException("Configuration file not found", e); + } catch (IOException e) { + // IOException (other than FileNotFoundException which is caught above) is only thrown + // above by FileInputStream.close() + throw new ConfigurationException("Error closing file", e); + } + } + + @Nullable + public static AutoConfiguredOpenTelemetrySdk configureFromSpi(ComponentLoader componentLoader) { + return createWithFactory( + "SPI", + () -> { + Class providerClass = + Class.forName( + "io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider"); + Method getConfigurationModel = providerClass.getMethod("getConfigurationModel"); + + for (Object configProvider : componentLoader.load(providerClass)) { + Object model = getConfigurationModel.invoke(configProvider); + if (model != null) { + return getOpenTelemetrySdk(model, componentLoader); + } + } + return null; + }); + } + + private static AutoConfiguredOpenTelemetrySdk getOpenTelemetrySdk( + Object model, ComponentLoader componentLoader) + throws IllegalAccessException, + InvocationTargetException, + ClassNotFoundException, + NoSuchMethodException { + + Class openTelemetryConfiguration = + Class.forName( + "io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel"); + Class declarativeConfiguration = + Class.forName( + "io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration"); + Method create = + declarativeConfiguration.getMethod( + "create", openTelemetryConfiguration, ComponentLoader.class); + + OpenTelemetrySdk sdk = (OpenTelemetrySdk) create.invoke(null, model, componentLoader); + Class sdkConfigProvider = + Class.forName("io.opentelemetry.sdk.extension.incubator.fileconfig.SdkConfigProvider"); + Method createFileConfigProvider = + sdkConfigProvider.getMethod("create", openTelemetryConfiguration, ComponentLoader.class); + ConfigProvider configProvider = + (ConfigProvider) createFileConfigProvider.invoke(null, model, componentLoader); + // Note: can't access file configuration resource without reflection so setting a dummy + // resource + return AutoConfiguredOpenTelemetrySdk.create(sdk, Resource.getDefault(), null, configProvider); + } + + // Visible for testing + @Nullable + static AutoConfiguredOpenTelemetrySdk createWithFactory(String name, Factory factory) { + try { + return factory.create(); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { throw new ConfigurationException( - "Error configuring from file. Is opentelemetry-sdk-extension-incubator on the classpath?", + String.format( + "Error configuring from %s. Is opentelemetry-sdk-extension-incubator on the classpath?", + name), e); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof DeclarativeConfigException) { throw toConfigurationException((DeclarativeConfigException) cause); } - throw new ConfigurationException("Unexpected error configuring from file", e); - } catch (IOException e) { - // IOException (other than FileNotFoundException which is caught above) is only thrown - // above by FileInputStream.close() - throw new ConfigurationException("Error closing file", e); + throw new ConfigurationException("Unexpected error configuring from " + name, e); } } private static ConfigurationException toConfigurationException( DeclarativeConfigException exception) { - String message = Objects.requireNonNull(exception.getMessage()); + String message = requireNonNull(exception.getMessage()); return new ConfigurationException(message, exception); } diff --git a/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationSpiTest.java b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationSpiTest.java new file mode 100644 index 00000000000..a3f0d6892ac --- /dev/null +++ b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationSpiTest.java @@ -0,0 +1,48 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; + +import io.opentelemetry.exporter.logging.LoggingSpanExporter; +import io.opentelemetry.internal.testing.CleanupExtension; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class DeclarativeConfigurationSpiTest { + + @RegisterExtension private static final CleanupExtension cleanup = new CleanupExtension(); + + @Test + void configFromSpi() { + OpenTelemetrySdk expectedSdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .setResource( + Resource.getDefault().toBuilder().put("service.name", "test").build()) + .addSpanProcessor(SimpleSpanProcessor.create(LoggingSpanExporter.create())) + .build()) + .build(); + cleanup.addCloseable(expectedSdk); + AutoConfiguredOpenTelemetrySdkBuilder builder = spy(AutoConfiguredOpenTelemetrySdk.builder()); + Thread thread = new Thread(); + doReturn(thread).when(builder).shutdownHook(any()); + + AutoConfiguredOpenTelemetrySdk autoConfiguredOpenTelemetrySdk = builder.build(); + cleanup.addCloseable(autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk()); + + assertThat(autoConfiguredOpenTelemetrySdk.getOpenTelemetrySdk().toString()) + .isEqualTo(expectedSdk.toString()); + } +} diff --git a/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtilTest.java b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtilTest.java new file mode 100644 index 00000000000..12cb8274ed8 --- /dev/null +++ b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/IncubatingUtilTest.java @@ -0,0 +1,44 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThatCode; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigurationException; +import java.lang.reflect.InvocationTargetException; +import org.junit.jupiter.api.Test; + +class IncubatingUtilTest { + + @Test + void classNotFoundException() { + assertThatCode( + () -> + IncubatingUtil.createWithFactory( + "test", + () -> { + Class.forName("foo"); + return null; + })) + .isInstanceOf(ConfigurationException.class) + .hasMessage( + "Error configuring from test. Is opentelemetry-sdk-extension-incubator on the classpath?"); + } + + @Test + void invocationTargetException() { + assertThatCode( + () -> + IncubatingUtil.createWithFactory( + "test", + () -> { + throw new InvocationTargetException(new RuntimeException("test exception")); + })) + .isInstanceOf(ConfigurationException.class) + .hasMessage("Unexpected error configuring from test") + .hasRootCauseMessage("test exception"); + } +} diff --git a/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/TestDeclarativeConfigurationProvider.java b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/TestDeclarativeConfigurationProvider.java new file mode 100644 index 00000000000..28a33710394 --- /dev/null +++ b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/java/io/opentelemetry/sdk/autoconfigure/TestDeclarativeConfigurationProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.autoconfigure; + +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfiguration; +import io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; + +public class TestDeclarativeConfigurationProvider implements DeclarativeConfigurationProvider { + @Override + public OpenTelemetryConfigurationModel getConfigurationModel() { + String yaml = + "file_format: \"1.0-rc.1\"\n" + + "resource:\n" + + " attributes:\n" + + " - name: service.name\n" + + " value: test\n" + + "tracer_provider:\n" + + " processors:\n" + + " - simple:\n" + + " exporter:\n" + + " console: {}\n"; + + return DeclarativeConfiguration.parse( + new ByteArrayInputStream(yaml.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider new file mode 100644 index 00000000000..80610d72e48 --- /dev/null +++ b/sdk-extensions/autoconfigure/src/testDeclarativeConfigSpi/resources/META-INF/services/io.opentelemetry.sdk.extension.incubator.fileconfig.DeclarativeConfigurationProvider @@ -0,0 +1 @@ +io.opentelemetry.sdk.autoconfigure.TestDeclarativeConfigurationProvider diff --git a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java index a29a9f581d1..62d00718e7b 100644 --- a/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java +++ b/sdk-extensions/autoconfigure/src/testIncubating/java/io/opentelemetry/sdk/autoconfigure/DeclarativeConfigurationTest.java @@ -96,10 +96,9 @@ void configFile_fileNotFound() { assertThatThrownBy( () -> AutoConfiguredOpenTelemetrySdk.builder() - .addPropertiesSupplier(() -> singletonMap("otel.config.file", "foo")) - .addPropertiesSupplier( - () -> singletonMap("otel.experimental.config.file", "foo")) - .addPropertiesSupplier(() -> singletonMap("otel.sdk.disabled", "true")) + .setConfig( + DefaultConfigProperties.createFromMap( + Collections.singletonMap("otel.experimental.config.file", "foo"))) .build()) .isInstanceOf(ConfigurationException.class) .hasMessageContaining("Configuration file not found"); diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationProvider.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationProvider.java new file mode 100644 index 00000000000..37c181f9401 --- /dev/null +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationProvider.java @@ -0,0 +1,19 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import javax.annotation.Nullable; + +/** A service provider interface (SPI) for providing a declarative configuration model. */ +public interface DeclarativeConfigurationProvider { + /** + * Returns an OpenTelemetry configuration model to be used when configuring the SDK, or {@code + * null} if no configuration is provided by this provider. + */ + @Nullable + OpenTelemetryConfigurationModel getConfigurationModel(); +}