diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java index ddecba22df1..663cf374b6d 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigContext.java @@ -36,6 +36,7 @@ class DeclarativeConfigContext { @Nullable private Resource resource = null; @Nullable private ConfigProvider configProvider; @Nullable private List componentProviders = null; + @Nullable private DeclarativeConfigurationBuilder builder; // Visible for testing DeclarativeConfigContext(SpiHelper spiHelper) { @@ -79,6 +80,14 @@ void setResource(Resource resource) { this.resource = resource; } + void setBuilder(DeclarativeConfigurationBuilder builder) { + this.builder = builder; + } + + DeclarativeConfigurationBuilder getBuilder() { + return Objects.requireNonNull(builder, "builder has not been set"); + } + /** * Overload of {@link #setInternalTelemetry(Consumer, Consumer)} for components which do not * support setting {@link InternalTelemetryVersion} because they only support {@link diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java index 169a99fc3c9..6d351386e1d 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfiguration.java @@ -160,6 +160,7 @@ public static ExtendedOpenTelemetrySdk create( private static ExtendedOpenTelemetrySdk create( OpenTelemetryConfigurationModel configurationModel, DeclarativeConfigContext context) { DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + context.setBuilder(builder); SpiHelper spiHelper = context.getSpiHelper(); for (DeclarativeConfigurationCustomizerProvider provider : diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilder.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilder.java index c96aa629fe9..6ec4bebc3ce 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilder.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilder.java @@ -5,20 +5,68 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; /** Builder for the declarative configuration. */ public class DeclarativeConfigurationBuilder implements DeclarativeConfigurationCustomizer { private Function modelCustomizer = Function.identity(); + private final List> spanExporterCustomizers = new ArrayList<>(); + private final List> metricExporterCustomizers = new ArrayList<>(); + private final List> logRecordExporterCustomizers = + new ArrayList<>(); + @Override public void addModelCustomizer( Function customizer) { modelCustomizer = mergeCustomizer(modelCustomizer, customizer); } + @Override + public void addSpanExporterCustomizer( + Class exporterType, BiFunction customizer) { + spanExporterCustomizers.add(new Customizer<>(exporterType, customizer)); + } + + @Override + public void addMetricExporterCustomizer( + Class exporterType, BiFunction customizer) { + metricExporterCustomizers.add(new Customizer<>(exporterType, customizer)); + } + + @Override + public void addLogRecordExporterCustomizer( + Class exporterType, BiFunction customizer) { + logRecordExporterCustomizers.add(new Customizer<>(exporterType, customizer)); + } + + List> getSpanExporterCustomizers() { + return Collections.unmodifiableList(spanExporterCustomizers); + } + + List> getMetricExporterCustomizers() { + return Collections.unmodifiableList(metricExporterCustomizers); + } + + List> getLogRecordExporterCustomizers() { + return Collections.unmodifiableList(logRecordExporterCustomizers); + } + private static Function mergeCustomizer( Function first, Function second) { return (I configured) -> { @@ -32,4 +80,37 @@ public OpenTelemetryConfigurationModel customizeModel( OpenTelemetryConfigurationModel configurationModel) { return modelCustomizer.apply(configurationModel); } + + static class Customizer { + private static final Logger logger = Logger.getLogger(Customizer.class.getName()); + + private final Class exporterType; + private final BiFunction customizer; + + @SuppressWarnings("unchecked") + Customizer( + Class exporterType, BiFunction customizer) { + this.exporterType = exporterType; + this.customizer = (BiFunction) customizer; + } + + T maybeCustomize(T exporter, String name, DeclarativeConfigProperties properties) { + if (!exporterType.isInstance(exporter)) { + return exporter; + } + T customized = customizer.apply(exporter, properties); + if (customized == null) { + throw new DeclarativeConfigException( + "Customizer returned null for " + exporterType.getSimpleName() + ": " + name); + } + if (customized != exporter && exporter instanceof Closeable) { + try { + ((Closeable) exporter).close(); + } catch (IOException e) { + logger.log(Level.WARNING, "Failed to close exporter after customization", e); + } + } + return customized; + } + } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCustomizer.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCustomizer.java index 3e8e327355c..727accff05c 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCustomizer.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCustomizer.java @@ -5,7 +5,12 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.function.BiFunction; import java.util.function.Function; /** A service provider interface (SPI) for customizing declarative configuration. */ @@ -18,4 +23,43 @@ public interface DeclarativeConfigurationCustomizer { */ void addModelCustomizer( Function customizer); + + /** + * Add customizer for {@link SpanExporter} instances created from declarative configuration. + * Multiple customizers compose in registration order. + * + * @param exporterType the exporter type to customize + * @param customizer function receiving (exporter, properties) and returning customized exporter; + * must not return null + * @param the exporter type + */ + void addSpanExporterCustomizer( + Class exporterType, BiFunction customizer); + + /** + * Add customizer for {@link MetricExporter} instances created from declarative configuration. + * Multiple customizers compose in registration order. + * + * @param exporterType the exporter type to customize + * @param customizer function receiving (exporter, properties) and returning customized exporter; + * must not return null + * @param the exporter type + */ + void addMetricExporterCustomizer( + Class exporterType, BiFunction customizer); + + /** + * Add customizer for {@link LogRecordExporter} instances created from declarative configuration. + * Multiple customizers compose in registration order. + * + *

If the customizer wraps the exporter in a new {@link java.io.Closeable} instance, the + * customizer is responsible for resource cleanup. + * + * @param exporterType the exporter type to customize + * @param customizer function receiving (exporter, properties) and returning customized exporter; + * must not return null + * @param the exporter type + */ + void addLogRecordExporterCustomizer( + Class exporterType, BiFunction customizer); } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactory.java index dec45d47ae8..350a3c02621 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactory.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactory.java @@ -21,6 +21,14 @@ static LogRecordExporterFactory getInstance() { public LogRecordExporter create(LogRecordExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue logRecordExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "log record exporter"); - return context.loadComponent(LogRecordExporter.class, logRecordExporterKeyValue); + LogRecordExporter exporter = + context.loadComponent(LogRecordExporter.class, logRecordExporterKeyValue); + for (DeclarativeConfigurationBuilder.Customizer customizer : + context.getBuilder().getLogRecordExporterCustomizers()) { + exporter = + customizer.maybeCustomize( + exporter, logRecordExporterKeyValue.getKey(), logRecordExporterKeyValue.getValue()); + } + return exporter; } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactory.java index a093cebe884..c884e42b753 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactory.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactory.java @@ -21,6 +21,13 @@ static MetricExporterFactory getInstance() { public MetricExporter create(PushMetricExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue metricExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "metric exporter"); - return context.loadComponent(MetricExporter.class, metricExporterKeyValue); + MetricExporter exporter = context.loadComponent(MetricExporter.class, metricExporterKeyValue); + for (DeclarativeConfigurationBuilder.Customizer customizer : + context.getBuilder().getMetricExporterCustomizers()) { + exporter = + customizer.maybeCustomize( + exporter, metricExporterKeyValue.getKey(), metricExporterKeyValue.getValue()); + } + return exporter; } } diff --git a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactory.java b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactory.java index 9fb740a5ef7..11bf3dfbf93 100644 --- a/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactory.java +++ b/sdk-extensions/incubator/src/main/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactory.java @@ -22,6 +22,13 @@ static SpanExporterFactory getInstance() { public SpanExporter create(SpanExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue spanExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "span exporter"); - return context.loadComponent(SpanExporter.class, spanExporterKeyValue); + SpanExporter exporter = context.loadComponent(SpanExporter.class, spanExporterKeyValue); + for (DeclarativeConfigurationBuilder.Customizer customizer : + context.getBuilder().getSpanExporterCustomizers()) { + exporter = + customizer.maybeCustomize( + exporter, spanExporterKeyValue.getKey(), spanExporterKeyValue.getValue()); + } + return exporter; } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilderTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilderTest.java new file mode 100644 index 00000000000..d1abd33fcd9 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilderTest.java @@ -0,0 +1,116 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.extension.incubator.fileconfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import org.junit.jupiter.api.Test; + +class DeclarativeConfigurationBuilderTest { + + @Test + void spanExporterCustomizer_Single() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + + builder.addSpanExporterCustomizer( + SpanExporter.class, (exporter, properties) -> mock(SpanExporter.class)); + + assertThat(builder.getSpanExporterCustomizers()).hasSize(1); + } + + @Test + void spanExporterCustomizer_Multiple_Compose() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + + builder.addSpanExporterCustomizer( + SpanExporter.class, (exporter, properties) -> mock(SpanExporter.class)); + builder.addSpanExporterCustomizer( + SpanExporter.class, (exporter, properties) -> mock(SpanExporter.class)); + + assertThat(builder.getSpanExporterCustomizers()).hasSize(2); + } + + @Test + void metricExporterCustomizer_Single() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + + builder.addMetricExporterCustomizer( + MetricExporter.class, (exporter, properties) -> mock(MetricExporter.class)); + + assertThat(builder.getMetricExporterCustomizers()).hasSize(1); + } + + @Test + void metricExporterCustomizer_Multiple_Compose() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + + builder.addMetricExporterCustomizer( + MetricExporter.class, (exporter, properties) -> mock(MetricExporter.class)); + builder.addMetricExporterCustomizer( + MetricExporter.class, (exporter, properties) -> mock(MetricExporter.class)); + + assertThat(builder.getMetricExporterCustomizers()).hasSize(2); + } + + @Test + void logRecordExporterCustomizer_Single() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + + builder.addLogRecordExporterCustomizer( + LogRecordExporter.class, (exporter, properties) -> mock(LogRecordExporter.class)); + + assertThat(builder.getLogRecordExporterCustomizers()).hasSize(1); + } + + @Test + void logRecordExporterCustomizer_Multiple_Compose() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + + builder.addLogRecordExporterCustomizer( + LogRecordExporter.class, (exporter, properties) -> mock(LogRecordExporter.class)); + builder.addLogRecordExporterCustomizer( + LogRecordExporter.class, (exporter, properties) -> mock(LogRecordExporter.class)); + + assertThat(builder.getLogRecordExporterCustomizers()).hasSize(2); + } + + @Test + void customizer_ClosesOriginalWhenReplaced() throws Exception { + SpanExporter original = mock(SpanExporter.class); + SpanExporter replacement = mock(SpanExporter.class); + DeclarativeConfigProperties props = mock(DeclarativeConfigProperties.class); + + DeclarativeConfigurationBuilder.Customizer customizer = + new DeclarativeConfigurationBuilder.Customizer<>( + SpanExporter.class, (exporter, properties) -> replacement); + + SpanExporter result = customizer.maybeCustomize(original, "test", props); + + assertThat(result).isSameAs(replacement); + verify(original).close(); + } + + @Test + void customizer_DoesNotCloseWhenSameInstance() throws Exception { + SpanExporter exporter = mock(SpanExporter.class); + DeclarativeConfigProperties props = mock(DeclarativeConfigProperties.class); + + DeclarativeConfigurationBuilder.Customizer customizer = + new DeclarativeConfigurationBuilder.Customizer<>(SpanExporter.class, (e, properties) -> e); + + SpanExporter result = customizer.maybeCustomize(exporter, "test", props); + + assertThat(result).isSameAs(exporter); + verify(exporter, never()).close(); + } +} diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java index c27bfa11221..83d256003a7 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationCreateTest.java @@ -114,7 +114,7 @@ void parseAndCreate_Exception_CleansUpPartials() { // exporter with OTLP exporter, following by invalid batch exporter which references invalid // exporter "foo". String yaml = - "file_format: \"1.0-rc.1\"\n" + "file_format: \"1.0-rc.3\"\n" + "logger_provider:\n" + " processors:\n" + " - batch:\n" @@ -141,7 +141,7 @@ void parseAndCreate_Exception_CleansUpPartials() { @Test void parseAndCreate_EmptyComponentProviderConfig() { String yaml = - "file_format: \"1.0-rc.1\"\n" + "file_format: \"1.0-rc.3\"\n" + "logger_provider:\n" + " processors:\n" + " - test:\n" @@ -159,7 +159,7 @@ void parseAndCreate_EmptyComponentProviderConfig() { @Test void create_ModelCustomizer() { OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); - model.withFileFormat("1.0-rc.1"); + model.withFileFormat("1.0-rc.3"); model.withTracerProvider( new TracerProviderModel() .withProcessors( diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactoryTest.java index bd9ca17ce94..0894163e260 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordExporterFactoryTest.java @@ -12,12 +12,14 @@ import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.exporter.logging.SystemOutLogRecordExporter; import io.opentelemetry.exporter.logging.otlp.internal.logs.OtlpStdoutLogRecordExporter; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.exporter.otlp.logs.OtlpGrpcLogRecordExporter; import io.opentelemetry.internal.testing.CleanupExtension; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.extension.incubator.fileconfig.component.LogRecordExporterComponentProvider; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ConsoleExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalOtlpFileExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.GrpcTlsModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.HttpTlsModel; @@ -35,6 +37,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -59,6 +62,7 @@ void setup() { capturingComponentLoader = new CapturingComponentLoader(); spiHelper = SpiHelper.create(capturingComponentLoader); context = new DeclarativeConfigContext(spiHelper); + context.setBuilder(new DeclarativeConfigurationBuilder()); } @Test @@ -343,4 +347,78 @@ void create_SpiExporter_Valid() { .config.getString("key1")) .isEqualTo("value1"); } + + @Test + void create_Customizer() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addLogRecordExporterCustomizer( + LogRecordExporter.class, (exporter, properties) -> SystemOutLogRecordExporter.create()); + + LogRecordExporter result = + LogRecordExporterFactory.getInstance() + .create(new LogRecordExporterModel().withConsole(new ConsoleExporterModel()), context); + cleanup.addCloseable(result); + + assertThat(result).isInstanceOf(SystemOutLogRecordExporter.class); + } + + @Test + void create_Customizer_TypeSafe() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addLogRecordExporterCustomizer( + OtlpGrpcLogRecordExporter.class, + (exporter, properties) -> + exporter.toBuilder().setTimeout(Duration.ofSeconds(42)).build()); + + LogRecordExporter result = + LogRecordExporterFactory.getInstance() + .create( + new LogRecordExporterModel().withOtlpGrpc(new OtlpGrpcExporterModel()), context); + cleanup.addCloseable(result); + + assertThat(result).isInstanceOf(OtlpGrpcLogRecordExporter.class); + assertThat(result.toString()).contains("timeoutNanos=42000000000"); + } + + @Test + void create_Customizer_TypeMismatch() { + AtomicInteger callCount = new AtomicInteger(0); + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addLogRecordExporterCustomizer( + OtlpGrpcLogRecordExporter.class, + (exporter, properties) -> { + callCount.incrementAndGet(); + return exporter; + }); + + LogRecordExporter result = + LogRecordExporterFactory.getInstance() + .create(new LogRecordExporterModel().withConsole(new ConsoleExporterModel()), context); + cleanup.addCloseable(result); + + assertThat(callCount.get()).isEqualTo(0); + } + + @Test + void create_Customizer_ReturnsNull() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addLogRecordExporterCustomizer(LogRecordExporter.class, (exporter, properties) -> null); + + assertThatThrownBy( + () -> + LogRecordExporterFactory.getInstance() + .create( + new LogRecordExporterModel().withConsole(new ConsoleExporterModel()), + context)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessageContaining("Customizer returned null for LogRecordExporter: console"); + } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordProcessorFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordProcessorFactoryTest.java index 110f31b5aa8..66f01ad0dbb 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordProcessorFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LogRecordProcessorFactoryTest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -38,6 +39,11 @@ class LogRecordProcessorFactoryTest { new DeclarativeConfigContext( SpiHelper.create(LogRecordProcessorFactoryTest.class.getClassLoader())); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @Test void create_BatchNullExporter() { assertThatThrownBy( diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LoggerProviderFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LoggerProviderFactoryTest.java index d1d5e01f286..1fa93f1d0cd 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LoggerProviderFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/LoggerProviderFactoryTest.java @@ -34,6 +34,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -47,6 +48,11 @@ class LoggerProviderFactoryTest { new DeclarativeConfigContext( SpiHelper.create(LoggerProviderFactoryTest.class.getClassLoader())); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @ParameterizedTest @MethodSource("createArguments") void create(LoggerProviderAndAttributeLimits model, SdkLoggerProvider expectedProvider) { diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MeterProviderFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MeterProviderFactoryTest.java index 2d0a3055a92..98dca51b32e 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MeterProviderFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MeterProviderFactoryTest.java @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -48,6 +49,11 @@ class MeterProviderFactoryTest { new DeclarativeConfigContext( SpiHelper.create(MeterProviderFactoryTest.class.getClassLoader())); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @ParameterizedTest @MethodSource("createArguments") void create(MeterProviderModel model, SdkMeterProvider expectedProvider) { diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactoryTest.java index 884efd83c2a..e6b8f5cc712 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactoryTest.java @@ -41,6 +41,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -65,6 +66,7 @@ void setup() { capturingComponentLoader = new CapturingComponentLoader(); spiHelper = SpiHelper.create(capturingComponentLoader); context = new DeclarativeConfigContext(spiHelper); + context.setBuilder(new DeclarativeConfigurationBuilder()); } @Test @@ -393,4 +395,83 @@ void create_SpiExporter_Valid() { .config.getString("key1")) .isEqualTo("value1"); } + + @Test + void create_Customizer() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addMetricExporterCustomizer( + MetricExporter.class, (exporter, properties) -> LoggingMetricExporter.create()); + + MetricExporter result = + MetricExporterFactory.getInstance() + .create( + new PushMetricExporterModel().withConsole(new ConsoleMetricExporterModel()), + context); + cleanup.addCloseable(result); + + assertThat(result).isInstanceOf(LoggingMetricExporter.class); + } + + @Test + void create_Customizer_TypeSafe() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addMetricExporterCustomizer( + OtlpGrpcMetricExporter.class, + (exporter, properties) -> + exporter.toBuilder().setTimeout(Duration.ofSeconds(42)).build()); + + MetricExporter result = + MetricExporterFactory.getInstance() + .create( + new PushMetricExporterModel().withOtlpGrpc(new OtlpGrpcMetricExporterModel()), + context); + cleanup.addCloseable(result); + + assertThat(result).isInstanceOf(OtlpGrpcMetricExporter.class); + assertThat(result.toString()).contains("timeoutNanos=42000000000"); + } + + @Test + void create_Customizer_TypeMismatch() { + AtomicInteger callCount = new AtomicInteger(0); + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addMetricExporterCustomizer( + OtlpGrpcMetricExporter.class, + (exporter, properties) -> { + callCount.incrementAndGet(); + return exporter; + }); + + MetricExporter result = + MetricExporterFactory.getInstance() + .create( + new PushMetricExporterModel().withConsole(new ConsoleMetricExporterModel()), + context); + cleanup.addCloseable(result); + + assertThat(callCount.get()).isEqualTo(0); + } + + @Test + void create_Customizer_ReturnsNull() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addMetricExporterCustomizer(MetricExporter.class, (exporter, properties) -> null); + + assertThatThrownBy( + () -> + MetricExporterFactory.getInstance() + .create( + new PushMetricExporterModel().withConsole(new ConsoleMetricExporterModel()), + context)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessageContaining("Customizer returned null for MetricExporter: console"); + } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java index 3ecdc64a711..6df217bf937 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricReaderFactoryTest.java @@ -38,6 +38,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -54,6 +55,11 @@ class MetricReaderFactoryTest { new DeclarativeConfigContext( SpiHelper.create(MetricReaderFactoryTest.class.getClassLoader()))); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @Test void create_PeriodicNullExporter() { assertThatThrownBy( diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/OpenTelemetryConfigurationFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/OpenTelemetryConfigurationFactoryTest.java index 1d8ed0d3eab..440da77f928 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/OpenTelemetryConfigurationFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/OpenTelemetryConfigurationFactoryTest.java @@ -72,6 +72,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; @@ -92,6 +93,11 @@ class OpenTelemetryConfigurationFactoryTest { new DeclarativeConfigContext( SpiHelper.create(OpenTelemetryConfigurationFactoryTest.class.getClassLoader())); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @ParameterizedTest @MethodSource("fileFormatArgs") void create_FileFormat(String fileFormat, boolean isValid) { diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactoryTest.java index 6cb853e4b69..741eec448c1 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactoryTest.java @@ -37,6 +37,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -61,6 +62,7 @@ void setup() { capturingComponentLoader = new CapturingComponentLoader(); spiHelper = SpiHelper.create(capturingComponentLoader); context = new DeclarativeConfigContext(spiHelper); + context.setBuilder(new DeclarativeConfigurationBuilder()); } @Test @@ -356,4 +358,83 @@ void create_SpiExporter_Valid() { .config.getString("key1")) .isEqualTo("value1"); } + + @Test + void create_Customizer() { + // Generic customizer applied to all span exporters + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addSpanExporterCustomizer( + SpanExporter.class, + (exporter, properties) -> + SpanExporter.composite(exporter, LoggingSpanExporter.create())); + + SpanExporter result = + SpanExporterFactory.getInstance() + .create(new SpanExporterModel().withConsole(new ConsoleExporterModel()), context); + cleanup.addCloseable(result); + + // Result should be wrapped in composite + assertThat(result.toString()).contains("LoggingSpanExporter"); + } + + @Test + void create_Customizer_TypeSafe() { + // Customizer for specific type gets type-safe access to exporter builder methods + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addSpanExporterCustomizer( + OtlpGrpcSpanExporter.class, + (exporter, properties) -> + exporter.toBuilder().setTimeout(Duration.ofSeconds(42)).build()); + + SpanExporter result = + SpanExporterFactory.getInstance() + .create(new SpanExporterModel().withOtlpGrpc(new OtlpGrpcExporterModel()), context); + cleanup.addCloseable(result); + + assertThat(result).isInstanceOf(OtlpGrpcSpanExporter.class); + assertThat(result.toString()).contains("timeoutNanos=42000000000"); + } + + @Test + void create_Customizer_TypeMismatch() { + // Customizer registered for OtlpGrpcSpanExporter should NOT be called for other types + AtomicInteger callCount = new AtomicInteger(0); + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addSpanExporterCustomizer( + OtlpGrpcSpanExporter.class, + (exporter, properties) -> { + callCount.incrementAndGet(); + return exporter; + }); + + SpanExporter result = + SpanExporterFactory.getInstance() + .create(new SpanExporterModel().withConsole(new ConsoleExporterModel()), context); + cleanup.addCloseable(result); + + // Customizer should not have been called since types don't match + assertThat(callCount.get()).isEqualTo(0); + } + + @Test + void create_Customizer_ReturnsNull() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + context + .getBuilder() + .addSpanExporterCustomizer(SpanExporter.class, (exporter, properties) -> null); + + assertThatThrownBy( + () -> + SpanExporterFactory.getInstance() + .create( + new SpanExporterModel().withConsole(new ConsoleExporterModel()), context)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessageContaining("Customizer returned null for SpanExporter: console"); + } } diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanProcessorFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanProcessorFactoryTest.java index ef41e271913..0b1c910487e 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanProcessorFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanProcessorFactoryTest.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.List; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -38,6 +39,11 @@ class SpanProcessorFactoryTest { new DeclarativeConfigContext( SpiHelper.create(SpanProcessorFactoryTest.class.getClassLoader())); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @Test void create_BatchNullExporter() { assertThatThrownBy( diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TestDeclarativeConfigurationCustomizerProvider.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TestDeclarativeConfigurationCustomizerProvider.java index 1c1c0e4a649..ac80fa388da 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TestDeclarativeConfigurationCustomizerProvider.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TestDeclarativeConfigurationCustomizerProvider.java @@ -12,6 +12,7 @@ public class TestDeclarativeConfigurationCustomizerProvider implements DeclarativeConfigurationCustomizerProvider { + @Override public void customize(DeclarativeConfigurationCustomizer customizer) { customizer.addModelCustomizer( diff --git a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TracerProviderFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TracerProviderFactoryTest.java index 53c00444c9b..a93c030aa7b 100644 --- a/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TracerProviderFactoryTest.java +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/TracerProviderFactoryTest.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.List; import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -49,6 +50,11 @@ class TracerProviderFactoryTest { new DeclarativeConfigContext( SpiHelper.create(TracerProviderFactoryTest.class.getClassLoader())); + @BeforeEach + void setup() { + context.setBuilder(new DeclarativeConfigurationBuilder()); + } + @ParameterizedTest @MethodSource("createArguments") void create(TracerProviderAndAttributeLimits model, SdkTracerProvider expectedProvider) {