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/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/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 01112018b76..725addb4e64 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,25 @@ 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 importPackages + )) + } + } + 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..c4fe9b291f8 --- /dev/null +++ b/integration-tests/osgi/build.gradle.kts @@ -0,0 +1,99 @@ +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 { + // 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")) + + // 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..7cb6c388491 --- /dev/null +++ b/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java @@ -0,0 +1,130 @@ +/* + * 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 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; +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; + + @BeforeEach + void setup() { + // Verify we're in an OSGi environment + assertThat(bundleContext).isNotNull(); + } + + @Test + public void vanillaSdkInitializes() { + 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(); + assertThat(current).isNotNull(); + } +} 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/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 { 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")