From 2d918a4078f70b167dcf74af79ef178934b2609a Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 8 Jan 2026 10:12:54 -0600 Subject: [PATCH 01/13] Add minimalize osgi configuration and integration test --- buildSrc/build.gradle.kts | 1 + .../kotlin/otel.java-conventions.gradle.kts | 12 +++ dependencyManagement/build.gradle.kts | 3 + integration-tests/osgi/build.gradle.kts | 90 +++++++++++++++++++ .../osgi/OpenTelemetryOsgiTest.java | 37 ++++++++ integration-tests/osgi/test.bndrun | 8 ++ settings.gradle.kts | 1 + 7 files changed, 152 insertions(+) create mode 100644 integration-tests/osgi/build.gradle.kts create mode 100644 integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java create mode 100644 integration-tests/osgi/test.bndrun diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index bbd7539c7d4..0e2fd74b2c6 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -33,6 +33,7 @@ repositories { } dependencies { + implementation("biz.aQute.bnd:biz.aQute.bnd.gradle:7.2.0") implementation(enforcedPlatform("com.squareup.wire:wire-bom:5.4.0")) implementation("com.google.auto.value:auto-value-annotations:1.11.1") // When updating, update above in plugins too diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 01112018b76..f7b5da28bd5 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -9,6 +9,7 @@ plugins { eclipse idea + id("biz.aQute.bnd.builder") id("otel.errorprone-conventions") id("otel.jacoco-conventions") id("otel.spotless-conventions") @@ -142,6 +143,17 @@ tasks { } } + named("jar") { + // Configure OSGi metadata + bundle { + bnd(mapOf( + // Once https://github.com/open-telemetry/opentelemetry-java/issues/6970 is resolved, exclude .internal packages + "-exportcontents" to "io.opentelemetry.*", + "Import-Package" to "javax.annotation.*;resolution:=optional;version=\"\${@}\",*", + )) + } + } + withType().configureEach { inputs.property("moduleName", otelJava.moduleName) diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 298124c3e91..c1c3bc465a4 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -36,6 +36,7 @@ val DEPENDENCY_BOMS = listOf( "io.zipkin.brave:brave-bom:6.3.0", "io.zipkin.reporter2:zipkin-reporter-bom:3.5.1", "org.assertj:assertj-bom:3.27.6", + "org.osgi:org.osgi.test.bom:1.2.1", "org.testcontainers:testcontainers-bom:2.0.3", "org.snakeyaml:snakeyaml-engine:2.10" ) @@ -87,12 +88,14 @@ val DEPENDENCIES = listOf( "io.opentracing:opentracing-noop:0.33.0", "junit:junit:4.13.2", "nl.jqno.equalsverifier:equalsverifier:3.19.4", + "org.apache.felix:org.apache.felix.framework:7.0.5", "org.awaitility:awaitility:4.3.0", "org.bouncycastle:bcpkix-jdk15on:1.70", "org.codehaus.mojo:animal-sniffer-annotations:1.26", "org.jctools:jctools-core:4.0.5", "org.junit-pioneer:junit-pioneer:1.9.1", "org.mock-server:mockserver-netty:5.15.0:shaded", + "org.osgi:osgi.core:8.0.0", "org.skyscreamer:jsonassert:1.5.3", "com.android.tools:desugar_jdk_libs:2.1.5", ) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts new file mode 100644 index 00000000000..bed7a60b612 --- /dev/null +++ b/integration-tests/osgi/build.gradle.kts @@ -0,0 +1,90 @@ +import aQute.bnd.gradle.Bundle +import aQute.bnd.gradle.Resolve +import aQute.bnd.gradle.TestOSGi + +plugins { + id("otel.java-conventions") +} + +description = "OpenTelemetry OSGi Integration Tests" +otelJava.moduleName.set("io.opentelemetry.integration.tests.osgi") + +// For similar test examples see: +// https://github.com/micrometer-metrics/micrometer/tree/main/micrometer-osgi-test +// https://github.com/eclipse-osgi-technology/osgi-test/tree/main/examples/osgi-test-example-gradle + +configurations.all { + resolutionStrategy { + // BND not compatible with JUnit 5.13+; see https://github.com/bndtools/bnd/issues/6651 + val junitVersion = "5.12.2" + val junitLauncherVersion = "1.12.1" + force("org.junit.jupiter:junit-jupiter:$junitVersion") + force("org.junit.jupiter:junit-jupiter-api:$junitVersion") + force("org.junit.jupiter:junit-jupiter-params:$junitVersion") + force("org.junit.jupiter:junit-jupiter-engine:$junitVersion") + force("org.junit.platform:junit-platform-launcher:$junitLauncherVersion") + } +} + +dependencies { + testImplementation(project(":context")) + + // For some reason, changing this to testImplementation causes the tests to pass even when failures are present. + // Probably some dependency resolution interplay with otel.java-conventions but I couldn't figure it out. + implementation("org.junit.jupiter:junit-jupiter") + + testCompileOnly("org.osgi:osgi.core") + testImplementation("org.osgi:org.osgi.test.junit5") + testImplementation("org.osgi:org.osgi.test.assertj.framework") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + testRuntimeOnly("org.apache.felix:org.apache.felix.framework") +} + +val testingBundleTask = tasks.register("testingBundle") { + archiveClassifier.set("testing") + from(sourceSets.test.get().output) + bundle { + bnd( + "Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}", + "Import-Package: javax.annotation.*;resolution:=optional;version=\"\${@}\",*" + ) + } +} + +val resolveTask = tasks.register("resolve") { + dependsOn(testingBundleTask) + project.ext.set("osgiRunee", "JavaSE-${java.toolchain.languageVersion.get()}") + description = "Resolve test.bndrun" + group = JavaBasePlugin.VERIFICATION_GROUP + bndrun = file("test.bndrun") + outputBndrun = layout.buildDirectory.file("test.bndrun") + bundles = files(sourceSets.test.get().runtimeClasspath, testingBundleTask.get().archiveFile) +} + +val testOSGiTask = tasks.register("testOSGi") { + description = "OSGi Test test.bndrun" + group = JavaBasePlugin.VERIFICATION_GROUP + bndrun = resolveTask.flatMap { it.outputBndrun } + bundles = files(sourceSets.test.get().runtimeClasspath, testingBundleTask.get().archiveFile) +} + +tasks.named(LifecycleBasePlugin.CHECK_TASK_NAME) { + dependsOn(testOSGiTask) +} + +tasks { + jar { + enabled = false + } + test { + // We need to replace junit testing with the testOSGi task, so we clear test actions and add a dependency on testOSGi. + // As a result, running :test runs only :testOSGi. + actions.clear() + dependsOn(testOSGiTask) + } +} + +// Skip OWASP check +dependencyCheck { + skip = true +} diff --git a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java new file mode 100644 index 00000000000..d606fc0c864 --- /dev/null +++ b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java @@ -0,0 +1,37 @@ +/* + * 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.context.Context; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.framework.BundleContext; +import org.osgi.test.common.annotation.InjectBundleContext; +import org.osgi.test.junit5.context.BundleContextExtension; +import org.osgi.test.junit5.service.ServiceExtension; + +@ExtendWith(BundleContextExtension.class) +@ExtendWith(ServiceExtension.class) +public class OpenTelemetryOsgiTest { + + @InjectBundleContext @Nullable BundleContext bundleContext; + + @Test + public void contextApiAvailable() { + // Verify we're running in OSGi + assertThat(bundleContext).isNotNull(); + + // Verify Context API is available + Context current = Context.current(); + assertThat(current).isNotNull(); + + // Intentionally fail to verify test execution works + assertThat(1).isEqualTo(2); + } +} diff --git a/integration-tests/osgi/test.bndrun b/integration-tests/osgi/test.bndrun new file mode 100644 index 00000000000..8a148d32d39 --- /dev/null +++ b/integration-tests/osgi/test.bndrun @@ -0,0 +1,8 @@ +-tester: biz.aQute.tester.junit-platform +-runfw: org.apache.felix.framework +-runee: ${project.osgiRunee} + +-runrequires: \ + bnd.identity;id='opentelemetry-osgi-testing',\ + bnd.identity;id='junit-jupiter-engine',\ + bnd.identity;id='junit-platform-launcher' diff --git a/settings.gradle.kts b/settings.gradle.kts index cb3853487f9..7fc7cf4a5f8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -54,6 +54,7 @@ include(":integration-tests:otlp") include(":integration-tests:tracecontext") include(":integration-tests:graal") include(":integration-tests:graal-incubating") +include(":integration-tests:osgi") include(":javadoc-crawler") include(":opencensus-shim") include(":opentracing-shim") From 16e7ab81b5449e67f6749b3808b2e1842060e3da Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 8 Jan 2026 10:27:16 -0600 Subject: [PATCH 02/13] Extend test to basic SDK test --- integration-tests/osgi/build.gradle.kts | 7 +- .../osgi/OpenTelemetryOsgiTest.java | 100 +++++++++++++++++- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index bed7a60b612..3003b99023e 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -27,7 +27,12 @@ configurations.all { } dependencies { - testImplementation(project(":context")) + testImplementation(project(":sdk:all")) + // TODO: should be able to remove these and everything functions + testImplementation(project(":api:incubator")) + testImplementation(project(":sdk-extensions:autoconfigure")) + testImplementation(project(":sdk-extensions:incubator")) + testImplementation("com.fasterxml.jackson.core:jackson-databind") // For some reason, changing this to testImplementation causes the tests to pass even when failures are present. // Probably some dependency resolution interplay with otel.java-conventions but I couldn't figure it out. diff --git a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java index d606fc0c864..057aa31319a 100644 --- a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java +++ b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java @@ -8,7 +8,25 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.context.Context; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.CompletableResultCode; +import io.opentelemetry.sdk.logs.SdkLoggerProvider; +import io.opentelemetry.sdk.logs.data.LogRecordData; +import io.opentelemetry.sdk.logs.export.LogRecordExporter; +import io.opentelemetry.sdk.logs.export.SimpleLogRecordProcessor; +import io.opentelemetry.sdk.metrics.InstrumentType; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.metrics.data.AggregationTemporality; +import io.opentelemetry.sdk.metrics.data.MetricData; +import io.opentelemetry.sdk.metrics.export.MetricExporter; +import io.opentelemetry.sdk.metrics.export.PeriodicMetricReader; +import io.opentelemetry.sdk.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.data.SpanData; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import io.opentelemetry.sdk.trace.export.SpanExporter; +import java.util.Collection; import javax.annotation.Nullable; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.osgi.framework.BundleContext; @@ -22,10 +40,88 @@ public class OpenTelemetryOsgiTest { @InjectBundleContext @Nullable BundleContext bundleContext; + @BeforeEach + void setup() { + // Verify we're in an OSGi environment + assertThat(bundleContext).isNotNull(); + } + @Test public void contextApiAvailable() { - // Verify we're running in OSGi - assertThat(bundleContext).isNotNull(); + OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.create( + new MetricExporter() { + @Override + public CompletableResultCode export(Collection metrics) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public AggregationTemporality getAggregationTemporality( + InstrumentType instrumentType) { + return AggregationTemporality.CUMULATIVE; + } + })) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor( + SimpleLogRecordProcessor.create( + new LogRecordExporter() { + @Override + public CompletableResultCode export(Collection logs) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + })) + .build()) + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor( + SimpleSpanProcessor.create( + new SpanExporter() { + @Override + public CompletableResultCode export(Collection spans) { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode flush() { + return CompletableResultCode.ofSuccess(); + } + + @Override + public CompletableResultCode shutdown() { + return CompletableResultCode.ofSuccess(); + } + })) + .build()) + .build(); + + assertThat(sdk).isNotNull(); // Verify Context API is available Context current = Context.current(); From 0e55acf2eabc5b0ead5e970a2999ee93829ea381 Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 8 Jan 2026 11:02:27 -0600 Subject: [PATCH 03/13] Add osgi optional dependencies for vanilla sdk case --- api/all/build.gradle.kts | 1 + api/incubator/build.gradle.kts | 1 + .../io/opentelemetry/gradle/OtelJavaExtension.kt | 4 ++++ .../main/kotlin/otel.java-conventions.gradle.kts | 10 +++++++++- integration-tests/osgi/build.gradle.kts | 16 +++++++++++----- .../osgi/OpenTelemetryOsgiTest.java | 3 --- sdk-extensions/autoconfigure/build.gradle.kts | 1 + sdk/common/build.gradle.kts | 1 + sdk/logs/build.gradle.kts | 1 + sdk/metrics/build.gradle.kts | 1 + sdk/trace/build.gradle.kts | 1 + 11 files changed, 31 insertions(+), 9 deletions(-) diff --git a/api/all/build.gradle.kts b/api/all/build.gradle.kts index ad6896387d8..e4fb9ce500a 100644 --- a/api/all/build.gradle.kts +++ b/api/all/build.gradle.kts @@ -9,6 +9,7 @@ plugins { description = "OpenTelemetry API" otelJava.moduleName.set("io.opentelemetry.api") base.archivesName.set("opentelemetry-api") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.autoconfigure", "io.opentelemetry.api.incubator")) dependencies { api(project(":context")) diff --git a/api/incubator/build.gradle.kts b/api/incubator/build.gradle.kts index 6bd0669222d..b4e4003299b 100644 --- a/api/incubator/build.gradle.kts +++ b/api/incubator/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry API Incubator" otelJava.moduleName.set("io.opentelemetry.api.incubator") +otelJava.osgiOptionalPackages.set(listOf("com.fasterxml.jackson.databind")) dependencies { api(project(":api:all")) diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt index cf45d5a2725..cf5f6336ef3 100644 --- a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt +++ b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt @@ -6,14 +6,18 @@ package io.opentelemetry.gradle import org.gradle.api.JavaVersion +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property abstract class OtelJavaExtension { abstract val moduleName: Property + abstract val osgiOptionalPackages: ListProperty + abstract val minJavaVersionSupported: Property init { minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) + osgiOptionalPackages.convention(emptyList()) } } diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index f7b5da28bd5..725addb4e64 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -146,10 +146,18 @@ tasks { named("jar") { // Configure OSGi metadata bundle { + // Compute import packages. + // Certain packages like javax.annotation.* are always optional. + // Modules may have additional optional packages, typically corresponding to compileOnly dependencies. + // Append wildcard "*" last to import any other referenced packages + val optionalPackages = mutableListOf("javax.annotation") + optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) + val importPackages = optionalPackages.joinToString(",") { it + ".*;resolution:=optional;version\"\${@}\"" } + ",*" + bnd(mapOf( // Once https://github.com/open-telemetry/opentelemetry-java/issues/6970 is resolved, exclude .internal packages "-exportcontents" to "io.opentelemetry.*", - "Import-Package" to "javax.annotation.*;resolution:=optional;version=\"\${@}\",*", + "Import-Package" to importPackages )) } } diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 3003b99023e..6ab464f5e67 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -27,12 +27,16 @@ configurations.all { } dependencies { + // Testing the "kitchen sink" hides OSGi configuration issues. For example, opentelemetry-api has + // optional dependencies on :sdk-extensions:autoconfigure and :api:incubator. If we only test a + // 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 OTLP exporters + // - with autoconfigure + // - with file configuration testImplementation(project(":sdk:all")) - // TODO: should be able to remove these and everything functions - testImplementation(project(":api:incubator")) - testImplementation(project(":sdk-extensions:autoconfigure")) - testImplementation(project(":sdk-extensions:incubator")) - testImplementation("com.fasterxml.jackson.core:jackson-databind") // For some reason, changing this to testImplementation causes the tests to pass even when failures are present. // Probably some dependency resolution interplay with otel.java-conventions but I couldn't figure it out. @@ -45,6 +49,8 @@ dependencies { testRuntimeOnly("org.apache.felix:org.apache.felix.framework") } + + val testingBundleTask = tasks.register("testingBundle") { archiveClassifier.set("testing") from(sourceSets.test.get().output) diff --git a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java index 057aa31319a..74d42ba84d3 100644 --- a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java +++ b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java @@ -126,8 +126,5 @@ public CompletableResultCode shutdown() { // Verify Context API is available Context current = Context.current(); assertThat(current).isNotNull(); - - // Intentionally fail to verify test execution works - assertThat(1).isEqualTo(2); } } diff --git a/sdk-extensions/autoconfigure/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts index 3d54ea736c4..11cab2fe90c 100644 --- a/sdk-extensions/autoconfigure/build.gradle.kts +++ b/sdk-extensions/autoconfigure/build.gradle.kts @@ -5,6 +5,7 @@ plugins { description = "OpenTelemetry SDK Auto-configuration" otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.extension.incubator")) dependencies { api(project(":sdk:all")) diff --git a/sdk/common/build.gradle.kts b/sdk/common/build.gradle.kts index 8570cbe7148..e695eaf1d83 100644 --- a/sdk/common/build.gradle.kts +++ b/sdk/common/build.gradle.kts @@ -10,6 +10,7 @@ apply() description = "OpenTelemetry SDK Common" otelJava.moduleName.set("io.opentelemetry.sdk.common") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":api:all")) diff --git a/sdk/logs/build.gradle.kts b/sdk/logs/build.gradle.kts index b205b03e90e..6754edc5113 100644 --- a/sdk/logs/build.gradle.kts +++ b/sdk/logs/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry Log SDK" otelJava.moduleName.set("io.opentelemetry.sdk.logs") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":api:all")) diff --git a/sdk/metrics/build.gradle.kts b/sdk/metrics/build.gradle.kts index fdde4938120..90d965e7789 100644 --- a/sdk/metrics/build.gradle.kts +++ b/sdk/metrics/build.gradle.kts @@ -9,6 +9,7 @@ plugins { description = "OpenTelemetry SDK Metrics" otelJava.moduleName.set("io.opentelemetry.sdk.metrics") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":api:all")) diff --git a/sdk/trace/build.gradle.kts b/sdk/trace/build.gradle.kts index 5dac8fbeb81..b548c56cd69 100644 --- a/sdk/trace/build.gradle.kts +++ b/sdk/trace/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry SDK For Tracing" otelJava.moduleName.set("io.opentelemetry.sdk.trace") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) sourceSets { main { From 86622d06806247d172f36e288ed28e6f5cb23d2c Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 8 Jan 2026 11:02:27 -0600 Subject: [PATCH 04/13] Add osgi optional dependencies for vanilla sdk case --- api/all/build.gradle.kts | 1 + api/incubator/build.gradle.kts | 1 + .../io/opentelemetry/gradle/OtelJavaExtension.kt | 4 ++++ .../main/kotlin/otel.java-conventions.gradle.kts | 10 +++++++++- integration-tests/osgi/build.gradle.kts | 16 +++++++++++----- .../osgi/OpenTelemetryOsgiTest.java | 5 +---- sdk-extensions/autoconfigure/build.gradle.kts | 1 + sdk/common/build.gradle.kts | 1 + sdk/logs/build.gradle.kts | 1 + sdk/metrics/build.gradle.kts | 1 + sdk/trace/build.gradle.kts | 1 + 11 files changed, 32 insertions(+), 10 deletions(-) diff --git a/api/all/build.gradle.kts b/api/all/build.gradle.kts index ad6896387d8..e4fb9ce500a 100644 --- a/api/all/build.gradle.kts +++ b/api/all/build.gradle.kts @@ -9,6 +9,7 @@ plugins { description = "OpenTelemetry API" otelJava.moduleName.set("io.opentelemetry.api") base.archivesName.set("opentelemetry-api") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.autoconfigure", "io.opentelemetry.api.incubator")) dependencies { api(project(":context")) diff --git a/api/incubator/build.gradle.kts b/api/incubator/build.gradle.kts index 6bd0669222d..b4e4003299b 100644 --- a/api/incubator/build.gradle.kts +++ b/api/incubator/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry API Incubator" otelJava.moduleName.set("io.opentelemetry.api.incubator") +otelJava.osgiOptionalPackages.set(listOf("com.fasterxml.jackson.databind")) dependencies { api(project(":api:all")) diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt index cf45d5a2725..cf5f6336ef3 100644 --- a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt +++ b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt @@ -6,14 +6,18 @@ package io.opentelemetry.gradle import org.gradle.api.JavaVersion +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property abstract class OtelJavaExtension { abstract val moduleName: Property + abstract val osgiOptionalPackages: ListProperty + abstract val minJavaVersionSupported: Property init { minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) + osgiOptionalPackages.convention(emptyList()) } } diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index f7b5da28bd5..725addb4e64 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -146,10 +146,18 @@ tasks { named("jar") { // Configure OSGi metadata bundle { + // Compute import packages. + // Certain packages like javax.annotation.* are always optional. + // Modules may have additional optional packages, typically corresponding to compileOnly dependencies. + // Append wildcard "*" last to import any other referenced packages + val optionalPackages = mutableListOf("javax.annotation") + optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) + val importPackages = optionalPackages.joinToString(",") { it + ".*;resolution:=optional;version\"\${@}\"" } + ",*" + bnd(mapOf( // Once https://github.com/open-telemetry/opentelemetry-java/issues/6970 is resolved, exclude .internal packages "-exportcontents" to "io.opentelemetry.*", - "Import-Package" to "javax.annotation.*;resolution:=optional;version=\"\${@}\",*", + "Import-Package" to importPackages )) } } diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 3003b99023e..6ab464f5e67 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -27,12 +27,16 @@ configurations.all { } dependencies { + // Testing the "kitchen sink" hides OSGi configuration issues. For example, opentelemetry-api has + // optional dependencies on :sdk-extensions:autoconfigure and :api:incubator. If we only test a + // 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 OTLP exporters + // - with autoconfigure + // - with file configuration testImplementation(project(":sdk:all")) - // TODO: should be able to remove these and everything functions - testImplementation(project(":api:incubator")) - testImplementation(project(":sdk-extensions:autoconfigure")) - testImplementation(project(":sdk-extensions:incubator")) - testImplementation("com.fasterxml.jackson.core:jackson-databind") // For some reason, changing this to testImplementation causes the tests to pass even when failures are present. // Probably some dependency resolution interplay with otel.java-conventions but I couldn't figure it out. @@ -45,6 +49,8 @@ dependencies { testRuntimeOnly("org.apache.felix:org.apache.felix.framework") } + + val testingBundleTask = tasks.register("testingBundle") { archiveClassifier.set("testing") from(sourceSets.test.get().output) diff --git a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java index 057aa31319a..7cb6c388491 100644 --- a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java +++ b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java @@ -47,7 +47,7 @@ void setup() { } @Test - public void contextApiAvailable() { + public void vanillaSdkInitializes() { OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() .setMeterProvider( @@ -126,8 +126,5 @@ public CompletableResultCode shutdown() { // Verify Context API is available Context current = Context.current(); assertThat(current).isNotNull(); - - // Intentionally fail to verify test execution works - assertThat(1).isEqualTo(2); } } diff --git a/sdk-extensions/autoconfigure/build.gradle.kts b/sdk-extensions/autoconfigure/build.gradle.kts index 3d54ea736c4..11cab2fe90c 100644 --- a/sdk-extensions/autoconfigure/build.gradle.kts +++ b/sdk-extensions/autoconfigure/build.gradle.kts @@ -5,6 +5,7 @@ plugins { description = "OpenTelemetry SDK Auto-configuration" otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.sdk.extension.incubator")) dependencies { api(project(":sdk:all")) diff --git a/sdk/common/build.gradle.kts b/sdk/common/build.gradle.kts index 8570cbe7148..e695eaf1d83 100644 --- a/sdk/common/build.gradle.kts +++ b/sdk/common/build.gradle.kts @@ -10,6 +10,7 @@ apply() description = "OpenTelemetry SDK Common" otelJava.moduleName.set("io.opentelemetry.sdk.common") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":api:all")) diff --git a/sdk/logs/build.gradle.kts b/sdk/logs/build.gradle.kts index b205b03e90e..6754edc5113 100644 --- a/sdk/logs/build.gradle.kts +++ b/sdk/logs/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry Log SDK" otelJava.moduleName.set("io.opentelemetry.sdk.logs") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":api:all")) diff --git a/sdk/metrics/build.gradle.kts b/sdk/metrics/build.gradle.kts index fdde4938120..90d965e7789 100644 --- a/sdk/metrics/build.gradle.kts +++ b/sdk/metrics/build.gradle.kts @@ -9,6 +9,7 @@ plugins { description = "OpenTelemetry SDK Metrics" otelJava.moduleName.set("io.opentelemetry.sdk.metrics") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":api:all")) diff --git a/sdk/trace/build.gradle.kts b/sdk/trace/build.gradle.kts index 5dac8fbeb81..b548c56cd69 100644 --- a/sdk/trace/build.gradle.kts +++ b/sdk/trace/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry SDK For Tracing" otelJava.moduleName.set("io.opentelemetry.sdk.trace") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) sourceSets { main { From 5a0f044a79e7056148b97efd648d9617c4591a7b Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 8 Jan 2026 11:37:15 -0600 Subject: [PATCH 05/13] spotless --- integration-tests/osgi/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 6ab464f5e67..c4fe9b291f8 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -49,8 +49,6 @@ dependencies { testRuntimeOnly("org.apache.felix:org.apache.felix.framework") } - - val testingBundleTask = tasks.register("testingBundle") { archiveClassifier.set("testing") from(sourceSets.test.get().output) From 379199067118871966319cbb08c62fdfe65aa3fd Mon Sep 17 00:00:00 2001 From: Jack Berg Date: Thu, 8 Jan 2026 11:39:27 -0600 Subject: [PATCH 06/13] remove extra whitespace --- integration-tests/osgi/build.gradle.kts | 2 -- 1 file changed, 2 deletions(-) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 6ab464f5e67..c4fe9b291f8 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -49,8 +49,6 @@ dependencies { testRuntimeOnly("org.apache.felix:org.apache.felix.framework") } - - val testingBundleTask = tasks.register("testingBundle") { archiveClassifier.set("testing") from(sourceSets.test.get().output) From 164e9dca6d9e61c0d091c7166476c30a4210476b Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 7 Apr 2026 12:34:57 +0000 Subject: [PATCH 07/13] Fix missing = in OSGi version constraint string Signed-off-by: Gregor Zeitlinger --- buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 725addb4e64..5201f483782 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -152,7 +152,7 @@ tasks { // Append wildcard "*" last to import any other referenced packages val optionalPackages = mutableListOf("javax.annotation") optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) - val importPackages = optionalPackages.joinToString(",") { it + ".*;resolution:=optional;version\"\${@}\"" } + ",*" + val importPackages = optionalPackages.joinToString(",") { it + ".*;resolution:=optional;version=\"\${@}\"" } + ",*" bnd(mapOf( // Once https://github.com/open-telemetry/opentelemetry-java/issues/6970 is resolved, exclude .internal packages From d28dc0bd26366510ed00df123f4a643470f1e18a Mon Sep 17 00:00:00 2001 From: Gregor Zeitlinger Date: Tue, 7 Apr 2026 13:56:29 +0000 Subject: [PATCH 08/13] Fix testingBundle classpath so junit-jupiter can be testImplementation Signed-off-by: Gregor Zeitlinger --- integration-tests/osgi/build.gradle.kts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index c4fe9b291f8..8e00d89d863 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -38,9 +38,7 @@ dependencies { // - with file configuration testImplementation(project(":sdk:all")) - // For some reason, changing this to testImplementation causes the tests to pass even when failures are present. - // Probably some dependency resolution interplay with otel.java-conventions but I couldn't figure it out. - implementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter") testCompileOnly("org.osgi:osgi.core") testImplementation("org.osgi:org.osgi.test.junit5") @@ -52,6 +50,10 @@ dependencies { val testingBundleTask = tasks.register("testingBundle") { archiveClassifier.set("testing") from(sourceSets.test.get().output) + // The Bundle task uses compileClasspath by default for BND analysis (e.g. resolving the + // @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(sourceSets.test.get().runtimeClasspath) bundle { bnd( "Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}", From cd0cd3038c449cc1a639849380c8db9096f5feb2 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:06:54 -0500 Subject: [PATCH 09/13] resolveTask is not cacheable --- integration-tests/osgi/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 8995d2562df..ddd5da8ec11 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -70,6 +70,9 @@ val resolveTask = tasks.register("resolve") { bndrun = file("test.bndrun") outputBndrun = layout.buildDirectory.file("test.bndrun") bundles = files(sourceSets.test.get().runtimeClasspath, testingBundleTask.get().archiveFile) + // The generated output embeds an absolute path to the source bndrun, making it unsafe to share + // across machines or worktrees via the build cache. + outputs.cacheIf { false } } val testOSGiTask = tasks.register("testOSGi") { From a4340f7b496b94ff138bc43a35b4dfb4ff15159e Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 15 Apr 2026 12:21:34 -0500 Subject: [PATCH 10/13] testOsgi fails when no tests run --- integration-tests/osgi/build.gradle.kts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index ddd5da8ec11..187383a9332 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -80,6 +80,13 @@ val testOSGiTask = tasks.register("testOSGi") { group = JavaBasePlugin.VERIFICATION_GROUP bndrun = resolveTask.flatMap { it.outputBndrun } bundles = files(sourceSets.test.get().runtimeClasspath, testingBundleTask.get().archiveFile) + // BND reports success when zero tests ran (e.g. if bundles failed to start). Fail explicitly. + val testResultsDir = layout.buildDirectory.dir("test-results/testOSGi") + doLast { + check(testResultsDir.get().asFile.listFiles()?.isNotEmpty() == true) { + "No OSGi test results found — bundles may have failed to start. Check the output above." + } + } } tasks.named(LifecycleBasePlugin.CHECK_TASK_NAME) { From 373985517853c1614337053228e737149865e830 Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:42:23 -0500 Subject: [PATCH 11/13] Cleanup bnd warnings by disabling on test modules --- all/build.gradle.kts | 1 + animal-sniffer-signature/build.gradle.kts | 1 + api/testing-internal/build.gradle.kts | 1 + .../opentelemetry/gradle/OtelJavaExtension.kt | 5 ++ .../kotlin/otel.java-conventions.gradle.kts | 57 ++++++++++--------- .../common/compile-stub/build.gradle.kts | 1 + integration-tests/build.gradle.kts | 1 + .../graal-incubating/build.gradle.kts | 1 + integration-tests/graal/build.gradle.kts | 1 + integration-tests/otlp/build.gradle.kts | 1 + javadoc-crawler/build.gradle.kts | 1 + perf-harness/build.gradle.kts | 1 + sdk/trace-shaded-deps/build.gradle.kts | 1 + testing-internal/build.gradle.kts | 1 + 14 files changed, 47 insertions(+), 27 deletions(-) diff --git a/all/build.gradle.kts b/all/build.gradle.kts index b21a619bab5..00f48fe9a02 100644 --- a/all/build.gradle.kts +++ b/all/build.gradle.kts @@ -4,6 +4,7 @@ plugins { description = "OpenTelemetry All" otelJava.moduleName.set("io.opentelemetry.all") +otelJava.osgiEnabled.set(false) // Skip ossIndexAudit on test module tasks.named("ossIndexAudit") { diff --git a/animal-sniffer-signature/build.gradle.kts b/animal-sniffer-signature/build.gradle.kts index 82f84e0edae..6c192c5246c 100644 --- a/animal-sniffer-signature/build.gradle.kts +++ b/animal-sniffer-signature/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "Build tool to generate the Animal Sniffer Android signature" otelJava.moduleName.set("io.opentelemetry.internal.animalsniffer") +otelJava.osgiEnabled.set(false) val signatureJar = configurations.create("signatureJar") { isCanBeConsumed = false diff --git a/api/testing-internal/build.gradle.kts b/api/testing-internal/build.gradle.kts index 6098501ea03..fb38ce6aa28 100644 --- a/api/testing-internal/build.gradle.kts +++ b/api/testing-internal/build.gradle.kts @@ -4,6 +4,7 @@ plugins { description = "OpenTelemetry API Testing (Internal)" otelJava.moduleName.set("io.opentelemetry.api.testing.internal") +otelJava.osgiEnabled.set(false) dependencies { api(project(":api:all")) diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt index d68fbf6389f..a47f20155c2 100644 --- a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt +++ b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt @@ -12,6 +12,10 @@ import org.gradle.api.provider.Property abstract class OtelJavaExtension { abstract val moduleName: Property + // Set to false for modules that are not OSGi bundles (e.g. test helpers, build tooling, + // aggregators). Skips BND bundle metadata generation entirely. + abstract val osgiEnabled: Property + abstract val osgiOptionalPackages: ListProperty // Packages accessed via Class.forName that are not on the compile classpath (e.g. due to @@ -24,6 +28,7 @@ abstract class OtelJavaExtension { init { minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) + osgiEnabled.convention(true) osgiOptionalPackages.convention(emptyList()) osgiDynamicImportPackages.convention(emptyList()) } diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 23031651784..2bca84d797a 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -133,34 +133,37 @@ tasks { } afterEvaluate { - named("jar") { - // Configure OSGi metadata - bundle { - // Compute import packages. - // Certain packages like javax.annotation.* are always optional. - // Modules may have additional optional packages, typically corresponding to compileOnly dependencies. - // Append wildcard "*" last to import any other referenced packages - val optionalPackages = mutableListOf("javax.annotation") - optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) - val importPackages = optionalPackages.joinToString(",") { "$it.*;resolution:=optional;version=\"\${@}\"" } + ",*" - - // Packages accessed via Class.forName that are not on the compile classpath cannot be made - // optional via Import-Package (BND cannot resolve version="${@}" for them). Instead, exclude - // them from Import-Package and declare them as DynamicImport-Package so OSGi does not - // require them at resolution time but can still wire them at runtime when available. - val dynamicImportPackages = otelJava.osgiDynamicImportPackages.get() - val negations = dynamicImportPackages.joinToString(",") { "!$it" } - val fullImportPackages = if (negations.isNotEmpty()) "$negations,$importPackages" else importPackages - - val bndInstructions = mutableMapOf( - // Once https://github.com/open-telemetry/opentelemetry-java/issues/6970 is resolved, exclude .internal packages - "-exportcontents" to "io.opentelemetry.*", - "Import-Package" to fullImportPackages - ) - if (dynamicImportPackages.isNotEmpty()) { - bndInstructions["DynamicImport-Package"] = dynamicImportPackages.joinToString(",") { "$it,$it.*" } + if (otelJava.osgiEnabled.get()) { + named("jar") { + // Configure OSGi metadata + bundle { + // Compute import packages. + // Certain packages like javax.annotation.* are always optional. + // Modules may have additional optional packages, typically corresponding to compileOnly dependencies. + // Append wildcard "*" last to import any other referenced packages. + val optionalPackages = mutableListOf("javax.annotation") + optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) + val importPackages = optionalPackages.joinToString(",") { "$it.*;resolution:=optional;version=\"\${@}\"" } + ",*" + + // Packages accessed via Class.forName that are not on the compile classpath cannot be made + // optional via Import-Package (BND cannot resolve version="${@}" for them). Instead, exclude + // them from Import-Package and declare them as DynamicImport-Package so OSGi does not + // require them at resolution time but can still wire them at runtime when available. + val dynamicImportPackages = otelJava.osgiDynamicImportPackages.get() + val negations = dynamicImportPackages.joinToString(",") { "!$it" } + val fullImportPackages = if (negations.isNotEmpty()) "$negations,$importPackages" else importPackages + + val bndInstructions = mutableMapOf( + // Exclude shaded internal packages from exports; they are implementation details and + // should not be part of the OSGi bundle's public API surface. + "-exportcontents" to "!io.opentelemetry.internal.shaded.*,io.opentelemetry.*", + "Import-Package" to fullImportPackages + ) + if (dynamicImportPackages.isNotEmpty()) { + bndInstructions["DynamicImport-Package"] = dynamicImportPackages.joinToString(",") { "$it,$it.*" } + } + bnd(bndInstructions) } - bnd(bndInstructions) } } } diff --git a/exporters/common/compile-stub/build.gradle.kts b/exporters/common/compile-stub/build.gradle.kts index f93bd1883c9..5b2a1c921ee 100644 --- a/exporters/common/compile-stub/build.gradle.kts +++ b/exporters/common/compile-stub/build.gradle.kts @@ -4,3 +4,4 @@ plugins { description = "OpenTelemetry Exporter Compile Stub" otelJava.moduleName.set("io.opentelemetry.exporter.internal.compile-stub") +otelJava.osgiEnabled.set(false) diff --git a/integration-tests/build.gradle.kts b/integration-tests/build.gradle.kts index c9dfab1d920..2ddb84f0b44 100644 --- a/integration-tests/build.gradle.kts +++ b/integration-tests/build.gradle.kts @@ -4,6 +4,7 @@ plugins { description = "OpenTelemetry Integration Tests" otelJava.moduleName.set("io.opentelemetry.integration.tests") +otelJava.osgiEnabled.set(false) dependencies { testImplementation(project(":sdk:all")) diff --git a/integration-tests/graal-incubating/build.gradle.kts b/integration-tests/graal-incubating/build.gradle.kts index 8b2153620c4..d097a9b37ab 100644 --- a/integration-tests/graal-incubating/build.gradle.kts +++ b/integration-tests/graal-incubating/build.gradle.kts @@ -7,6 +7,7 @@ plugins { description = "OpenTelemetry Graal Integration Tests (Incubating)" otelJava.moduleName.set("io.opentelemetry.graal.integration.tests.incubating") +otelJava.osgiEnabled.set(false) otelJava.minJavaVersionSupported.set(JavaVersion.VERSION_17) sourceSets { diff --git a/integration-tests/graal/build.gradle.kts b/integration-tests/graal/build.gradle.kts index 99c86443ebf..bfa31e5a26f 100644 --- a/integration-tests/graal/build.gradle.kts +++ b/integration-tests/graal/build.gradle.kts @@ -7,6 +7,7 @@ plugins { description = "OpenTelemetry Graal Integration Tests" otelJava.moduleName.set("io.opentelemetry.graal.integration.tests") +otelJava.osgiEnabled.set(false) otelJava.minJavaVersionSupported.set(JavaVersion.VERSION_17) sourceSets { diff --git a/integration-tests/otlp/build.gradle.kts b/integration-tests/otlp/build.gradle.kts index bdc208a5392..96001fc5a24 100644 --- a/integration-tests/otlp/build.gradle.kts +++ b/integration-tests/otlp/build.gradle.kts @@ -4,6 +4,7 @@ plugins { description = "OTLP Exporter Integration Tests" otelJava.moduleName.set("io.opentelemetry.integration.tests.otlp") +otelJava.osgiEnabled.set(false) dependencies { api("org.testcontainers:testcontainers-junit-jupiter") diff --git a/javadoc-crawler/build.gradle.kts b/javadoc-crawler/build.gradle.kts index e76591abf24..bae241198b3 100644 --- a/javadoc-crawler/build.gradle.kts +++ b/javadoc-crawler/build.gradle.kts @@ -11,6 +11,7 @@ dependencies { description = "OpenTelemetry Javadoc Crawler" otelJava.moduleName.set("io.opentelemetry.javadocs") +otelJava.osgiEnabled.set(false) otelJava.minJavaVersionSupported.set(JavaVersion.VERSION_17) tasks { diff --git a/perf-harness/build.gradle.kts b/perf-harness/build.gradle.kts index c73ec2ad29c..ebb9c5f26de 100644 --- a/perf-harness/build.gradle.kts +++ b/perf-harness/build.gradle.kts @@ -4,6 +4,7 @@ plugins { description = "Performance Testing Harness" otelJava.moduleName.set("io.opentelemetry.perf-harness") +otelJava.osgiEnabled.set(false) dependencies { implementation(project(":api:all")) diff --git a/sdk/trace-shaded-deps/build.gradle.kts b/sdk/trace-shaded-deps/build.gradle.kts index 6e3abea489b..e3a47fb8e6e 100644 --- a/sdk/trace-shaded-deps/build.gradle.kts +++ b/sdk/trace-shaded-deps/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "Internal use only - shaded dependencies of OpenTelemetry SDK for Tracing" otelJava.moduleName.set("io.opentelemetry.sdk.trace.internal") +otelJava.osgiEnabled.set(false) dependencies { implementation("org.jctools:jctools-core") diff --git a/testing-internal/build.gradle.kts b/testing-internal/build.gradle.kts index 6a0cd9521ec..c1dc9ebe96c 100644 --- a/testing-internal/build.gradle.kts +++ b/testing-internal/build.gradle.kts @@ -4,6 +4,7 @@ plugins { description = "OpenTelemetry Testing (Internal)" otelJava.moduleName.set("io.opentelemetry.internal.testing") +otelJava.osgiEnabled.set(false) dependencies { api("org.junit.jupiter:junit-jupiter-api") From 202e349bcc360d51044c86f887cfd3b8185d0c7d Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 15 Apr 2026 13:50:02 -0500 Subject: [PATCH 12/13] DynamicImport-Package only include .* --- buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 2bca84d797a..10e9a72f134 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -160,7 +160,7 @@ tasks { "Import-Package" to fullImportPackages ) if (dynamicImportPackages.isNotEmpty()) { - bndInstructions["DynamicImport-Package"] = dynamicImportPackages.joinToString(",") { "$it,$it.*" } + bndInstructions["DynamicImport-Package"] = dynamicImportPackages.joinToString(",") { "$it.*" } } bnd(bndInstructions) } From e239a0e00d8c3071b033b62bf08f0bf7155ad2ef Mon Sep 17 00:00:00 2001 From: Jack Berg <34418638+jack-berg@users.noreply.github.com> Date: Wed, 15 Apr 2026 14:38:23 -0500 Subject: [PATCH 13/13] Use Import-Package without version instead of DynamicImport-Package --- api/all/build.gradle.kts | 8 +++---- .../opentelemetry/gradle/OtelJavaExtension.kt | 11 ++++----- .../kotlin/otel.java-conventions.gradle.kts | 24 ++++++++----------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/api/all/build.gradle.kts b/api/all/build.gradle.kts index 06348bb11bf..c6dfcbe8da1 100644 --- a/api/all/build.gradle.kts +++ b/api/all/build.gradle.kts @@ -9,10 +9,10 @@ plugins { description = "OpenTelemetry API" otelJava.moduleName.set("io.opentelemetry.api") base.archivesName.set("opentelemetry-api") -// These packages are accessed via Class.forName and cannot be added as compileOnly dependencies -// (api:incubator depends on api:all, creating a circular dependency). Use DynamicImport-Package -// so OSGi does not require them at resolution time but can wire them at runtime when available. -otelJava.osgiDynamicImportPackages.set(listOf("io.opentelemetry.sdk.autoconfigure", "io.opentelemetry.api.incubator", "io.opentelemetry.api.incubator.internal")) +// These packages cannot be compileOnly dependencies (api:incubator depends on api:all, creating a +// circular dependency; sdk:autoconfigure is in a different module family). Declare them as optional +// imports without a version constraint so OSGi wires them if present but does not require them. +otelJava.osgiUnversionedOptionalPackages.set(listOf("io.opentelemetry.sdk.autoconfigure", "io.opentelemetry.api.incubator", "io.opentelemetry.api.incubator.internal")) dependencies { api(project(":context")) diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt index a47f20155c2..025391bbd6c 100644 --- a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt +++ b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt @@ -18,11 +18,10 @@ abstract class OtelJavaExtension { abstract val osgiOptionalPackages: ListProperty - // Packages accessed via Class.forName that are not on the compile classpath (e.g. due to - // circular dependencies). These are excluded from Import-Package and added to - // DynamicImport-Package so OSGi does not require them at resolution time, but can still wire - // them at runtime when available. - abstract val osgiDynamicImportPackages: ListProperty + // Packages that should be optional imports but are not on the compile classpath (e.g. due to + // circular dependencies), so BND cannot resolve version="${@}" for them. Added to Import-Package + // with resolution:=optional but no version constraint. + abstract val osgiUnversionedOptionalPackages: ListProperty abstract val minJavaVersionSupported: Property @@ -30,6 +29,6 @@ abstract class OtelJavaExtension { minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) osgiEnabled.convention(true) osgiOptionalPackages.convention(emptyList()) - osgiDynamicImportPackages.convention(emptyList()) + osgiUnversionedOptionalPackages.convention(emptyList()) } } diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 10e9a72f134..36100dcd887 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -145,24 +145,20 @@ tasks { optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) val importPackages = optionalPackages.joinToString(",") { "$it.*;resolution:=optional;version=\"\${@}\"" } + ",*" - // Packages accessed via Class.forName that are not on the compile classpath cannot be made - // optional via Import-Package (BND cannot resolve version="${@}" for them). Instead, exclude - // them from Import-Package and declare them as DynamicImport-Package so OSGi does not - // require them at resolution time but can still wire them at runtime when available. - val dynamicImportPackages = otelJava.osgiDynamicImportPackages.get() - val negations = dynamicImportPackages.joinToString(",") { "!$it" } - val fullImportPackages = if (negations.isNotEmpty()) "$negations,$importPackages" else importPackages - - val bndInstructions = mutableMapOf( + // Packages not on the compile classpath (e.g. due to circular dependencies) cannot use + // version="${@}" since BND cannot resolve the version. Add them as optional imports without + // a version constraint; they are listed before the wildcard so BND uses our explicit + // instruction rather than auto-detecting them with a version. + val unversionedOptionalPackages = otelJava.osgiUnversionedOptionalPackages.get() + val unversionedImports = unversionedOptionalPackages.joinToString(",") { "$it.*;resolution:=optional" } + val fullImportPackages = if (unversionedImports.isNotEmpty()) "$unversionedImports,$importPackages" else importPackages + + bnd(mapOf( // Exclude shaded internal packages from exports; they are implementation details and // should not be part of the OSGi bundle's public API surface. "-exportcontents" to "!io.opentelemetry.internal.shaded.*,io.opentelemetry.*", "Import-Package" to fullImportPackages - ) - if (dynamicImportPackages.isNotEmpty()) { - bndInstructions["DynamicImport-Package"] = dynamicImportPackages.joinToString(",") { "$it.*" } - } - bnd(bndInstructions) + )) } } }