From c5dab106c72af934a3ad32295741573fbbcc9b8b Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 13 Feb 2026 14:34:09 +0000 Subject: [PATCH 1/8] Add exporter customizers for declarative config (#6576) - Add SpanExporter/MetricExporter/LogRecordExporter customizers to DeclarativeConfigurationCustomizer - Customizers compose in SPI registration order - Apply in factories after component creation, before return - Null returns throw DeclarativeConfigException - Tests: factory unit tests, builder composition tests, integration tests --- .../fileconfig/DeclarativeConfigContext.java | 36 +++++ .../fileconfig/DeclarativeConfiguration.java | 5 + .../DeclarativeConfigurationBuilder.java | 49 +++++++ .../DeclarativeConfigurationCustomizer.java | 32 +++++ .../fileconfig/LogRecordExporterFactory.java | 15 ++- .../fileconfig/MetricExporterFactory.java | 13 +- .../fileconfig/SpanExporterFactory.java | 13 +- .../DeclarativeConfigurationBuilderTest.java | 123 ++++++++++++++++++ .../DeclarativeConfigurationCreateTest.java | 93 +++++++++++++ .../fileconfig/SpanExporterFactoryTest.java | 59 +++++++++ ...rativeConfigurationCustomizerProvider.java | 10 ++ 11 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/DeclarativeConfigurationBuilderTest.java 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/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; + }); } } From 52c4f98ec9d37c46b58946a1e21db04310d30035 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 13 Feb 2026 15:19:47 +0000 Subject: [PATCH 2/8] Add null handling tests for metric and log exporter customizers --- .../fileconfig/LogRecordExporterFactoryTest.java | 15 +++++++++++++++ .../fileconfig/MetricExporterFactoryTest.java | 15 +++++++++++++++ 2 files changed, 30 insertions(+) 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"); + } } From c95e072eb390ad5a0197e4a4cb625907b6f93a16 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Tue, 17 Feb 2026 14:46:05 +0000 Subject: [PATCH 3/8] Pass builder to context instead of separate setters Reduces noise of 3 separate setter calls for exporter customizers. Context stores builder ref and delegates customizer access. Note: Built using Claude Sonnet 4.5 --- .../fileconfig/DeclarativeConfigContext.java | 29 +++++-------------- .../fileconfig/DeclarativeConfiguration.java | 5 +--- .../DeclarativeConfigurationCreateTest.java | 4 +-- .../LogRecordExporterFactoryTest.java | 4 ++- .../fileconfig/MetricExporterFactoryTest.java | 4 ++- .../fileconfig/SpanExporterFactoryTest.java | 8 +++-- 6 files changed, 23 insertions(+), 31 deletions(-) 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 6f232c98b8b..46fa703cae8 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 @@ -31,13 +31,7 @@ class DeclarativeConfigContext { @Nullable private volatile MeterProvider meterProvider; @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; + @Nullable private DeclarativeConfigurationBuilder builder = null; // Visible for testing DeclarativeConfigContext(SpiHelper spiHelper) { @@ -82,29 +76,22 @@ void setResource(Resource resource) { this.resource = resource; } - void setSpanExporterCustomizer(BiFunction customizer) { - this.spanExporterCustomizer = customizer; + void setBuilder(DeclarativeConfigurationBuilder builder) { + this.builder = builder; } BiFunction getSpanExporterCustomizer() { - return spanExporterCustomizer; - } - - void setMetricExporterCustomizer(BiFunction customizer) { - this.metricExporterCustomizer = customizer; + return builder != null ? builder.getSpanExporterCustomizer() : (name, exporter) -> exporter; } BiFunction getMetricExporterCustomizer() { - return metricExporterCustomizer; - } - - void setLogRecordExporterCustomizer( - BiFunction customizer) { - this.logRecordExporterCustomizer = customizer; + return builder != null ? builder.getMetricExporterCustomizer() : (name, exporter) -> exporter; } BiFunction getLogRecordExporterCustomizer() { - return logRecordExporterCustomizer; + return builder != null + ? builder.getLogRecordExporterCustomizer() + : (name, exporter) -> exporter; } SpiHelper getSpiHelper() { 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 316b9b9ac59..5929c69d7e9 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,10 +128,7 @@ private static ExtendedOpenTelemetrySdk create( provider.customize(builder); } - // Pass exporter customizers to context - context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer()); - context.setMetricExporterCustomizer(builder.getMetricExporterCustomizer()); - context.setLogRecordExporterCustomizer(builder.getLogRecordExporterCustomizer()); + context.setBuilder(builder); ExtendedOpenTelemetrySdk sdk = createAndMaybeCleanup( 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 e173626802a..8e79750036c 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 @@ -235,7 +235,7 @@ void create_ExporterCustomizer() { DeclarativeConfigContext.create( ComponentLoader.forClassLoader( DeclarativeConfigurationCreateTest.class.getClassLoader())); - context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer()); + context.setBuilder(builder); ExtendedOpenTelemetrySdk sdk = DeclarativeConfiguration.createAndMaybeCleanup( @@ -282,7 +282,7 @@ void create_ExporterCustomizer_MultipleCompose() { DeclarativeConfigContext.create( ComponentLoader.forClassLoader( DeclarativeConfigurationCreateTest.class.getClassLoader())); - context.setSpanExporterCustomizer(builder.getSpanExporterCustomizer()); + context.setBuilder(builder); ExtendedOpenTelemetrySdk sdk = DeclarativeConfiguration.createAndMaybeCleanup( 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 95a65396b56..d8d34094777 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 @@ -347,7 +347,9 @@ void create_SpiExporter_Valid() { @Test void create_CustomizerReturnsNull() { // Set up a customizer that returns null - context.setLogRecordExporterCustomizer((name, exporter) -> null); + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + builder.addLogRecordExporterCustomizer((name, exporter) -> null); + context.setBuilder(builder); assertThatThrownBy( () -> 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 4fb2a33ca5e..b55efcabdf4 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 @@ -397,7 +397,9 @@ void create_SpiExporter_Valid() { @Test void create_CustomizerReturnsNull() { // Set up a customizer that returns null - context.setMetricExporterCustomizer((name, exporter) -> null); + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + builder.addMetricExporterCustomizer((name, exporter) -> null); + context.setBuilder(builder); assertThatThrownBy( () -> 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 2b4b9c809cf..eef2308ce56 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 @@ -360,11 +360,13 @@ void create_SpiExporter_Valid() { @Test void create_CustomizerApplied() { // Set up a customizer that wraps the exporter - context.setSpanExporterCustomizer( + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + builder.addSpanExporterCustomizer( (name, exporter) -> { assertThat(name).isEqualTo("console"); return new SpanExporterWrapper(exporter, "customized"); }); + context.setBuilder(builder); SpanExporter exporter = SpanExporterFactory.getInstance() @@ -378,7 +380,9 @@ void create_CustomizerApplied() { @Test void create_CustomizerReturnsNull() { // Set up a customizer that returns null - context.setSpanExporterCustomizer((name, exporter) -> null); + DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); + builder.addSpanExporterCustomizer((name, exporter) -> null); + context.setBuilder(builder); assertThatThrownBy( () -> From eab2d561989d8faeb40d375fcb37272820f58725 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Wed, 18 Feb 2026 10:15:40 +0000 Subject: [PATCH 4/8] feat(config): Improve exporter customizer contract for declarative config - Change customizer signature to use Class for type-safe filtering - Add DeclarativeConfigProperties parameter for access to config - Consolidate type cast in ExporterCustomizer constructor - Handle null returns gracefully with logging instead of exception --- .../fileconfig/DeclarativeConfigContext.java | 19 +---- .../DeclarativeConfigurationBuilder.java | 72 ++++++++++------- .../DeclarativeConfigurationCustomizer.java | 27 ++++--- .../fileconfig/LogRecordExporterFactory.java | 29 +++++-- .../fileconfig/MetricExporterFactory.java | 28 +++++-- .../fileconfig/SpanExporterFactory.java | 28 +++++-- .../DeclarativeConfigurationBuilderTest.java | 78 +++++-------------- .../DeclarativeConfigurationCreateTest.java | 35 ++++----- .../LogRecordExporterFactoryTest.java | 17 ++-- .../fileconfig/MetricExporterFactoryTest.java | 18 ++--- .../fileconfig/SpanExporterFactoryTest.java | 31 ++++---- ...rativeConfigurationCustomizerProvider.java | 4 +- 12 files changed, 198 insertions(+), 188 deletions(-) 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 46fa703cae8..4fa19ac9fc2 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,15 +11,11 @@ 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; @@ -80,18 +76,9 @@ void setBuilder(DeclarativeConfigurationBuilder builder) { this.builder = builder; } - BiFunction getSpanExporterCustomizer() { - return builder != null ? builder.getSpanExporterCustomizer() : (name, exporter) -> exporter; - } - - BiFunction getMetricExporterCustomizer() { - return builder != null ? builder.getMetricExporterCustomizer() : (name, exporter) -> exporter; - } - - BiFunction getLogRecordExporterCustomizer() { - return builder != null - ? builder.getLogRecordExporterCustomizer() - : (name, exporter) -> exporter; + @Nullable + DeclarativeConfigurationBuilder getBuilder() { + return builder; } SpiHelper getSpiHelper() { 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 a7fee28818b..0b073876331 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,10 +5,13 @@ 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.ArrayList; +import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; @@ -17,12 +20,31 @@ 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; + private final List> spanExporterCustomizers = new ArrayList<>(); + private final List> metricExporterCustomizers = + new ArrayList<>(); + private final List> logRecordExporterCustomizers = + new ArrayList<>(); + + static class ExporterCustomizer { + private final Class exporterType; + private final BiFunction customizer; + + @SuppressWarnings("unchecked") + ExporterCustomizer( + Class exporterType, BiFunction customizer) { + this.exporterType = exporterType; + this.customizer = (BiFunction) customizer; + } + + Class getExporterType() { + return exporterType; + } + + BiFunction getCustomizer() { + return customizer; + } + } @Override public void addModelCustomizer( @@ -31,21 +53,21 @@ public void addModelCustomizer( } @Override - public void addSpanExporterCustomizer(BiFunction customizer) { - spanExporterCustomizer = mergeBiFunctionCustomizer(spanExporterCustomizer, customizer); + public void addSpanExporterCustomizer( + Class exporterType, BiFunction customizer) { + spanExporterCustomizers.add(new ExporterCustomizer<>(exporterType, customizer)); } @Override - public void addMetricExporterCustomizer( - BiFunction customizer) { - metricExporterCustomizer = mergeBiFunctionCustomizer(metricExporterCustomizer, customizer); + public void addMetricExporterCustomizer( + Class exporterType, BiFunction customizer) { + metricExporterCustomizers.add(new ExporterCustomizer<>(exporterType, customizer)); } @Override - public void addLogRecordExporterCustomizer( - BiFunction customizer) { - logRecordExporterCustomizer = - mergeBiFunctionCustomizer(logRecordExporterCustomizer, customizer); + public void addLogRecordExporterCustomizer( + Class exporterType, BiFunction customizer) { + logRecordExporterCustomizers.add(new ExporterCustomizer<>(exporterType, customizer)); } private static Function mergeCustomizer( @@ -56,29 +78,21 @@ 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; + List> getSpanExporterCustomizers() { + return spanExporterCustomizers; } - BiFunction getMetricExporterCustomizer() { - return metricExporterCustomizer; + List> getMetricExporterCustomizers() { + return metricExporterCustomizers; } - BiFunction getLogRecordExporterCustomizer() { - return logRecordExporterCustomizer; + List> getLogRecordExporterCustomizers() { + return logRecordExporterCustomizers; } } 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 62ca441a44d..922edb4d641 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,6 +5,7 @@ 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; @@ -27,27 +28,35 @@ void addModelCustomizer( * 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 + * @param exporterType the exporter type to customize + * @param customizer function receiving (exporter, properties) and returning customized exporter + * @param the exporter type */ - void addSpanExporterCustomizer(BiFunction customizer); + void addSpanExporterCustomizer( + Class exporterType, 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 + * @param exporterType the exporter type to customize + * @param customizer function receiving (exporter, properties) and returning customized exporter + * @param the exporter type */ - void addMetricExporterCustomizer(BiFunction customizer); + void addMetricExporterCustomizer( + Class exporterType, 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. + *

Important: 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 + * @param exporterType the exporter type to customize + * @param customizer function receiving (exporter, properties) and returning customized exporter + * @param the exporter type */ - void addLogRecordExporterCustomizer( - BiFunction customizer); + 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 c44ecb92b5e..ed66b49939f 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,11 +5,12 @@ 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; +import java.util.logging.Logger; final class LogRecordExporterFactory implements Factory { + private static final Logger logger = Logger.getLogger(LogRecordExporterFactory.class.getName()); private static final LogRecordExporterFactory INSTANCE = new LogRecordExporterFactory(); private LogRecordExporterFactory() {} @@ -27,13 +28,25 @@ public LogRecordExporter create(LogRecordExporterModel model, DeclarativeConfigC 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); + // Apply customizers + DeclarativeConfigurationBuilder builder = context.getBuilder(); + if (builder != null) { + for (DeclarativeConfigurationBuilder.ExporterCustomizer customizerEntry : + builder.getLogRecordExporterCustomizers()) { + if (customizerEntry.getExporterType().isInstance(exporter)) { + LogRecordExporter customized = + customizerEntry.getCustomizer().apply(exporter, logRecordExporterKeyValue.getValue()); + if (customized == null) { + logger.warning( + "Log record exporter customizer returned null for exporter: " + + exporterName + + ", using original exporter"); + } else { + exporter = customized; + } + } + } } - return customized; + 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 42a6089c12b..c6776e07b88 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,11 +5,12 @@ 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; +import java.util.logging.Logger; final class MetricExporterFactory implements Factory { + private static final Logger logger = Logger.getLogger(MetricExporterFactory.class.getName()); private static final MetricExporterFactory INSTANCE = new MetricExporterFactory(); private MetricExporterFactory() {} @@ -26,12 +27,25 @@ public MetricExporter create(PushMetricExporterModel model, DeclarativeConfigCon 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); + // Apply customizers + DeclarativeConfigurationBuilder builder = context.getBuilder(); + if (builder != null) { + for (DeclarativeConfigurationBuilder.ExporterCustomizer customizerEntry : + builder.getMetricExporterCustomizers()) { + if (customizerEntry.getExporterType().isInstance(exporter)) { + MetricExporter customized = + customizerEntry.getCustomizer().apply(exporter, metricExporterKeyValue.getValue()); + if (customized == null) { + logger.warning( + "Metric exporter customizer returned null for exporter: " + + exporterName + + ", using original exporter"); + } else { + exporter = customized; + } + } + } } - return customized; + 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 9bf1018ecad..84e4c73e87f 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,12 +5,13 @@ 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; +import java.util.logging.Logger; final class SpanExporterFactory implements Factory { + private static final Logger logger = Logger.getLogger(SpanExporterFactory.class.getName()); private static final SpanExporterFactory INSTANCE = new SpanExporterFactory(); private SpanExporterFactory() {} @@ -27,12 +28,25 @@ public SpanExporter create(SpanExporterModel model, DeclarativeConfigContext con 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); + // Apply customizers + DeclarativeConfigurationBuilder builder = context.getBuilder(); + if (builder != null) { + for (DeclarativeConfigurationBuilder.ExporterCustomizer customizerEntry : + builder.getSpanExporterCustomizers()) { + if (customizerEntry.getExporterType().isInstance(exporter)) { + SpanExporter customized = + customizerEntry.getCustomizer().apply(exporter, spanExporterKeyValue.getValue()); + if (customized == null) { + logger.warning( + "Span exporter customizer returned null for exporter: " + + exporterName + + ", using original exporter"); + } else { + exporter = customized; + } + } + } } - return customized; + 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 index 76ceb18d518..cb60cee6bf2 100644 --- 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 @@ -18,106 +18,66 @@ class DeclarativeConfigurationBuilderTest { @Test void spanExporterCustomizer_Single() { DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - SpanExporter mockExporter = mock(SpanExporter.class); - builder.addSpanExporterCustomizer((name, exporter) -> mockExporter); + builder.addSpanExporterCustomizer( + SpanExporter.class, (exporter, properties) -> mock(SpanExporter.class)); - SpanExporter result = - builder.getSpanExporterCustomizer().apply("test", mock(SpanExporter.class)); - assertThat(result).isSameAs(mockExporter); + assertThat(builder.getSpanExporterCustomizers()).hasSize(1); } @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; - }); - + SpanExporter.class, (exporter, properties) -> mock(SpanExporter.class)); builder.addSpanExporterCustomizer( - (name, exporter) -> { - assertThat(exporter).isSameAs(firstResult); - return secondResult; - }); + SpanExporter.class, (exporter, properties) -> mock(SpanExporter.class)); - SpanExporter result = builder.getSpanExporterCustomizer().apply("test", originalExporter); - assertThat(result).isSameAs(secondResult); + assertThat(builder.getSpanExporterCustomizers()).hasSize(2); } @Test void metricExporterCustomizer_Single() { DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - MetricExporter mockExporter = mock(MetricExporter.class); - builder.addMetricExporterCustomizer((name, exporter) -> mockExporter); + builder.addMetricExporterCustomizer( + MetricExporter.class, (exporter, properties) -> mock(MetricExporter.class)); - MetricExporter result = - builder.getMetricExporterCustomizer().apply("test", mock(MetricExporter.class)); - assertThat(result).isSameAs(mockExporter); + assertThat(builder.getMetricExporterCustomizers()).hasSize(1); } @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; - }); - + MetricExporter.class, (exporter, properties) -> mock(MetricExporter.class)); builder.addMetricExporterCustomizer( - (name, exporter) -> { - assertThat(exporter).isSameAs(firstResult); - return secondResult; - }); + MetricExporter.class, (exporter, properties) -> mock(MetricExporter.class)); - MetricExporter result = builder.getMetricExporterCustomizer().apply("test", originalExporter); - assertThat(result).isSameAs(secondResult); + assertThat(builder.getMetricExporterCustomizers()).hasSize(2); } @Test void logRecordExporterCustomizer_Single() { DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - LogRecordExporter mockExporter = mock(LogRecordExporter.class); - builder.addLogRecordExporterCustomizer((name, exporter) -> mockExporter); + builder.addLogRecordExporterCustomizer( + LogRecordExporter.class, (exporter, properties) -> mock(LogRecordExporter.class)); - LogRecordExporter result = - builder.getLogRecordExporterCustomizer().apply("test", mock(LogRecordExporter.class)); - assertThat(result).isSameAs(mockExporter); + assertThat(builder.getLogRecordExporterCustomizers()).hasSize(1); } @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; - }); - + LogRecordExporter.class, (exporter, properties) -> mock(LogRecordExporter.class)); builder.addLogRecordExporterCustomizer( - (name, exporter) -> { - assertThat(exporter).isSameAs(firstResult); - return secondResult; - }); - - LogRecordExporter result = - builder.getLogRecordExporterCustomizer().apply("test", originalExporter); - assertThat(result).isSameAs(secondResult); + LogRecordExporter.class, (exporter, properties) -> mock(LogRecordExporter.class)); + + assertThat(builder.getLogRecordExporterCustomizers()).hasSize(2); } } 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 8e79750036c..4853af20542 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 @@ -20,10 +20,14 @@ import io.opentelemetry.internal.testing.slf4j.SuppressLogger; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.BatchSpanProcessorModel; +import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ConsoleExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; +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 io.opentelemetry.sdk.internal.ExtendedOpenTelemetrySdk; +import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.ParentBasedSamplerBuilder; import java.io.ByteArrayInputStream; import java.io.File; @@ -215,19 +219,16 @@ void create_ExporterCustomizer() { Collections.singletonList( new SpanProcessorModel() .withBatch( - new io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model - .BatchSpanProcessorModel() + new BatchSpanProcessorModel() .withExporter( - new io.opentelemetry.sdk.extension.incubator.fileconfig.internal - .model.SpanExporterModel() - .withConsole( - new io.opentelemetry.sdk.extension.incubator.fileconfig - .internal.model.ConsoleExporterModel())))))); + new SpanExporterModel() + .withConsole(new ConsoleExporterModel())))))); DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); builder.addSpanExporterCustomizer( - (name, exporter) -> { - customizedExporters.add(name); + SpanExporter.class, + (exporter, properties) -> { + customizedExporters.add("console"); return exporter; }); @@ -257,23 +258,21 @@ void create_ExporterCustomizer_MultipleCompose() { Collections.singletonList( new SpanProcessorModel() .withBatch( - new io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model - .BatchSpanProcessorModel() + new BatchSpanProcessorModel() .withExporter( - new io.opentelemetry.sdk.extension.incubator.fileconfig.internal - .model.SpanExporterModel() - .withConsole( - new io.opentelemetry.sdk.extension.incubator.fileconfig - .internal.model.ConsoleExporterModel())))))); + new SpanExporterModel() + .withConsole(new ConsoleExporterModel())))))); DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); builder.addSpanExporterCustomizer( - (name, exporter) -> { + SpanExporter.class, + (exporter, properties) -> { customizationOrder.add("first"); return exporter; }); builder.addSpanExporterCustomizer( - (name, exporter) -> { + SpanExporter.class, + (exporter, properties) -> { customizationOrder.add("second"); return exporter; }); 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 d8d34094777..e168284e389 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 @@ -348,16 +348,15 @@ void create_SpiExporter_Valid() { void create_CustomizerReturnsNull() { // Set up a customizer that returns null DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addLogRecordExporterCustomizer((name, exporter) -> null); + builder.addLogRecordExporterCustomizer(LogRecordExporter.class, (exporter, properties) -> null); context.setBuilder(builder); - assertThatThrownBy( - () -> - LogRecordExporterFactory.getInstance() - .create( - new LogRecordExporterModel().withOtlpHttp(new OtlpHttpExporterModel()), - context)) - .isInstanceOf(DeclarativeConfigException.class) - .hasMessage("Log record exporter customizer returned null for exporter: otlp_http"); + LogRecordExporter result = + LogRecordExporterFactory.getInstance() + .create( + new LogRecordExporterModel().withOtlpHttp(new OtlpHttpExporterModel()), context); + + // Should return original exporter when customizer returns null + assertThat(result).isInstanceOf(OtlpHttpLogRecordExporter.class); } } 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 0bfdd9aafd4..4aa583527fe 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 @@ -398,16 +398,16 @@ void create_SpiExporter_Valid() { void create_CustomizerReturnsNull() { // Set up a customizer that returns null DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addMetricExporterCustomizer((name, exporter) -> null); + builder.addMetricExporterCustomizer(MetricExporter.class, (exporter, properties) -> null); context.setBuilder(builder); - assertThatThrownBy( - () -> - MetricExporterFactory.getInstance() - .create( - new PushMetricExporterModel().withConsole(new ConsoleMetricExporterModel()), - context)) - .isInstanceOf(DeclarativeConfigException.class) - .hasMessage("Metric exporter customizer returned null for exporter: console"); + MetricExporter result = + MetricExporterFactory.getInstance() + .create( + new PushMetricExporterModel().withConsole(new ConsoleMetricExporterModel()), + context); + + // Should return original exporter when customizer returns null + assertThat(result).isInstanceOf(LoggingMetricExporter.class); } } 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 eef2308ce56..95f1d1153c2 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 @@ -18,6 +18,7 @@ import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.internal.testing.CleanupExtension; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; +import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.extension.incubator.fileconfig.component.SpanExporterComponentProvider; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ConsoleExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalOtlpFileExporterModel; @@ -28,6 +29,7 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpHttpExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterPropertyModel; +import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.Closeable; import java.io.IOException; @@ -36,6 +38,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -362,10 +365,8 @@ void create_CustomizerApplied() { // Set up a customizer that wraps the exporter DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); builder.addSpanExporterCustomizer( - (name, exporter) -> { - assertThat(name).isEqualTo("console"); - return new SpanExporterWrapper(exporter, "customized"); - }); + SpanExporter.class, + (exporter, properties) -> new SpanExporterWrapper(exporter, "customized")); context.setBuilder(builder); SpanExporter exporter = @@ -381,16 +382,15 @@ void create_CustomizerApplied() { void create_CustomizerReturnsNull() { // Set up a customizer that returns null DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addSpanExporterCustomizer((name, exporter) -> null); + builder.addSpanExporterCustomizer(SpanExporter.class, (exporter, properties) -> null); context.setBuilder(builder); - assertThatThrownBy( - () -> - SpanExporterFactory.getInstance() - .create( - new SpanExporterModel().withConsole(new ConsoleExporterModel()), context)) - .isInstanceOf(DeclarativeConfigException.class) - .hasMessage("Span exporter customizer returned null for exporter: console"); + SpanExporter result = + SpanExporterFactory.getInstance() + .create(new SpanExporterModel().withConsole(new ConsoleExporterModel()), context); + + // Should return original exporter when customizer returns null + assertThat(result).isInstanceOf(LoggingSpanExporter.class); } /** Test wrapper for verifying customizer behavior. */ @@ -404,18 +404,17 @@ private static class SpanExporterWrapper implements SpanExporter { } @Override - public io.opentelemetry.sdk.common.CompletableResultCode export( - java.util.Collection spans) { + public CompletableResultCode export(Collection spans) { return delegate.export(spans); } @Override - public io.opentelemetry.sdk.common.CompletableResultCode flush() { + public CompletableResultCode flush() { return delegate.flush(); } @Override - public io.opentelemetry.sdk.common.CompletableResultCode shutdown() { + public 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 fcba8feba8c..283edd7ae26 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 @@ -7,6 +7,7 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.AttributeNameValueModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ResourceModel; +import io.opentelemetry.sdk.trace.export.SpanExporter; import java.util.ArrayList; import java.util.List; @@ -44,7 +45,8 @@ public void customize(DeclarativeConfigurationCustomizer customizer) { // Add exporter customizers that inject a resource attribute marker customizer.addSpanExporterCustomizer( - (name, exporter) -> { + SpanExporter.class, + (exporter, properties) -> { // Mark that exporter customizer was applied by adding attribute to resource return exporter; }); From d4566392e3b0d87765dac8b9d41684df337b2a48 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 20 Feb 2026 15:06:24 +0000 Subject: [PATCH 5/8] Refactor exporter customizers: Customizer class, throw on null, setBuilder - Rename ExporterCustomizer -> Customizer (concept isn't exporter-specific) - Move null-check/type-check into Customizer.maybeCustomize(); throw DeclarativeConfigException if customizer returns null (fail-fast) - Add exporter customizer methods to DeclarativeConfigurationCustomizer interface and DeclarativeConfigurationBuilder - Add setBuilder/getBuilder to DeclarativeConfigContext; getBuilder uses requireNonNull (missing builder is a programming error) - Wire builder into DeclarativeConfiguration.create() and all three exporter factory classes Claude Sonnet 4.6 assisted with this change. --- .../fileconfig/DeclarativeConfigContext.java | 6 +- .../fileconfig/DeclarativeConfiguration.java | 3 +- .../DeclarativeConfigurationBuilder.java | 75 ++++++++++--------- .../DeclarativeConfigurationCustomizer.java | 13 ++-- .../fileconfig/LogRecordExporterFactory.java | 28 ++----- .../fileconfig/MetricExporterFactory.java | 28 ++----- .../fileconfig/SpanExporterFactory.java | 28 ++----- 7 files changed, 67 insertions(+), 114 deletions(-) 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 4fa19ac9fc2..0c3d7673ae3 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 @@ -16,6 +16,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; import javax.annotation.Nullable; @@ -27,7 +28,7 @@ class DeclarativeConfigContext { @Nullable private volatile MeterProvider meterProvider; @Nullable private Resource resource = null; @Nullable private List componentProviders = null; - @Nullable private DeclarativeConfigurationBuilder builder = null; + @Nullable private DeclarativeConfigurationBuilder builder; // Visible for testing DeclarativeConfigContext(SpiHelper spiHelper) { @@ -76,9 +77,8 @@ void setBuilder(DeclarativeConfigurationBuilder builder) { this.builder = builder; } - @Nullable DeclarativeConfigurationBuilder getBuilder() { - return builder; + return Objects.requireNonNull(builder, "builder has not been set"); } SpiHelper getSpiHelper() { 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 4809c428151..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 : @@ -167,8 +168,6 @@ private static ExtendedOpenTelemetrySdk create( provider.customize(builder); } - context.setBuilder(builder); - 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 0b073876331..527dc8c8ad0 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,12 +5,14 @@ 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.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.function.BiFunction; import java.util.function.Function; @@ -20,31 +22,10 @@ public class DeclarativeConfigurationBuilder implements DeclarativeConfiguration private Function modelCustomizer = Function.identity(); - private final List> spanExporterCustomizers = new ArrayList<>(); - private final List> metricExporterCustomizers = + private final List> spanExporterCustomizers = new ArrayList<>(); + private final List> metricExporterCustomizers = new ArrayList<>(); + private final List> logRecordExporterCustomizers = new ArrayList<>(); - private final List> logRecordExporterCustomizers = - new ArrayList<>(); - - static class ExporterCustomizer { - private final Class exporterType; - private final BiFunction customizer; - - @SuppressWarnings("unchecked") - ExporterCustomizer( - Class exporterType, BiFunction customizer) { - this.exporterType = exporterType; - this.customizer = (BiFunction) customizer; - } - - Class getExporterType() { - return exporterType; - } - - BiFunction getCustomizer() { - return customizer; - } - } @Override public void addModelCustomizer( @@ -55,19 +36,31 @@ public void addModelCustomizer( @Override public void addSpanExporterCustomizer( Class exporterType, BiFunction customizer) { - spanExporterCustomizers.add(new ExporterCustomizer<>(exporterType, customizer)); + spanExporterCustomizers.add(new Customizer<>(exporterType, customizer)); } @Override public void addMetricExporterCustomizer( Class exporterType, BiFunction customizer) { - metricExporterCustomizers.add(new ExporterCustomizer<>(exporterType, customizer)); + metricExporterCustomizers.add(new Customizer<>(exporterType, customizer)); } @Override public void addLogRecordExporterCustomizer( Class exporterType, BiFunction customizer) { - logRecordExporterCustomizers.add(new ExporterCustomizer<>(exporterType, 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( @@ -84,15 +77,27 @@ public OpenTelemetryConfigurationModel customizeModel( return modelCustomizer.apply(configurationModel); } - List> getSpanExporterCustomizers() { - return spanExporterCustomizers; - } + static class Customizer { + private final Class exporterType; + private final BiFunction customizer; - List> getMetricExporterCustomizers() { - return metricExporterCustomizers; - } + @SuppressWarnings("unchecked") + Customizer( + Class exporterType, BiFunction customizer) { + this.exporterType = exporterType; + this.customizer = (BiFunction) customizer; + } - List> getLogRecordExporterCustomizers() { - return logRecordExporterCustomizers; + 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); + } + 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 922edb4d641..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 @@ -29,7 +29,8 @@ void addModelCustomizer( * Multiple customizers compose in registration order. * * @param exporterType the exporter type to customize - * @param customizer function receiving (exporter, properties) and returning customized exporter + * @param customizer function receiving (exporter, properties) and returning customized exporter; + * must not return null * @param the exporter type */ void addSpanExporterCustomizer( @@ -40,7 +41,8 @@ void addSpanExporterCustomizer( * Multiple customizers compose in registration order. * * @param exporterType the exporter type to customize - * @param customizer function receiving (exporter, properties) and returning customized exporter + * @param customizer function receiving (exporter, properties) and returning customized exporter; + * must not return null * @param the exporter type */ void addMetricExporterCustomizer( @@ -50,11 +52,12 @@ void addMetricExporterCustomizer( * Add customizer for {@link LogRecordExporter} instances created from declarative configuration. * Multiple customizers compose in registration order. * - *

Important: If the customizer wraps the exporter in a new {@link java.io.Closeable} instance, - * the customizer is responsible for resource cleanup. + *

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 + * @param customizer function receiving (exporter, properties) and returning customized exporter; + * must not return null * @param the exporter type */ void addLogRecordExporterCustomizer( 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 ed66b49939f..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 @@ -7,10 +7,8 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.LogRecordExporterModel; import io.opentelemetry.sdk.logs.export.LogRecordExporter; -import java.util.logging.Logger; final class LogRecordExporterFactory implements Factory { - private static final Logger logger = Logger.getLogger(LogRecordExporterFactory.class.getName()); private static final LogRecordExporterFactory INSTANCE = new LogRecordExporterFactory(); private LogRecordExporterFactory() {} @@ -23,29 +21,13 @@ static LogRecordExporterFactory getInstance() { public LogRecordExporter create(LogRecordExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue logRecordExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "log record exporter"); - - String exporterName = logRecordExporterKeyValue.getKey(); LogRecordExporter exporter = context.loadComponent(LogRecordExporter.class, logRecordExporterKeyValue); - - // Apply customizers - DeclarativeConfigurationBuilder builder = context.getBuilder(); - if (builder != null) { - for (DeclarativeConfigurationBuilder.ExporterCustomizer customizerEntry : - builder.getLogRecordExporterCustomizers()) { - if (customizerEntry.getExporterType().isInstance(exporter)) { - LogRecordExporter customized = - customizerEntry.getCustomizer().apply(exporter, logRecordExporterKeyValue.getValue()); - if (customized == null) { - logger.warning( - "Log record exporter customizer returned null for exporter: " - + exporterName - + ", using original exporter"); - } else { - exporter = customized; - } - } - } + 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 c6776e07b88..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 @@ -7,10 +7,8 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.PushMetricExporterModel; import io.opentelemetry.sdk.metrics.export.MetricExporter; -import java.util.logging.Logger; final class MetricExporterFactory implements Factory { - private static final Logger logger = Logger.getLogger(MetricExporterFactory.class.getName()); private static final MetricExporterFactory INSTANCE = new MetricExporterFactory(); private MetricExporterFactory() {} @@ -23,28 +21,12 @@ static MetricExporterFactory getInstance() { public MetricExporter create(PushMetricExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue metricExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "metric exporter"); - - String exporterName = metricExporterKeyValue.getKey(); MetricExporter exporter = context.loadComponent(MetricExporter.class, metricExporterKeyValue); - - // Apply customizers - DeclarativeConfigurationBuilder builder = context.getBuilder(); - if (builder != null) { - for (DeclarativeConfigurationBuilder.ExporterCustomizer customizerEntry : - builder.getMetricExporterCustomizers()) { - if (customizerEntry.getExporterType().isInstance(exporter)) { - MetricExporter customized = - customizerEntry.getCustomizer().apply(exporter, metricExporterKeyValue.getValue()); - if (customized == null) { - logger.warning( - "Metric exporter customizer returned null for exporter: " - + exporterName - + ", using original exporter"); - } else { - exporter = customized; - } - } - } + 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 84e4c73e87f..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 @@ -7,11 +7,9 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterModel; import io.opentelemetry.sdk.trace.export.SpanExporter; -import java.util.logging.Logger; final class SpanExporterFactory implements Factory { - private static final Logger logger = Logger.getLogger(SpanExporterFactory.class.getName()); private static final SpanExporterFactory INSTANCE = new SpanExporterFactory(); private SpanExporterFactory() {} @@ -24,28 +22,12 @@ static SpanExporterFactory getInstance() { public SpanExporter create(SpanExporterModel model, DeclarativeConfigContext context) { ConfigKeyValue spanExporterKeyValue = FileConfigUtil.validateSingleKeyValue(context, model, "span exporter"); - - String exporterName = spanExporterKeyValue.getKey(); SpanExporter exporter = context.loadComponent(SpanExporter.class, spanExporterKeyValue); - - // Apply customizers - DeclarativeConfigurationBuilder builder = context.getBuilder(); - if (builder != null) { - for (DeclarativeConfigurationBuilder.ExporterCustomizer customizerEntry : - builder.getSpanExporterCustomizers()) { - if (customizerEntry.getExporterType().isInstance(exporter)) { - SpanExporter customized = - customizerEntry.getCustomizer().apply(exporter, spanExporterKeyValue.getValue()); - if (customized == null) { - logger.warning( - "Span exporter customizer returned null for exporter: " - + exporterName - + ", using original exporter"); - } else { - exporter = customized; - } - } - } + for (DeclarativeConfigurationBuilder.Customizer customizer : + context.getBuilder().getSpanExporterCustomizers()) { + exporter = + customizer.maybeCustomize( + exporter, spanExporterKeyValue.getKey(), spanExporterKeyValue.getValue()); } return exporter; } From d66ee8250e009ca99d4fbe3a0eb06a1d49ffe04f Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 20 Feb 2026 15:06:31 +0000 Subject: [PATCH 6/8] Add customizer tests to exporter factory tests Each factory test now has four new cases: - create_Customizer: generic customizer applied to all exporters - create_Customizer_TypeSafe: type-specific customizer with builder access - create_Customizer_TypeMismatch: verifies customizer not called for wrong type - create_Customizer_ReturnsNull: verifies DeclarativeConfigException thrown Claude Sonnet 4.6 assisted with this change. --- .../LogRecordExporterFactoryTest.java | 78 +++++++++++-- .../fileconfig/MetricExporterFactoryTest.java | 76 +++++++++++- .../fileconfig/SpanExporterFactoryTest.java | 109 ++++++++++-------- 3 files changed, 204 insertions(+), 59 deletions(-) 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 e168284e389..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 @@ -345,18 +349,76 @@ void create_SpiExporter_Valid() { } @Test - void create_CustomizerReturnsNull() { - // Set up a customizer that returns null - DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addLogRecordExporterCustomizer(LogRecordExporter.class, (exporter, properties) -> null); - context.setBuilder(builder); + 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().withOtlpHttp(new OtlpHttpExporterModel()), context); + 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; + }); - // Should return original exporter when customizer returns null - assertThat(result).isInstanceOf(OtlpHttpLogRecordExporter.class); + 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/MetricExporterFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/MetricExporterFactoryTest.java index 4aa583527fe..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 @@ -395,19 +397,81 @@ void create_SpiExporter_Valid() { } @Test - void create_CustomizerReturnsNull() { - // Set up a customizer that returns null - DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addMetricExporterCustomizer(MetricExporter.class, (exporter, properties) -> null); - context.setBuilder(builder); + 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); - // Should return original exporter when customizer returns null 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/SpanExporterFactoryTest.java b/sdk-extensions/incubator/src/test/java/io/opentelemetry/sdk/extension/incubator/fileconfig/SpanExporterFactoryTest.java index 95f1d1153c2..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 @@ -18,7 +18,6 @@ import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; import io.opentelemetry.internal.testing.CleanupExtension; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; -import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.extension.incubator.fileconfig.component.SpanExporterComponentProvider; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ConsoleExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ExperimentalOtlpFileExporterModel; @@ -29,7 +28,6 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OtlpHttpExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.SpanExporterPropertyModel; -import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.export.SpanExporter; import java.io.Closeable; import java.io.IOException; @@ -38,8 +36,8 @@ import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; 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; @@ -64,6 +62,7 @@ void setup() { capturingComponentLoader = new CapturingComponentLoader(); spiHelper = SpiHelper.create(capturingComponentLoader); context = new DeclarativeConfigContext(spiHelper); + context.setBuilder(new DeclarativeConfigurationBuilder()); } @Test @@ -361,61 +360,81 @@ void create_SpiExporter_Valid() { } @Test - void create_CustomizerApplied() { - // Set up a customizer that wraps the exporter - DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addSpanExporterCustomizer( - SpanExporter.class, - (exporter, properties) -> new SpanExporterWrapper(exporter, "customized")); - context.setBuilder(builder); + 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 exporter = + SpanExporter result = SpanExporterFactory.getInstance() .create(new SpanExporterModel().withConsole(new ConsoleExporterModel()), context); - cleanup.addCloseable(exporter); + cleanup.addCloseable(result); - assertThat(exporter).isInstanceOf(SpanExporterWrapper.class); - assertThat(((SpanExporterWrapper) exporter).tag).isEqualTo("customized"); + // Result should be wrapped in composite + assertThat(result.toString()).contains("LoggingSpanExporter"); } @Test - void create_CustomizerReturnsNull() { - // Set up a customizer that returns null - DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addSpanExporterCustomizer(SpanExporter.class, (exporter, properties) -> null); - context.setBuilder(builder); + 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); - // Should return original exporter when customizer returns null - assertThat(result).isInstanceOf(LoggingSpanExporter.class); + // Customizer should not have been called since types don't match + assertThat(callCount.get()).isEqualTo(0); } - /** 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 CompletableResultCode export(Collection spans) { - return delegate.export(spans); - } - - @Override - public CompletableResultCode flush() { - return delegate.flush(); - } - - @Override - public CompletableResultCode shutdown() { - return delegate.shutdown(); - } + @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"); } } From 7f35762efb02ecaa678c4da4f587a5b3aef34c1c Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 20 Feb 2026 15:06:37 +0000 Subject: [PATCH 7/8] Fix existing tests: file_format version and setBuilder - Update file_format from 1.0-rc.1 to 1.0-rc.3 in create tests - Add context.setBuilder() to @BeforeEach in all factory tests that indirectly call exporter factories (processor, reader, provider tests) Claude Sonnet 4.6 assisted with this change. --- .../DeclarativeConfigurationCreateTest.java | 98 +------------------ .../LogRecordProcessorFactoryTest.java | 6 ++ .../fileconfig/LoggerProviderFactoryTest.java | 6 ++ .../fileconfig/MeterProviderFactoryTest.java | 6 ++ .../fileconfig/MetricReaderFactoryTest.java | 6 ++ ...OpenTelemetryConfigurationFactoryTest.java | 6 ++ .../fileconfig/SpanProcessorFactoryTest.java | 6 ++ ...rativeConfigurationCustomizerProvider.java | 11 --- .../fileconfig/TracerProviderFactoryTest.java | 6 ++ 9 files changed, 45 insertions(+), 106 deletions(-) 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 4853af20542..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 @@ -20,14 +20,10 @@ import io.opentelemetry.internal.testing.slf4j.SuppressLogger; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.internal.SpiHelper; -import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.BatchSpanProcessorModel; -import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ConsoleExporterModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.OpenTelemetryConfigurationModel; -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 io.opentelemetry.sdk.internal.ExtendedOpenTelemetrySdk; -import io.opentelemetry.sdk.trace.export.SpanExporter; import io.opentelemetry.sdk.trace.samplers.ParentBasedSamplerBuilder; import java.io.ByteArrayInputStream; import java.io.File; @@ -37,9 +33,7 @@ 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; @@ -120,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" @@ -147,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" @@ -165,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( @@ -205,90 +199,4 @@ 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 BatchSpanProcessorModel() - .withExporter( - new SpanExporterModel() - .withConsole(new ConsoleExporterModel())))))); - - DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addSpanExporterCustomizer( - SpanExporter.class, - (exporter, properties) -> { - customizedExporters.add("console"); - return exporter; - }); - - DeclarativeConfigContext context = - DeclarativeConfigContext.create( - ComponentLoader.forClassLoader( - DeclarativeConfigurationCreateTest.class.getClassLoader())); - context.setBuilder(builder); - - 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 BatchSpanProcessorModel() - .withExporter( - new SpanExporterModel() - .withConsole(new ConsoleExporterModel())))))); - - DeclarativeConfigurationBuilder builder = new DeclarativeConfigurationBuilder(); - builder.addSpanExporterCustomizer( - SpanExporter.class, - (exporter, properties) -> { - customizationOrder.add("first"); - return exporter; - }); - builder.addSpanExporterCustomizer( - SpanExporter.class, - (exporter, properties) -> { - customizationOrder.add("second"); - return exporter; - }); - - DeclarativeConfigContext context = - DeclarativeConfigContext.create( - ComponentLoader.forClassLoader( - DeclarativeConfigurationCreateTest.class.getClassLoader())); - context.setBuilder(builder); - - 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/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/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/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 283edd7ae26..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 @@ -7,15 +7,12 @@ import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.AttributeNameValueModel; import io.opentelemetry.sdk.extension.incubator.fileconfig.internal.model.ResourceModel; -import io.opentelemetry.sdk.trace.export.SpanExporter; import java.util.ArrayList; import java.util.List; public class TestDeclarativeConfigurationCustomizerProvider implements DeclarativeConfigurationCustomizerProvider { - public static final String EXPORTER_CUSTOMIZER_ATTRIBUTE = "exporter.customized"; - @Override public void customize(DeclarativeConfigurationCustomizer customizer) { customizer.addModelCustomizer( @@ -42,13 +39,5 @@ public void customize(DeclarativeConfigurationCustomizer customizer) { .withValue("blue")); return model; }); - - // Add exporter customizers that inject a resource attribute marker - customizer.addSpanExporterCustomizer( - SpanExporter.class, - (exporter, properties) -> { - // Mark that exporter customizer was applied by adding attribute to resource - return exporter; - }); } } 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) { From d9778712269facdaddeb38201ac35afa900c9c60 Mon Sep 17 00:00:00 2001 From: Mike Goldsmith Date: Fri, 20 Feb 2026 16:32:03 +0000 Subject: [PATCH 8/8] Close original exporter in maybeCustomize when replaced by customizer Claude Sonnet 4.6 assisted with this change. --- .../DeclarativeConfigurationBuilder.java | 13 ++++++++ .../DeclarativeConfigurationBuilderTest.java | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+) 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 527dc8c8ad0..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 @@ -11,11 +11,15 @@ 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 { @@ -78,6 +82,8 @@ public OpenTelemetryConfigurationModel customizeModel( } static class Customizer { + private static final Logger logger = Logger.getLogger(Customizer.class.getName()); + private final Class exporterType; private final BiFunction customizer; @@ -97,6 +103,13 @@ T maybeCustomize(T exporter, String name, DeclarativeConfigProperties properties 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/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 index cb60cee6bf2..d1abd33fcd9 100644 --- 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 @@ -7,7 +7,10 @@ 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; @@ -80,4 +83,34 @@ void logRecordExporterCustomizer_Multiple_Compose() { 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(); + } }