From d3f21b5c61c58ff3fa8bfef9c312cc644d710e81 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 20 May 2026 14:44:56 -0500 Subject: [PATCH 1/5] Add OSGi support for autoconfigure and declarative config --- exporters/otlp/all/build.gradle.kts | 6 + extensions/trace-propagators/build.gradle.kts | 5 + integration-tests/osgi/build.gradle.kts | 78 ++++++++- .../osgi/AutoconfigureTest.java | 108 ++++++++++++ ...stAutoConfigurationCustomizerProvider.java | 26 +++ .../osgi/TestMetricReaderProvider.java | 53 ++++++ .../osgi/TestSamplerProvider.java | 53 ++++++ ...re.spi.AutoConfigurationCustomizerProvider | 1 + ....internal.ConfigurableMetricReaderProvider | 1 + ...ure.spi.traces.ConfigurableSamplerProvider | 1 + .../AutoconfigureDeclarativeConfigTest.java | 155 ++++++++++++++++++ ...stAutoConfigurationCustomizerProvider.java | 26 +++ .../osgi/TestMetricReaderProvider.java | 53 ++++++ .../osgi/TestSamplerProvider.java | 53 ++++++ ...re.spi.AutoConfigurationCustomizerProvider | 1 + ....internal.ConfigurableMetricReaderProvider | 1 + ...ure.spi.traces.ConfigurableSamplerProvider | 1 + sdk-extensions/autoconfigure/build.gradle.kts | 21 ++- .../DeclarativeConfigContext.java | 5 + 19 files changed, 641 insertions(+), 7 deletions(-) create mode 100644 integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/AutoconfigureTest.java create mode 100644 integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider create mode 100644 integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider create mode 100644 integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider diff --git a/exporters/otlp/all/build.gradle.kts b/exporters/otlp/all/build.gradle.kts index 62fee8cfc87..3ab8bf3d499 100644 --- a/exporters/otlp/all/build.gradle.kts +++ b/exporters/otlp/all/build.gradle.kts @@ -12,6 +12,12 @@ otelJava.moduleName.set("io.opentelemetry.exporter.otlp") otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator.config")) // io.grpc and org.jspecify.annotations are not OSGi bundles; must use unversioned optional. otelJava.osgiUnversionedOptionalPackages.set(listOf("io.grpc", "org.jspecify.annotations")) +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) base.archivesName.set("opentelemetry-exporter-otlp") dependencies { diff --git a/extensions/trace-propagators/build.gradle.kts b/extensions/trace-propagators/build.gradle.kts index 8ede73f0b0e..192e638b224 100644 --- a/extensions/trace-propagators/build.gradle.kts +++ b/extensions/trace-propagators/build.gradle.kts @@ -8,6 +8,11 @@ plugins { description = "OpenTelemetry Extension : Trace Propagators" otelJava.moduleName.set("io.opentelemetry.extension.trace.propagation") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { api(project(":api:all")) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index c13bbfdf892..ef479e95f70 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -55,6 +55,9 @@ fun registerOsgiSuite( suiteName: String, extraRunrequires: List = emptyList(), extraRunsystempackages: List = emptyList(), + // SPI types that the testing bundle provides via META-INF/services (noop test implementations). + // Generates Provide-Capability + Require-Capability registrar so SPI Fly picks them up. + serviceLoaderProvides: List = emptyList(), minJavaVersion: Int? = null, configureDependencies: OsgiSuiteDependencies.() -> Unit = {} ): TaskProvider { @@ -84,10 +87,15 @@ fun registerOsgiSuite( // @Testable annotation to populate Test-Cases). Without this, testImplementation dependencies // like junit-jupiter are invisible to BND, causing Test-Cases to be empty and 0 tests to run. classpath(sourceSet.runtimeClasspath) - bnd( + val bndArgs = mutableListOf( "Bundle-SymbolicName: $bsn", "Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}" ) + if (serviceLoaderProvides.isNotEmpty()) { + bndArgs.add("Provide-Capability: ${serviceLoaderProvides.joinToString(",") { "osgi.serviceloader;osgi.serviceloader=\"$it\"" }}") + bndArgs.add("Require-Capability: osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\"") + } + bnd(*bndArgs.toTypedArray()) } } @@ -204,10 +212,6 @@ fun registerOsgiSuite( // bundle which includes those, then mask the fact that OSGi fails when using a bundle without those // until opentelemetry-api OSGi configuration is updated to indicate that they are optional. -// TODO (jack-berg): Add additional test bundles with dependency combinations reflecting popular use cases: -// - with autoconfigure -// - with file configuration - // Suite: sdk — exercises core SDK OSGi metadata in isolation val sdkSuiteTask = registerOsgiSuite("sdk") { implementation(project(":sdk:all")) @@ -248,6 +252,61 @@ val otlpGrpcOkHttpSuiteTask = registerOsgiSuite( implementation(project(":exporters:otlp:all")) } +// Autoconfigure suites. Use real exporter modules so both the consumer side +// (autoconfigure Require-Capability) and the provider side (exporter Provide-Capability) are +// validated by the BND resolver. + +// Noop SPI implementations provided by the autoconfigure testing bundles. These cover SPI types +// that autoconfigure loads but whose only real implementations (jaeger-remote-sampler, prometheus) +// are too heavyweight to include in a basic OSGi smoke test. +val autoconfigureNoopProvides = listOf( + "io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider", +) + +// Suite: autoconfigure with OTLP + JDK sender. Exercises the full SPI loading chain across all +// relevant SPI types: ConfigurableSpanExporterProvider / ConfigurableMetricExporterProvider / +// ConfigurableLogRecordExporterProvider (otlp:all), ConfigurablePropagatorProvider +// (trace-propagators), ResourceProvider (autoconfigure itself), plus noops for the rest. +val autoconfigureSuiteTask = registerOsgiSuite( + "autoconfigure", + extraRunrequires = listOf( + "opentelemetry-exporter-sender-jdk", + "opentelemetry-exporter-otlp", + "opentelemetry-extension-trace-propagators", + ), + serviceLoaderProvides = autoconfigureNoopProvides, + minJavaVersion = 11, +) { + implementation(project(":sdk:all")) + implementation(project(":sdk-extensions:autoconfigure")) + implementation(project(":exporters:otlp:all")) + implementation(project(":extensions:trace-propagators")) + runtimeOnly(project(":exporters:sender:jdk")) +} + +// Suite: autoconfigure + declarative-config. Same as above but with declarative-config on the +// classpath, additionally exercising declarative-config bundle OSGi metadata. +val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( + "autoconfigureDeclarativeConfig", + extraRunrequires = listOf( + "opentelemetry-exporter-sender-jdk", + "opentelemetry-exporter-otlp", + "opentelemetry-extension-trace-propagators", + "opentelemetry-sdk-extension-declarative-config", + ), + serviceLoaderProvides = autoconfigureNoopProvides, + minJavaVersion = 11, +) { + implementation(project(":sdk:all")) + implementation(project(":sdk-extensions:autoconfigure")) + implementation(project(":sdk-extensions:declarative-config")) + implementation(project(":exporters:otlp:all")) + implementation(project(":extensions:trace-propagators")) + runtimeOnly(project(":exporters:sender:jdk")) +} + tasks { jar { enabled = false @@ -256,7 +315,14 @@ tasks { // We need to replace junit testing with the testOSGi tasks, so we clear test actions and add // dependencies on all suite tasks. As a result, running :test runs all OSGi suites. actions.clear() - dependsOn(sdkSuiteTask, otlpHttpJdkSuiteTask, otlpHttpOkHttpSuiteTask, otlpGrpcOkHttpSuiteTask) + dependsOn( + sdkSuiteTask, + otlpHttpJdkSuiteTask, + otlpHttpOkHttpSuiteTask, + otlpGrpcOkHttpSuiteTask, + autoconfigureSuiteTask, + autoconfigureDeclarativeConfigSuiteTask + ) } } diff --git a/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/AutoconfigureTest.java b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/AutoconfigureTest.java new file mode 100644 index 00000000000..fb79075a1d7 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/AutoconfigureTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.BatchLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; +import java.util.HashMap; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +/** Verifies autoconfigure works in OSGi. */ +@ExtendWith(BundleContextExtension.class) +public class AutoconfigureTest { + + @Test + void autoConfiguredSdkInitializes() { + AutoConfiguredOpenTelemetrySdk autoConfigured = + AutoConfiguredOpenTelemetrySdk.builder() + .addPropertiesSupplier(AutoconfigureTest::config) + .build(); + + // The component loader autoconfigure uses: autoconfigure bundle's classloader. + ComponentLoader autoConfigureLoader = + ComponentLoader.forClassLoader(AutoConfiguredOpenTelemetrySdk.class.getClassLoader()); + + Resource resource = + Resource.getDefault() + .merge( + Resource.create( + Attributes.of( + AttributeKey.stringKey("test.customizer"), "test-osgi-customizer"))); + OpenTelemetrySdk expected = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .setResource(resource) + .setSampler(new TestSamplerProvider.NoopSampler()) + .addSpanProcessor( + BatchSpanProcessor.builder( + OtlpHttpSpanExporter.builder() + .setComponentLoader(autoConfigureLoader) + .build()) + .build()) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .setResource(resource) + .registerMetricReader(new TestMetricReaderProvider.NoopMetricReader()) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .setResource(resource) + .addLogRecordProcessor( + BatchLogRecordProcessor.builder( + OtlpHttpLogRecordExporter.builder() + .setComponentLoader(autoConfigureLoader) + .build()) + .build()) + .build()) + .setPropagators( + ContextPropagators.create( + TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), + W3CBaggagePropagator.getInstance(), + B3Propagator.injectingSingleHeader()))) + .build(); + try { + assertThat(autoConfigured.getOpenTelemetrySdk().toString()).isEqualTo(expected.toString()); + } finally { + autoConfigured.getOpenTelemetrySdk().close(); + expected.close(); + } + } + + private static Map config() { + Map props = new HashMap<>(); + props.put("otel.traces.exporter", "otlp"); + props.put("otel.metrics.exporter", "test-noop-reader"); + props.put("otel.logs.exporter", "otlp"); + props.put("otel.exporter.otlp.protocol", "http/protobuf"); + props.put("otel.propagators", "tracecontext,baggage,b3"); + props.put("otel.traces.sampler", "test-noop"); + return props; + } +} diff --git a/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java new file mode 100644 index 00000000000..830b4f5e123 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.resources.Resource; + +public class TestAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + autoConfiguration.addResourceCustomizer( + (resource, config) -> + resource.merge( + Resource.create( + Attributes.of( + AttributeKey.stringKey("test.customizer"), "test-osgi-customizer")))); + } +} diff --git a/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java new file mode 100644 index 00000000000..f54bbd93e71 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricReader; + +public class TestMetricReaderProvider implements ConfigurableMetricReaderProvider { + + @Override + public String getName() { + return "test-noop-reader"; + } + + @Override + public MetricReader createMetricReader(ConfigProperties config) { + return new NoopMetricReader(); + } + + static final class NoopMetricReader implements MetricReader { + + @Override + public void register(CollectionRegistration registration) {} + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public String toString() { + return "TestNoopMetricReader"; + } + } +} diff --git a/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java new file mode 100644 index 00000000000..2f50e8dc69a --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +public class TestSamplerProvider implements ConfigurableSamplerProvider { + + @Override + public String getName() { + return "test-noop"; + } + + @Override + public Sampler createSampler(ConfigProperties config) { + return new NoopSampler(); + } + + static final class NoopSampler implements Sampler { + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.recordAndSample(); + } + + @Override + public String getDescription() { + return "TestNoopSampler"; + } + + @Override + public String toString() { + return "TestNoopSampler"; + } + } +} diff --git a/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider new file mode 100644 index 00000000000..fd2a3e7b34e --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestAutoConfigurationCustomizerProvider diff --git a/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider b/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider new file mode 100644 index 00000000000..49f70c84af6 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestMetricReaderProvider diff --git a/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider b/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider new file mode 100644 index 00000000000..14663a9a1ab --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigure/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestSamplerProvider diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java new file mode 100644 index 00000000000..19e9e8baf20 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java @@ -0,0 +1,155 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; +import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; +import io.opentelemetry.common.ComponentLoader; +import io.opentelemetry.context.propagation.ContextPropagators; +import io.opentelemetry.context.propagation.TextMapPropagator; +import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.extension.trace.propagation.B3Propagator; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.internal.OpenTelemetrySdkBuilderUtil; +import io.opentelemetry.sdk.internal.SdkConfigProvider; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.osgi.test.junit5.context.BundleContextExtension; + +/** Verifies autoconfigure with declarative config works in OSGi. */ +@ExtendWith(BundleContextExtension.class) +public class AutoconfigureDeclarativeConfigTest { + + @Test + void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { + // No explicit endpoint — exporters use their defaults (e.g. http://localhost:4318/v1/traces). + // This avoids coupling the expected SDK to endpoint URL formatting details in the YAML parser. + String yaml = + "file_format: \"1.0\"\n" + + "resource:\n" + + " attributes:\n" + + " - name: service.name\n" + + " value: test-osgi-declarative\n" + + "propagator:\n" + + " composite:\n" + + " - tracecontext:\n" + + " - b3:\n" + + "tracer_provider:\n" + + " processors:\n" + + " - simple:\n" + + " exporter:\n" + + " otlp_http: {}\n" + + "meter_provider:\n" + + " readers:\n" + + " - periodic:\n" + + " exporter:\n" + + " otlp_http: {}\n" + + "logger_provider:\n" + + " processors:\n" + + " - simple:\n" + + " exporter:\n" + + " otlp_http: {}\n"; + Path configFile = tempDir.resolve("otel-config.yaml"); + Files.write(configFile, yaml.getBytes(StandardCharsets.UTF_8)); + + // System.setProperty is required: addPropertiesSupplier because property suppliers are not + // resolved until after otel.config.file check + System.setProperty("otel.config.file", configFile.toString()); + AutoConfiguredOpenTelemetrySdk autoConfigured = + AutoConfiguredOpenTelemetrySdk.builder().build(); + + ComponentLoader delegateLoader = + ComponentLoader.forClassLoader(AutoConfiguredOpenTelemetrySdk.class.getClassLoader()); + ComponentLoader autoConfigureLoader = + componentLoaderWithToString( + delegateLoader, "DeclarativeConfigContext{componentLoader=" + delegateLoader + "}"); + + Resource resource = + Resource.getDefault() + .merge( + Resource.create( + Attributes.of( + AttributeKey.stringKey("service.name"), "test-osgi-declarative"))); + OpenTelemetrySdk expected = + OpenTelemetrySdkBuilderUtil.setConfigProvider( + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .setResource(resource) + .addSpanProcessor( + SimpleSpanProcessor.create( + OtlpHttpSpanExporter.builder() + .setComponentLoader(autoConfigureLoader) + .build())) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .setResource(resource) + .registerMetricReader( + PeriodicMetricReader.create( + OtlpHttpMetricExporter.builder() + .setComponentLoader(autoConfigureLoader) + .build())) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .setResource(resource) + .addLogRecordProcessor( + SimpleLogRecordProcessor.create( + OtlpHttpLogRecordExporter.builder() + .setComponentLoader(autoConfigureLoader) + .build())) + .build()) + .setPropagators( + ContextPropagators.create( + TextMapPropagator.composite( + W3CTraceContextPropagator.getInstance(), + B3Propagator.injectingSingleHeader()))), + SdkConfigProvider.create(DeclarativeConfigProperties.empty())) + .build(); + try { + assertThat(autoConfigured.getOpenTelemetrySdk().toString()).isEqualTo(expected.toString()); + } finally { + autoConfigured.getOpenTelemetrySdk().close(); + expected.close(); + System.clearProperty("otel.config.file"); + } + } + + private static ComponentLoader componentLoaderWithToString( + ComponentLoader componentLoader, String toString) { + return new ComponentLoader() { + @Override + public Iterable load(Class spiClass) { + return componentLoader.load(spiClass); + } + + @Override + public String toString() { + return toString; + } + }; + } +} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java new file mode 100644 index 00000000000..830b4f5e123 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; +import io.opentelemetry.sdk.resources.Resource; + +public class TestAutoConfigurationCustomizerProvider + implements AutoConfigurationCustomizerProvider { + + @Override + public void customize(AutoConfigurationCustomizer autoConfiguration) { + autoConfiguration.addResourceCustomizer( + (resource, config) -> + resource.merge( + Resource.create( + Attributes.of( + AttributeKey.stringKey("test.customizer"), "test-osgi-customizer")))); + } +} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java new file mode 100644 index 00000000000..f54bbd93e71 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.export.CollectionRegistration; +import io.opentelemetry.sdk.metrics.export.MetricReader; + +public class TestMetricReaderProvider implements ConfigurableMetricReaderProvider { + + @Override + public String getName() { + return "test-noop-reader"; + } + + @Override + public MetricReader createMetricReader(ConfigProperties config) { + return new NoopMetricReader(); + } + + static final class NoopMetricReader implements MetricReader { + + @Override + public void register(CollectionRegistration registration) {} + + @Override + public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } + + @Override + public CompletableResultCode forceFlush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public String toString() { + return "TestNoopMetricReader"; + } + } +} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java new file mode 100644 index 00000000000..2f50e8dc69a --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; +import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider; +import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.samplers.Sampler; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.List; + +public class TestSamplerProvider implements ConfigurableSamplerProvider { + + @Override + public String getName() { + return "test-noop"; + } + + @Override + public Sampler createSampler(ConfigProperties config) { + return new NoopSampler(); + } + + static final class NoopSampler implements Sampler { + + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.recordAndSample(); + } + + @Override + public String getDescription() { + return "TestNoopSampler"; + } + + @Override + public String toString() { + return "TestNoopSampler"; + } + } +} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider new file mode 100644 index 00000000000..fd2a3e7b34e --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestAutoConfigurationCustomizerProvider diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider new file mode 100644 index 00000000000..49f70c84af6 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestMetricReaderProvider diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider new file mode 100644 index 00000000000..14663a9a1ab --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestSamplerProvider diff --git a/sdk-extensions/autoconfigure/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts index 9005fa59e94..288fff4be41 100644 --- a/sdk-extensions/autoconfigure/build.gradle.kts +++ b/sdk-extensions/autoconfigure/build.gradle.kts @@ -5,7 +5,26 @@ plugins { description = "OpenTelemetry SDK Auto-configuration" otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure") -otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.extension.incubator")) +otelJava.osgiOptionalPackages.set(listOf( + "io.opentelemetry.sdk.extension.incubator", + "io.opentelemetry.api.incubator", + "io.opentelemetry.sdk.autoconfigure.declarativeconfig", +)) +// autoconfigure discovers these SPI types at runtime via ServiceLoader +otelJava.osgiServiceLoaderRequires.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider", + "io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider", + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider", + "io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider", +)) +// autoconfigure provides EnvironmentResourceProvider via META-INF/services +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider", +)) dependencies { api(project(":sdk:all")) diff --git a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java index 607a4397df0..a93c0999bea 100644 --- a/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java +++ b/sdk-extensions/declarative-config/src/main/java/io/opentelemetry/sdk/autoconfigure/declarativeconfig/DeclarativeConfigContext.java @@ -197,6 +197,11 @@ T loadComponent(Class type, ConfigKeyValue configKeyValue) { } } + @Override + public String toString() { + return "DeclarativeConfigContext{componentLoader=" + componentLoader + '}'; + } + @Override public Iterable load(Class spiClass) { List result = ComponentLoader.loadList(componentLoader, spiClass); From b2f9b11c81fc97ff81a746bb039c4cc29a9446d8 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 20 May 2026 15:18:34 -0500 Subject: [PATCH 2/5] Refine declarative config osgi test, add missing provides-capability, add unit test to ensure provides-capability always matchines META-INF/services --- .../all/OsgiServiceLoaderManifestTest.java | 117 ++++++++++++++++++ exporters/logging-otlp/build.gradle.kts | 6 + exporters/logging/build.gradle.kts | 6 + exporters/prometheus/build.gradle.kts | 4 + .../grpc-managed-channel/build.gradle.kts | 3 + exporters/zipkin/build.gradle.kts | 3 + integration-tests/osgi/build.gradle.kts | 25 ++-- .../AutoconfigureDeclarativeConfigTest.java | 21 ++-- ...stAutoConfigurationCustomizerProvider.java | 26 ---- .../osgi/TestMetricReaderProvider.java | 53 -------- .../osgi/TestSamplerProvider.java | 53 -------- ...re.spi.AutoConfigurationCustomizerProvider | 1 - ....internal.ConfigurableMetricReaderProvider | 1 - ...ure.spi.traces.ConfigurableSamplerProvider | 1 - .../declarative-config/build.gradle.kts | 3 + sdk-extensions/incubator/build.gradle.kts | 4 + .../jaeger-remote-sampler/build.gradle.kts | 4 + sdk/testing/build.gradle.kts | 3 + 18 files changed, 174 insertions(+), 160 deletions(-) create mode 100644 all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java delete mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java delete mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java delete mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java delete mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider delete mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider delete mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider diff --git a/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java b/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java new file mode 100644 index 00000000000..8b90eec7caa --- /dev/null +++ b/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java @@ -0,0 +1,117 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.all; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.jar.Attributes; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.jar.Manifest; +import org.junit.jupiter.api.Test; + +/** + * Verifies that every OSGi bundle whose {@code META-INF/services/} directory registers SPI + * implementations also declares the corresponding {@code Provide-Capability: + * osgi.serviceloader;osgi.serviceloader=""} in its manifest. + */ +class OsgiServiceLoaderManifestTest { + + @Test + void allOsgiBundlesAdvertiseTheirServiceLoaderRegistrations() throws IOException { + List lines = Files.readAllLines(Path.of(System.getenv("ARTIFACTS_AND_JARS"))); + // violations: ": META-INF/services/ not in Provide-Capability" + List violations = new ArrayList<>(); + + for (String line : lines) { + String[] parts = line.split(":", 2); + String baseName = parts[0]; + String absolutePath = parts[1]; + + try (JarFile jar = new JarFile(new File(absolutePath))) { + Manifest manifest = jar.getManifest(); + if (manifest == null) { + continue; + } + Attributes mainAttrs = manifest.getMainAttributes(); + + // Only check OSGi bundles. + String bundleManifestVersion = mainAttrs.getValue("Bundle-ManifestVersion"); + if (bundleManifestVersion == null) { + continue; + } + + // Collect all SPI interface names from META-INF/services/. + List registeredSpis = new ArrayList<>(); + jar.stream() + .map(JarEntry::getName) + .filter(name -> name.startsWith("META-INF/services/") && !name.endsWith("/")) + .forEach(name -> registeredSpis.add(name.substring("META-INF/services/".length()))); + + if (registeredSpis.isEmpty()) { + continue; + } + + // Parse Provide-Capability for osgi.serviceloader entries. + String provideCapability = mainAttrs.getValue("Provide-Capability"); + List advertisedSpis = parseOsgiServiceLoaderCapabilities(provideCapability); + + for (String spi : registeredSpis) { + if (!advertisedSpis.contains(spi)) { + violations.add(baseName + ": META-INF/services/" + spi + " not in Provide-Capability"); + } + } + } + } + + assertThat(violations) + .as( + "OSGi bundles with META-INF/services registrations missing from Provide-Capability.\n" + + "Add the missing SPI to osgiServiceLoaderProvides in the module's build.gradle.kts.") + .isEmpty(); + } + + /** + * Parses the {@code Provide-Capability} manifest header and returns all {@code + * osgi.serviceloader} service type names. + * + *

Example: {@code osgi.serviceloader;osgi.serviceloader="com.example.Foo", + * osgi.serviceloader;osgi.serviceloader="com.example.Bar"} → {@code ["com.example.Foo", + * "com.example.Bar"]} + */ + private static List parseOsgiServiceLoaderCapabilities(String provideCapability) { + List result = new ArrayList<>(); + if (provideCapability == null || provideCapability.isEmpty()) { + return result; + } + // The header may be line-folded (continuation lines start with a space) — already unfolded + // by JarFile. Split on commas that are not inside quotes. + String[] clauses = provideCapability.split(",(?=\\s*osgi\\.)"); + for (String clause : clauses) { + clause = clause.trim(); + if (!clause.startsWith("osgi.serviceloader")) { + continue; + } + // Extract osgi.serviceloader="" + int eq = clause.indexOf("osgi.serviceloader=\""); + if (eq < 0) { + continue; + } + int start = eq + "osgi.serviceloader=\"".length(); + int end = clause.indexOf('"', start); + if (end > start) { + result.add(clause.substring(start, end)); + } + } + return result; + } +} diff --git a/exporters/logging-otlp/build.gradle.kts b/exporters/logging-otlp/build.gradle.kts index 0870f2c9937..1e9217386c5 100644 --- a/exporters/logging-otlp/build.gradle.kts +++ b/exporters/logging-otlp/build.gradle.kts @@ -7,6 +7,12 @@ plugins { description = "OpenTelemetry Protocol JSON Logging Exporters" otelJava.moduleName.set("io.opentelemetry.exporter.logging.otlp") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { implementation(project(":sdk:trace")) diff --git a/exporters/logging/build.gradle.kts b/exporters/logging/build.gradle.kts index 82e96771834..27beed5564c 100644 --- a/exporters/logging/build.gradle.kts +++ b/exporters/logging/build.gradle.kts @@ -7,6 +7,12 @@ plugins { description = "OpenTelemetry - Logging Exporter" otelJava.moduleName.set("io.opentelemetry.exporter.logging") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { api(project(":sdk:all")) diff --git a/exporters/prometheus/build.gradle.kts b/exporters/prometheus/build.gradle.kts index 6587f82784d..bf6ce726f60 100644 --- a/exporters/prometheus/build.gradle.kts +++ b/exporters/prometheus/build.gradle.kts @@ -5,6 +5,10 @@ plugins { description = "OpenTelemetry Prometheus Exporter" otelJava.moduleName.set("io.opentelemetry.exporter.prometheus") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { api(project(":sdk:metrics")) diff --git a/exporters/sender/grpc-managed-channel/build.gradle.kts b/exporters/sender/grpc-managed-channel/build.gradle.kts index 9ee493fb7a1..a92e6b54017 100644 --- a/exporters/sender/grpc-managed-channel/build.gradle.kts +++ b/exporters/sender/grpc-managed-channel/build.gradle.kts @@ -7,6 +7,9 @@ plugins { description = "OpenTelemetry gRPC Upstream Sender" otelJava.moduleName.set("io.opentelemetry.exporter.sender.grpc.managedchannel.internal") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.common.export.GrpcSenderProvider", +)) dependencies { annotationProcessor("com.google.auto.value:auto-value") diff --git a/exporters/zipkin/build.gradle.kts b/exporters/zipkin/build.gradle.kts index bee1da945c4..a80484500ae 100644 --- a/exporters/zipkin/build.gradle.kts +++ b/exporters/zipkin/build.gradle.kts @@ -7,6 +7,9 @@ plugins { description = "OpenTelemetry - Zipkin Exporter" otelJava.moduleName.set("io.opentelemetry.exporter.zipkin") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider", +)) dependencies { api(project(":sdk:all")) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index ef479e95f70..7768d7cee43 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -252,23 +252,10 @@ val otlpGrpcOkHttpSuiteTask = registerOsgiSuite( implementation(project(":exporters:otlp:all")) } -// Autoconfigure suites. Use real exporter modules so both the consumer side -// (autoconfigure Require-Capability) and the provider side (exporter Provide-Capability) are -// validated by the BND resolver. - -// Noop SPI implementations provided by the autoconfigure testing bundles. These cover SPI types -// that autoconfigure loads but whose only real implementations (jaeger-remote-sampler, prometheus) -// are too heavyweight to include in a basic OSGi smoke test. -val autoconfigureNoopProvides = listOf( - "io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider", - "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider", - "io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider", -) +// Autoconfigure suites. // Suite: autoconfigure with OTLP + JDK sender. Exercises the full SPI loading chain across all -// relevant SPI types: ConfigurableSpanExporterProvider / ConfigurableMetricExporterProvider / -// ConfigurableLogRecordExporterProvider (otlp:all), ConfigurablePropagatorProvider -// (trace-propagators), ResourceProvider (autoconfigure itself), plus noops for the rest. +// SPI types. val autoconfigureSuiteTask = registerOsgiSuite( "autoconfigure", extraRunrequires = listOf( @@ -276,7 +263,12 @@ val autoconfigureSuiteTask = registerOsgiSuite( "opentelemetry-exporter-otlp", "opentelemetry-extension-trace-propagators", ), - serviceLoaderProvides = autoconfigureNoopProvides, + // Some SPIs have implementations in project modules. Others do not. To verify the ones without implementation, we provide noop implementations here. + serviceLoaderProvides = listOf( + "io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider", + ), minJavaVersion = 11, ) { implementation(project(":sdk:all")) @@ -296,7 +288,6 @@ val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( "opentelemetry-extension-trace-propagators", "opentelemetry-sdk-extension-declarative-config", ), - serviceLoaderProvides = autoconfigureNoopProvides, minJavaVersion = 11, ) { implementation(project(":sdk:all")) diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java index 19e9e8baf20..56f1e8a8a8d 100644 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java @@ -8,7 +8,6 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.incubator.config.DeclarativeConfigProperties; import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator; import io.opentelemetry.common.ComponentLoader; @@ -20,6 +19,7 @@ import io.opentelemetry.extension.trace.propagation.B3Propagator; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.ServiceResourceDetector; import io.opentelemetry.sdk.internal.OpenTelemetrySdkBuilderUtil; import io.opentelemetry.sdk.internal.SdkConfigProvider; import io.opentelemetry.sdk.logs.SdkLoggerProvider; @@ -44,14 +44,15 @@ public class AutoconfigureDeclarativeConfigTest { @Test void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { - // No explicit endpoint — exporters use their defaults (e.g. http://localhost:4318/v1/traces). - // This avoids coupling the expected SDK to endpoint URL formatting details in the YAML parser. String yaml = "file_format: \"1.0\"\n" + "resource:\n" + " attributes:\n" + " - name: service.name\n" + " value: test-osgi-declarative\n" + + " detection/development:\n" + + " detectors:\n" + + " - service: {}\n" + "propagator:\n" + " composite:\n" + " - tracecontext:\n" @@ -86,12 +87,16 @@ void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { componentLoaderWithToString( delegateLoader, "DeclarativeConfigContext{componentLoader=" + delegateLoader + "}"); + // ServiceResourceDetector.RANDOM_SERVICE_INSTANCE_ID is a static field — same value within + // a JVM run, so the expected and actual resources match deterministically. + Resource detectedResource = + new ServiceResourceDetector().create(DeclarativeConfigProperties.empty()); Resource resource = - Resource.getDefault() - .merge( - Resource.create( - Attributes.of( - AttributeKey.stringKey("service.name"), "test-osgi-declarative"))); + Resource.getDefault().toBuilder() + .putAll(detectedResource.getAttributes()) + .put(AttributeKey.stringKey("service.name"), "test-osgi-declarative") + .build(); + OpenTelemetrySdk expected = OpenTelemetrySdkBuilderUtil.setConfigProvider( OpenTelemetrySdk.builder() diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java deleted file mode 100644 index 830b4f5e123..00000000000 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestAutoConfigurationCustomizerProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.integrationtest.osgi; - -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer; -import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider; -import io.opentelemetry.sdk.resources.Resource; - -public class TestAutoConfigurationCustomizerProvider - implements AutoConfigurationCustomizerProvider { - - @Override - public void customize(AutoConfigurationCustomizer autoConfiguration) { - autoConfiguration.addResourceCustomizer( - (resource, config) -> - resource.merge( - Resource.create( - Attributes.of( - AttributeKey.stringKey("test.customizer"), "test-osgi-customizer")))); - } -} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java deleted file mode 100644 index f54bbd93e71..00000000000 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestMetricReaderProvider.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.integrationtest.osgi; - -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider; -import io.opentelemetry.sdk.common.CompletableResultCode; -import io.opentelemetry.sdk.metrics.InstrumentType; -import io.opentelemetry.sdk.metrics.data.AggregationTemporality; -import io.opentelemetry.sdk.metrics.export.CollectionRegistration; -import io.opentelemetry.sdk.metrics.export.MetricReader; - -public class TestMetricReaderProvider implements ConfigurableMetricReaderProvider { - - @Override - public String getName() { - return "test-noop-reader"; - } - - @Override - public MetricReader createMetricReader(ConfigProperties config) { - return new NoopMetricReader(); - } - - static final class NoopMetricReader implements MetricReader { - - @Override - public void register(CollectionRegistration registration) {} - - @Override - public AggregationTemporality getAggregationTemporality(InstrumentType instrumentType) { - return AggregationTemporality.CUMULATIVE; - } - - @Override - public CompletableResultCode forceFlush() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode shutdown() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public String toString() { - return "TestNoopMetricReader"; - } - } -} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java deleted file mode 100644 index 2f50e8dc69a..00000000000 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestSamplerProvider.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.integrationtest.osgi; - -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.trace.SpanKind; -import io.opentelemetry.context.Context; -import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties; -import io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider; -import io.opentelemetry.sdk.trace.data.LinkData; -import io.opentelemetry.sdk.trace.samplers.Sampler; -import io.opentelemetry.sdk.trace.samplers.SamplingResult; -import java.util.List; - -public class TestSamplerProvider implements ConfigurableSamplerProvider { - - @Override - public String getName() { - return "test-noop"; - } - - @Override - public Sampler createSampler(ConfigProperties config) { - return new NoopSampler(); - } - - static final class NoopSampler implements Sampler { - - @Override - public SamplingResult shouldSample( - Context parentContext, - String traceId, - String name, - SpanKind spanKind, - Attributes attributes, - List parentLinks) { - return SamplingResult.recordAndSample(); - } - - @Override - public String getDescription() { - return "TestNoopSampler"; - } - - @Override - public String toString() { - return "TestNoopSampler"; - } - } -} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider deleted file mode 100644 index fd2a3e7b34e..00000000000 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider +++ /dev/null @@ -1 +0,0 @@ -io.opentelemetry.integrationtest.osgi.TestAutoConfigurationCustomizerProvider diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider deleted file mode 100644 index 49f70c84af6..00000000000 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider +++ /dev/null @@ -1 +0,0 @@ -io.opentelemetry.integrationtest.osgi.TestMetricReaderProvider diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider deleted file mode 100644 index 14663a9a1ab..00000000000 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider +++ /dev/null @@ -1 +0,0 @@ -io.opentelemetry.integrationtest.osgi.TestSamplerProvider diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index ba5f3b18494..441e18f0e7a 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -12,6 +12,9 @@ plugins { description = "OpenTelemetry SDK Declarative Config" otelJava.moduleName.set("io.opentelemetry.sdk.declarativeconfig") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { api(project(":sdk:all")) diff --git a/sdk-extensions/incubator/build.gradle.kts b/sdk-extensions/incubator/build.gradle.kts index 77bd6ef65a3..f81c6d1180b 100644 --- a/sdk-extensions/incubator/build.gradle.kts +++ b/sdk-extensions/incubator/build.gradle.kts @@ -9,6 +9,10 @@ plugins { description = "OpenTelemetry SDK Incubator" otelJava.moduleName.set("io.opentelemetry.sdk.extension.incubator") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { api(project(":sdk:all")) diff --git a/sdk-extensions/jaeger-remote-sampler/build.gradle.kts b/sdk-extensions/jaeger-remote-sampler/build.gradle.kts index 7852f360870..14b18cf8d05 100644 --- a/sdk-extensions/jaeger-remote-sampler/build.gradle.kts +++ b/sdk-extensions/jaeger-remote-sampler/build.gradle.kts @@ -9,6 +9,10 @@ plugins { description = "OpenTelemetry - Jaeger Remote sampler" otelJava.moduleName.set("io.opentelemetry.sdk.extension.trace.jaeger") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider", + "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", +)) dependencies { api(project(":sdk:all")) diff --git a/sdk/testing/build.gradle.kts b/sdk/testing/build.gradle.kts index 44bbcad90d8..e68f1b26eeb 100644 --- a/sdk/testing/build.gradle.kts +++ b/sdk/testing/build.gradle.kts @@ -5,6 +5,9 @@ plugins { description = "OpenTelemetry SDK Testing utilities" otelJava.moduleName.set("io.opentelemetry.sdk.testing") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.context.ContextStorageProvider", +)) dependencies { api(project(":api:all")) From 429e5777f25107fa585df80e278df16e542ef960 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 20 May 2026 15:26:48 -0500 Subject: [PATCH 3/5] Exercise DeclarativeConfigurationCustomizerProvider in osgi test --- integration-tests/osgi/build.gradle.kts | 3 + .../AutoconfigureDeclarativeConfigTest.java | 10 +++- ...rativeConfigurationCustomizerProvider.java | 59 +++++++++++++++++++ ...DeclarativeConfigurationCustomizerProvider | 1 + .../declarative-config/build.gradle.kts | 4 ++ 5 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestDeclarativeConfigurationCustomizerProvider.java create mode 100644 integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 7768d7cee43..8e2f7a122d9 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -288,6 +288,9 @@ val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite( "opentelemetry-extension-trace-propagators", "opentelemetry-sdk-extension-declarative-config", ), + serviceLoaderProvides = listOf( + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider", + ), minJavaVersion = 11, ) { implementation(project(":sdk:all")) diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java index 56f1e8a8a8d..a3a497980c1 100644 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java @@ -105,9 +105,13 @@ void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { .setResource(resource) .addSpanProcessor( SimpleSpanProcessor.create( - OtlpHttpSpanExporter.builder() - .setComponentLoader(autoConfigureLoader) - .build())) + // TestDeclarativeConfigurationCustomizerProvider wraps + // OtlpHttpSpanExporter — proves the SPI was invoked. + new TestDeclarativeConfigurationCustomizerProvider + .TestCustomizedSpanExporter( + OtlpHttpSpanExporter.builder() + .setComponentLoader(autoConfigureLoader) + .build()))) .build()) .setMeterProvider( SdkMeterProvider.builder() diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestDeclarativeConfigurationCustomizerProvider.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestDeclarativeConfigurationCustomizerProvider.java new file mode 100644 index 00000000000..9554c0b7f33 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/TestDeclarativeConfigurationCustomizerProvider.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.integrationtest.osgi; + +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizer; +import io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; + +/** + * Noop customizer provider that wraps the OTLP HTTP span exporter with a distinctive toString, + * making the customizer's invocation verifiable in the SDK toString comparison. + */ +public class TestDeclarativeConfigurationCustomizerProvider + implements DeclarativeConfigurationCustomizerProvider { + + @Override + public void customize(DeclarativeConfigurationCustomizer customizer) { + // SpanExporter.class matches all span exporters (OtlpHttpSpanExporter in this test). + // OtlpHttpSpanExporter is final so we can't use it as type bound for the wrapper return type. + customizer.addSpanExporterCustomizer( + SpanExporter.class, (exporter, props) -> new TestCustomizedSpanExporter(exporter)); + } + + /** Wraps a SpanExporter with a distinctive toString to prove the customizer was invoked. */ + public static final class TestCustomizedSpanExporter implements SpanExporter { + + private final SpanExporter delegate; + + public TestCustomizedSpanExporter(SpanExporter delegate) { + this.delegate = delegate; + } + + @Override + public CompletableResultCode export(Collection spans) { + return delegate.export(spans); + } + + @Override + public CompletableResultCode flush() { + return delegate.flush(); + } + + @Override + public CompletableResultCode shutdown() { + return delegate.shutdown(); + } + + @Override + public String toString() { + return "TestDeclarativeCustomized{" + delegate + '}'; + } + } +} diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider new file mode 100644 index 00000000000..1135822d04a --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/resources/META-INF/services/io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider @@ -0,0 +1 @@ +io.opentelemetry.integrationtest.osgi.TestDeclarativeConfigurationCustomizerProvider diff --git a/sdk-extensions/declarative-config/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index 441e18f0e7a..dc079e92b2b 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -15,6 +15,10 @@ otelJava.moduleName.set("io.opentelemetry.sdk.declarativeconfig") otelJava.osgiServiceLoaderProvides.set(listOf( "io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider", )) +// declarative-config discovers customizer providers at runtime via ServiceLoader +otelJava.osgiServiceLoaderRequires.set(listOf( + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider", +)) dependencies { api(project(":sdk:all")) From 780b0198209eb45129acd140c31fe744b65e9614 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 20 May 2026 15:35:33 -0500 Subject: [PATCH 4/5] Tweaks --- .../opentelemetry/all/OsgiServiceLoaderManifestTest.java | 4 ++-- .../osgi/AutoconfigureDeclarativeConfigTest.java | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java b/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java index 8b90eec7caa..6d67ee67101 100644 --- a/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java +++ b/all/src/test/java/io/opentelemetry/all/OsgiServiceLoaderManifestTest.java @@ -93,8 +93,8 @@ private static List parseOsgiServiceLoaderCapabilities(String provideCap if (provideCapability == null || provideCapability.isEmpty()) { return result; } - // The header may be line-folded (continuation lines start with a space) — already unfolded - // by JarFile. Split on commas that are not inside quotes. + // JarFile already unfolds line-folded headers. Split into individual capability clauses + // on commas immediately followed by an OSGi namespace (osgi.*). String[] clauses = provideCapability.split(",(?=\\s*osgi\\.)"); for (String clause : clauses) { clause = clause.trim(); diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java index a3a497980c1..005fbfc9752 100644 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java @@ -78,8 +78,13 @@ void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { // System.setProperty is required: addPropertiesSupplier because property suppliers are not // resolved until after otel.config.file check System.setProperty("otel.config.file", configFile.toString()); - AutoConfiguredOpenTelemetrySdk autoConfigured = - AutoConfiguredOpenTelemetrySdk.builder().build(); + AutoConfiguredOpenTelemetrySdk autoConfigured; + try { + autoConfigured = AutoConfiguredOpenTelemetrySdk.builder().build(); + } catch (RuntimeException e) { + System.clearProperty("otel.config.file"); + throw e; + } ComponentLoader delegateLoader = ComponentLoader.forClassLoader(AutoConfiguredOpenTelemetrySdk.class.getClassLoader()); From 2a5f5a7aa1f5dd91d180e2062418dfc811202d04 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Thu, 21 May 2026 08:51:41 -0500 Subject: [PATCH 5/5] Remove curly braces from declarative config --- .../osgi/AutoconfigureDeclarativeConfigTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java index 005fbfc9752..4a877494317 100644 --- a/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java @@ -52,7 +52,7 @@ void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { + " value: test-osgi-declarative\n" + " detection/development:\n" + " detectors:\n" - + " - service: {}\n" + + " - service:\n" + "propagator:\n" + " composite:\n" + " - tracecontext:\n" @@ -61,17 +61,17 @@ void declarativeConfigSdkInitializes(@TempDir Path tempDir) throws IOException { + " processors:\n" + " - simple:\n" + " exporter:\n" - + " otlp_http: {}\n" + + " otlp_http:\n" + "meter_provider:\n" + " readers:\n" + " - periodic:\n" + " exporter:\n" - + " otlp_http: {}\n" + + " otlp_http:\n" + "logger_provider:\n" + " processors:\n" + " - simple:\n" + " exporter:\n" - + " otlp_http: {}\n"; + + " otlp_http:\n"; Path configFile = tempDir.resolve("otel-config.yaml"); Files.write(configFile, yaml.getBytes(StandardCharsets.UTF_8));