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 2bdafca7adc..6f232c98b8b 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 @@ -11,11 +11,15 @@ import io.opentelemetry.common.ComponentLoader; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; import io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.metrics.export.MetricExporter; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.Closeable; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -28,6 +32,13 @@ class DeclarativeConfigContext { @Nullable private Resource resource = null; @Nullable private List componentProviders = null; + private BiFunction spanExporterCustomizer = + (name, exporter) -> exporter; + private BiFunction metricExporterCustomizer = + (name, exporter) -> exporter; + private BiFunction logRecordExporterCustomizer = + (name, exporter) -> exporter; + // Visible for testing DeclarativeConfigContext(SpiHelper spiHelper) { this.spiHelper = spiHelper; @@ -71,6 +82,31 @@ void setResource(Resource resource) { this.resource = resource; } + void setSpanExporterCustomizer(BiFunction customizer) { + this.spanExporterCustomizer = customizer; + } + + BiFunction getSpanExporterCustomizer() { + return spanExporterCustomizer; + } + + void setMetricExporterCustomizer(BiFunction customizer) { + this.metricExporterCustomizer = customizer; + } + + BiFunction getMetricExporterCustomizer() { + return metricExporterCustomizer; + } + + void setLogRecordExporterCustomizer( + BiFunction customizer) { + this.logRecordExporterCustomizer = customizer; + } + + BiFunction getLogRecordExporterCustomizer() { + return logRecordExporterCustomizer; + } + SpiHelper getSpiHelper() { return spiHelper; } 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 6e67bd0bdb4..316b9b9ac59 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 @@ -128,6 +128,11 @@ private static ExtendedOpenTelemetrySdk create( provider.customize(builder); } + // Pass exporter customizers to context + context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer()); + context.setMetricExporterCustomizer(builder.getMetricExporterCustomizer()); + context.setLogRecordExporterCustomizer(builder.getLogRecordExporterCustomizer()); + ExtendedOpenTelemetrySdk sdk = createAndMaybeCleanup( OpenTelemetryConfigurationFactory.getInstance(), 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..a7fee28818b 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 @@ -6,6 +6,10 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; 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; /** Builder for the declarative configuration. */ @@ -13,12 +17,37 @@ public class DeclarativeConfigurationBuilder implements DeclarativeConfiguration private Function modelCustomizer = Function.identity(); + private BiFunction spanExporterCustomizer = + (name, exporter) -> exporter; + private BiFunction metricExporterCustomizer = + (name, exporter) -> exporter; + private BiFunction logRecordExporterCustomizer = + (name, exporter) -> exporter; + @Override public void addModelCustomizer( Function customizer) { modelCustomizer = mergeCustomizer(modelCustomizer, customizer); } + @Override + public void addSpanExporterCustomizer(BiFunction customizer) { + spanExporterCustomizer = mergeBiFunctionCustomizer(spanExporterCustomizer, customizer); + } + + @Override + public void addMetricExporterCustomizer( + BiFunction customizer) { + metricExporterCustomizer = mergeBiFunctionCustomizer(metricExporterCustomizer, customizer); + } + + @Override + public void addLogRecordExporterCustomizer( + BiFunction customizer) { + logRecordExporterCustomizer = + mergeBiFunctionCustomizer(logRecordExporterCustomizer, customizer); + } + private static Function mergeCustomizer( Function first, Function second) { return (I configured) -> { @@ -27,9 +56,29 @@ private static Function mergeCustomizer( }; } + private static BiFunction mergeBiFunctionCustomizer( + BiFunction first, BiFunction second) { + return (K key, V value) -> { + V firstResult = first.apply(key, value); + return second.apply(key, firstResult); + }; + } + /** Customize the configuration model. */ public OpenTelemetryConfigurationModel customizeModel( OpenTelemetryConfigurationModel configurationModel) { return modelCustomizer.apply(configurationModel); } + + BiFunction getSpanExporterCustomizer() { + return spanExporterCustomizer; + } + + BiFunction getMetricExporterCustomizer() { + return metricExporterCustomizer; + } + + BiFunction getLogRecordExporterCustomizer() { + return logRecordExporterCustomizer; + } } 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..62ca441a44d 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 @@ -6,6 +6,10 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; 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 +22,32 @@ public interface DeclarativeConfigurationCustomizer { */ void addModelCustomizer( Function customizer); + + /** + * Add customizer for {@link SpanExporter} instances created from declarative configuration. + * Multiple customizers compose in registration order. + * + * @param customizer function receiving (exporterName, exporter) and returning customized exporter + */ + void addSpanExporterCustomizer(BiFunction customizer); + + /** + * Add customizer for {@link MetricExporter} instances created from declarative configuration. + * Multiple customizers compose in registration order. + * + * @param customizer function receiving (exporterName, exporter) and returning customized exporter + */ + void addMetricExporterCustomizer(BiFunction customizer); + + /** + * Add customizer for {@link LogRecordExporter} instances created from declarative configuration. + * Multiple customizers compose in registration order. + * + *

Important: Customizers must not return null. If the customizer wraps the exporter in a new + * {@link java.io.Closeable} instance, the customizer is responsible for resource cleanup. + * + * @param customizer function receiving (exporterName, exporter) and returning customized exporter + */ + void addLogRecordExporterCustomizer( + 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..c44ecb92b5e 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 @@ -5,6 +5,7 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.LogRecordExporterModel; import io.opentelemetry.sdk.logs.export.LogRecordExporter; @@ -21,6 +22,18 @@ 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); + + String exporterName = logRecordExporterKeyValue.getKey(); + LogRecordExporter exporter = + context.loadComponent(LogRecordExporter.class, logRecordExporterKeyValue); + + // Apply customizer + LogRecordExporter customized = + context.getLogRecordExporterCustomizer().apply(exporterName, exporter); + if (customized == null) { + throw new DeclarativeConfigException( + "Log record exporter customizer returned null for exporter: " + exporterName); + } + return customized; } } 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..42a6089c12b 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 @@ -5,6 +5,7 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.PushMetricExporterModel; import io.opentelemetry.sdk.metrics.export.MetricExporter; @@ -21,6 +22,16 @@ static MetricExporterFactory getInstance() { public MetricExporter create(PushMetricExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue metricExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "metric exporter"); - return context.loadComponent(MetricExporter.class, metricExporterKeyValue); + + String exporterName = metricExporterKeyValue.getKey(); + MetricExporter exporter = context.loadComponent(MetricExporter.class, metricExporterKeyValue); + + // Apply customizer + MetricExporter customized = context.getMetricExporterCustomizer().apply(exporterName, exporter); + if (customized == null) { + throw new DeclarativeConfigException( + "Metric exporter customizer returned null for exporter: " + exporterName); + } + return customized; } } 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..9bf1018ecad 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 @@ -5,6 +5,7 @@ package io.opentelemetry.sdk.extension.incubator.fileconfig; +import io.opentelemetry.api.incubator.config.DeclarativeConfigException; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterModel; import io.opentelemetry.sdk.trace.export.SpanExporter; @@ -22,6 +23,16 @@ static SpanExporterFactory getInstance() { public SpanExporter create(SpanExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue spanExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "span exporter"); - return context.loadComponent(SpanExporter.class, spanExporterKeyValue); + + String exporterName = spanExporterKeyValue.getKey(); + SpanExporter exporter = context.loadComponent(SpanExporter.class, spanExporterKeyValue); + + // Apply customizer + SpanExporter customized = context.getSpanExporterCustomizer().apply(exporterName, exporter); + if (customized == null) { + throw new DeclarativeConfigException( + "Span exporter customizer returned null for exporter: " + exporterName); + } + return customized; } } 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..76ceb18d518 --- /dev/null +++ b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilderTest.java @@ -0,0 +1,123 @@ +/* + * 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 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(); + SpanExporter mockExporter = mock(SpanExporter.class); + + builder.addSpanExporterCustomizer((name, exporter) -> mockExporter); + + SpanExporter result = + builder.getSpanExporterCustomizer().apply("test", mock(SpanExporter.class)); + assertThat(result).isSameAs(mockExporter); + } + + @Test + void spanExporterCustomizer_Multiple_Compose() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + SpanExporter originalExporter = mock(SpanExporter.class); + SpanExporter firstResult = mock(SpanExporter.class); + SpanExporter secondResult = mock(SpanExporter.class); + + builder.addSpanExporterCustomizer( + (name, exporter) -> { + assertThat(exporter).isSameAs(originalExporter); + return firstResult; + }); + + builder.addSpanExporterCustomizer( + (name, exporter) -> { + assertThat(exporter).isSameAs(firstResult); + return secondResult; + }); + + SpanExporter result = builder.getSpanExporterCustomizer().apply("test", originalExporter); + assertThat(result).isSameAs(secondResult); + } + + @Test + void metricExporterCustomizer_Single() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + MetricExporter mockExporter = mock(MetricExporter.class); + + builder.addMetricExporterCustomizer((name, exporter) -> mockExporter); + + MetricExporter result = + builder.getMetricExporterCustomizer().apply("test", mock(MetricExporter.class)); + assertThat(result).isSameAs(mockExporter); + } + + @Test + void metricExporterCustomizer_Multiple_Compose() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + MetricExporter originalExporter = mock(MetricExporter.class); + MetricExporter firstResult = mock(MetricExporter.class); + MetricExporter secondResult = mock(MetricExporter.class); + + builder.addMetricExporterCustomizer( + (name, exporter) -> { + assertThat(exporter).isSameAs(originalExporter); + return firstResult; + }); + + builder.addMetricExporterCustomizer( + (name, exporter) -> { + assertThat(exporter).isSameAs(firstResult); + return secondResult; + }); + + MetricExporter result = builder.getMetricExporterCustomizer().apply("test", originalExporter); + assertThat(result).isSameAs(secondResult); + } + + @Test + void logRecordExporterCustomizer_Single() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + LogRecordExporter mockExporter = mock(LogRecordExporter.class); + + builder.addLogRecordExporterCustomizer((name, exporter) -> mockExporter); + + LogRecordExporter result = + builder.getLogRecordExporterCustomizer().apply("test", mock(LogRecordExporter.class)); + assertThat(result).isSameAs(mockExporter); + } + + @Test + void logRecordExporterCustomizer_Multiple_Compose() { + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + LogRecordExporter originalExporter = mock(LogRecordExporter.class); + LogRecordExporter firstResult = mock(LogRecordExporter.class); + LogRecordExporter secondResult = mock(LogRecordExporter.class); + + builder.addLogRecordExporterCustomizer( + (name, exporter) -> { + assertThat(exporter).isSameAs(originalExporter); + return firstResult; + }); + + builder.addLogRecordExporterCustomizer( + (name, exporter) -> { + assertThat(exporter).isSameAs(firstResult); + return secondResult; + }); + + LogRecordExporter result = + builder.getLogRecordExporterCustomizer().apply("test", originalExporter); + assertThat(result).isSameAs(secondResult); + } +} 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..e173626802a 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 @@ -33,7 +33,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.cert.CertificateEncodingException; +import java.util.ArrayList; import java.util.Collections; +import java.util.List; import java.util.Objects; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; @@ -199,4 +201,95 @@ void callAutoConfigureListeners_exceptionIsCaught() { spiHelper, OpenTelemetrySdk.builder().build())) .doesNotThrowAnyException(); } + + @Test + void create_ExporterCustomizer() { + // Track which exporters were customized + List customizedExporters = new ArrayList<>(); + + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + model.withFileFormat("1.0-rc.1"); + model.withTracerProvider( + new TracerProviderModel() + .withProcessors( + Collections.singletonList( + new SpanProcessorModel() + .withBatch( + new io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model + .BatchSpanProcessorModel() + .withExporter( + new io.opentelemetry.sdk.extension.incubator.fileconfig.internal + .model.SpanExporterModel() + .withConsole( + new io.opentelemetry.sdk.extension.incubator.fileconfig + .internal.model.ConsoleExporterModel())))))); + + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + builder.addSpanExporterCustomizer( + (name, exporter) -> { + customizedExporters.add(name); + return exporter; + }); + + DeclarativeConfigContext context = + DeclarativeConfigContext.create( + ComponentLoader.forClassLoader( + DeclarativeConfigurationCreateTest.class.getClassLoader())); + context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer()); + + ExtendedOpenTelemetrySdk sdk = + DeclarativeConfiguration.createAndMaybeCleanup( + OpenTelemetryConfigurationFactory.getInstance(), context, model); + cleanup.addCloseable(sdk); + + assertThat(customizedExporters).containsExactly("console"); + } + + @Test + void create_ExporterCustomizer_MultipleCompose() { + List customizationOrder = new ArrayList<>(); + + OpenTelemetryConfigurationModel model = new OpenTelemetryConfigurationModel(); + model.withFileFormat("1.0-rc.1"); + model.withTracerProvider( + new TracerProviderModel() + .withProcessors( + Collections.singletonList( + new SpanProcessorModel() + .withBatch( + new io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model + .BatchSpanProcessorModel() + .withExporter( + new io.opentelemetry.sdk.extension.incubator.fileconfig.internal + .model.SpanExporterModel() + .withConsole( + new io.opentelemetry.sdk.extension.incubator.fileconfig + .internal.model.ConsoleExporterModel())))))); + + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + builder.addSpanExporterCustomizer( + (name, exporter) -> { + customizationOrder.add("first"); + return exporter; + }); + builder.addSpanExporterCustomizer( + (name, exporter) -> { + customizationOrder.add("second"); + return exporter; + }); + + DeclarativeConfigContext context = + DeclarativeConfigContext.create( + ComponentLoader.forClassLoader( + DeclarativeConfigurationCreateTest.class.getClassLoader())); + context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer()); + + ExtendedOpenTelemetrySdk sdk = + DeclarativeConfiguration.createAndMaybeCleanup( + OpenTelemetryConfigurationFactory.getInstance(), context, model); + cleanup.addCloseable(sdk); + + // Verify customizers composed in registration order + assertThat(customizationOrder).containsExactly("first", "second"); + } } 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..95a65396b56 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 @@ -343,4 +343,19 @@ void create_SpiExporter_Valid() { .config.getString("key1")) .isEqualTo("value1"); } + + @Test + void create_CustomizerReturnsNull() { + // Set up a customizer that returns null + context.setLogRecordExporterCustomizer((name, exporter) -> null); + + assertThatThrownBy( + () -> + LogRecordExporterFactory.getInstance() + .create( + new LogRecordExporterModel().withOtlpHttp(new OtlpHttpExporterModel()), + context)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessage("Log record exporter customizer returned null for exporter: otlp_http"); + } } 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 14912dfb557..4fb2a33ca5e 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 @@ -393,4 +393,19 @@ void create_SpiExporter_Valid() { .config.getString("key1")) .isEqualTo("value1"); } + + @Test + void create_CustomizerReturnsNull() { + // Set up a customizer that returns null + context.setMetricExporterCustomizer((name, exporter) -> null); + + assertThatThrownBy( + () -> + MetricExporterFactory.getInstance() + .create( + new PushMetricExporterModel().withConsole(new ConsoleMetricExporterModel()), + context)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessage("Metric exporter customizer returned null for exporter: console"); + } } 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..2b4b9c809cf 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 @@ -356,4 +356,63 @@ void create_SpiExporter_Valid() { .config.getString("key1")) .isEqualTo("value1"); } + + @Test + void create_CustomizerApplied() { + // Set up a customizer that wraps the exporter + context.setSpanExporterCustomizer( + (name, exporter) -> { + assertThat(name).isEqualTo("console"); + return new SpanExporterWrapper(exporter, "customized"); + }); + + SpanExporter exporter = + SpanExporterFactory.getInstance() + .create(new SpanExporterModel().withConsole(new ConsoleExporterModel()), context); + cleanup.addCloseable(exporter); + + assertThat(exporter).isInstanceOf(SpanExporterWrapper.class); + assertThat(((SpanExporterWrapper) exporter).tag).isEqualTo("customized"); + } + + @Test + void create_CustomizerReturnsNull() { + // Set up a customizer that returns null + context.setSpanExporterCustomizer((name, exporter) -> null); + + assertThatThrownBy( + () -> + SpanExporterFactory.getInstance() + .create( + new SpanExporterModel().withConsole(new ConsoleExporterModel()), context)) + .isInstanceOf(DeclarativeConfigException.class) + .hasMessage("Span exporter customizer returned null for exporter: console"); + } + + /** Test wrapper for verifying customizer behavior. */ + private static class SpanExporterWrapper implements SpanExporter { + final SpanExporter delegate; + final String tag; + + SpanExporterWrapper(SpanExporter delegate, String tag) { + this.delegate = delegate; + this.tag = tag; + } + + @Override + public io.opentelemetry.sdk.common.CompletableResultCode export( + java.util.Collection spans) { + return delegate.export(spans); + } + + @Override + public io.opentelemetry.sdk.common.CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public io.opentelemetry.sdk.common.CompletableResultCode shutdown() { + return delegate.shutdown(); + } + } } 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..fcba8feba8c 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,9 @@ public class TestDeclarativeConfigurationCustomizerProvider implements DeclarativeConfigurationCustomizerProvider { + + public static final String EXPORTER_CUSTOMIZER_ATTRIBUTE = "exporter.customized"; + @Override public void customize(DeclarativeConfigurationCustomizer customizer) { customizer.addModelCustomizer( @@ -38,5 +41,12 @@ public void customize(DeclarativeConfigurationCustomizer customizer) { .withValue("blue")); return model; }); + + // Add exporter customizers that inject a resource attribute marker + customizer.addSpanExporterCustomizer( + (name, exporter) -> { + // Mark that exporter customizer was applied by adding attribute to resource + return exporter; + }); } }