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..6d67ee67101 --- /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; + } + // 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(); + 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/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/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/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..8e2f7a122d9 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,55 @@ val otlpGrpcOkHttpSuiteTask = registerOsgiSuite( implementation(project(":exporters:otlp:all")) } +// Autoconfigure suites. + +// Suite: autoconfigure with OTLP + JDK sender. Exercises the full SPI loading chain across all +// SPI types. +val autoconfigureSuiteTask = registerOsgiSuite( + "autoconfigure", + extraRunrequires = listOf( + "opentelemetry-exporter-sender-jdk", + "opentelemetry-exporter-otlp", + "opentelemetry-extension-trace-propagators", + ), + // 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")) + 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 = listOf( + "io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider", + ), + 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 +309,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..4a877494317 --- /dev/null +++ b/integration-tests/osgi/src/testAutoconfigureDeclarativeConfig/java/io/opentelemetry/integrationtest/osgi/AutoconfigureDeclarativeConfigTest.java @@ -0,0 +1,169 @@ +/* + * 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.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.autoconfigure.declarativeconfig.ServiceResourceDetector; +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 { + 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" + + " - 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; + try { + autoConfigured = AutoConfiguredOpenTelemetrySdk.builder().build(); + } catch (RuntimeException e) { + System.clearProperty("otel.config.file"); + throw e; + } + + ComponentLoader delegateLoader = + ComponentLoader.forClassLoader(AutoConfiguredOpenTelemetrySdk.class.getClassLoader()); + ComponentLoader autoConfigureLoader = + 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().toBuilder() + .putAll(detectedResource.getAttributes()) + .put(AttributeKey.stringKey("service.name"), "test-osgi-declarative") + .build(); + + OpenTelemetrySdk expected = + OpenTelemetrySdkBuilderUtil.setConfigProvider( + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .setResource(resource) + .addSpanProcessor( + SimpleSpanProcessor.create( + // TestDeclarativeConfigurationCustomizerProvider wraps + // OtlpHttpSpanExporter — proves the SPI was invoked. + new TestDeclarativeConfigurationCustomizerProvider + .TestCustomizedSpanExporter( + 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/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/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/build.gradle.kts b/sdk-extensions/declarative-config/build.gradle.kts index ba5f3b18494..dc079e92b2b 100644 --- a/sdk-extensions/declarative-config/build.gradle.kts +++ b/sdk-extensions/declarative-config/build.gradle.kts @@ -12,6 +12,13 @@ plugins { description = "OpenTelemetry SDK Declarative Config" 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")) 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); 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"))