diff --git a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt index 025391bbd6c..254d1d4dd0f 100644 --- a/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt +++ b/buildSrc/src/main/kotlin/io/opentelemetry/gradle/OtelJavaExtension.kt @@ -23,12 +23,31 @@ abstract class OtelJavaExtension { // with resolution:=optional but no version constraint. abstract val osgiUnversionedOptionalPackages: ListProperty + // SPI interfaces whose implementations this bundle registers via META-INF/services. + // Generates Provide-Capability: osgi.serviceloader;... and requires the registrar extender. + abstract val osgiServiceLoaderProvides: ListProperty + + // SPI interfaces this bundle discovers at runtime via ServiceLoader. + // Generates Require-Capability: osgi.serviceloader;... (resolution:=optional) to hint the + // BND resolver to include provider bundles. Does NOT add the processor extender requirement — + // use osgiServiceLoaderProcessor for that. + abstract val osgiServiceLoaderRequires: ListProperty + + // When true, adds Require-Capability: osgi.extender=osgi.serviceloader.processor so that a + // ServiceLoader mediator (e.g. SPI Fly) weaves this bundle's ServiceLoader.load() calls to + // route through the OSGi service registry. Set this on whichever bundle contains the actual + // ServiceLoader.load() call site; the SPI types being loaded live elsewhere. + abstract val osgiServiceLoaderProcessor: Property + abstract val minJavaVersionSupported: Property init { minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) osgiEnabled.convention(true) - osgiOptionalPackages.convention(emptyList()) - osgiUnversionedOptionalPackages.convention(emptyList()) + osgiOptionalPackages.convention(emptyList()) + osgiUnversionedOptionalPackages.convention(emptyList()) + osgiServiceLoaderProvides.convention(emptyList()) + osgiServiceLoaderRequires.convention(emptyList()) + osgiServiceLoaderProcessor.convention(false) } } diff --git a/buildSrc/src/main/kotlin/otel.japicmp-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.japicmp-conventions.gradle.kts index 48ce7c56d70..6f26cf30c58 100644 --- a/buildSrc/src/main/kotlin/otel.japicmp-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.japicmp-conventions.gradle.kts @@ -168,7 +168,7 @@ if (!project.hasProperty("otel.release") && !project.name.startsWith("bom")) { packageExcludes.addAll( "*.internal", "*.internal.*", - "io.opentelemetry.internal.shaded.jctools.*", + "io.opentelemetry.sdk.trace.internal.shaded.jctools.*", // Temporarily suppress warnings from public generated classes from :sdk-extensions:jaeger-remote-sampler "io.opentelemetry.sdk.extension.trace.jaeger.proto.api_v2" ) diff --git a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts index 408539c5c08..105f2775057 100644 --- a/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts @@ -154,12 +154,43 @@ tasks { 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.*", + val bndInstructions = mutableMapOf( + "-exportcontents" to "io.opentelemetry.*", "Import-Package" to fullImportPackages - )) + ) + + // OSGi ServiceLoader Mediator capabilities. + // Providers declare what SPI implementations they register via META-INF/services. + // Consumers declare what SPI interfaces they discover at runtime via ServiceLoader. + // Both require the corresponding extender from a ServiceLoader mediator (e.g. SPI Fly). + val slProvides = otelJava.osgiServiceLoaderProvides.get() + val slRequires = otelJava.osgiServiceLoaderRequires.get() + val requireClauses = mutableListOf() + + if (slProvides.isNotEmpty()) { + bndInstructions["Provide-Capability"] = slProvides.joinToString(",") { + "osgi.serviceloader;osgi.serviceloader=\"$it\"" + } + requireClauses.add("osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\"") + } + if (slRequires.isNotEmpty()) { + slRequires.forEach { + // resolution:=optional: hints the BND resolver to include provider bundles. + // Does not add the processor extender — use osgiServiceLoaderProcessor for that. + requireClauses.add("osgi.serviceloader;filter:=\"(osgi.serviceloader=$it)\";cardinality:=multiple;resolution:=optional") + } + } + if (otelJava.osgiServiceLoaderProcessor.get()) { + // Mandatory: actively pulls a ServiceLoader processor (e.g. SPI Fly) into the resolved + // bundle set so that ServiceLoader.load() calls are routed via the OSGi service registry. + // Set on whichever bundle contains the ServiceLoader.load() call site. + requireClauses.add("osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.processor)\"") + } + if (requireClauses.isNotEmpty()) { + bndInstructions["Require-Capability"] = requireClauses.joinToString(",") + } + + bnd(bndInstructions) } } } diff --git a/common/build.gradle.kts b/common/build.gradle.kts index ffeef6877fa..1cb81f3d342 100644 --- a/common/build.gradle.kts +++ b/common/build.gradle.kts @@ -7,6 +7,9 @@ plugins { description = "OpenTelemetry API Common" otelJava.moduleName.set("io.opentelemetry.common") +// ServiceLoaderComponentLoader (this bundle) is the ServiceLoader.load() call site for all +// ComponentLoader.forClassLoader() usage; requires the processor extender to weave it. +otelJava.osgiServiceLoaderProcessor.set(true) dependencies { } diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 24d211fea99..e936a84952c 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -89,6 +89,7 @@ val DEPENDENCIES = listOf( "io.opentracing:opentracing-noop:0.33.0", "junit:junit:4.13.2", "nl.jqno.equalsverifier:equalsverifier:3.19.4", + "org.apache.aries.spifly:org.apache.aries.spifly.dynamic.bundle:1.3.7", "org.apache.felix:org.apache.felix.framework:7.0.5", "org.awaitility:awaitility:4.3.0", "org.codehaus.mojo:animal-sniffer-annotations:1.27", diff --git a/exporters/common/build.gradle.kts b/exporters/common/build.gradle.kts index 3f46660d830..d5ac6ff1473 100644 --- a/exporters/common/build.gradle.kts +++ b/exporters/common/build.gradle.kts @@ -7,6 +7,14 @@ plugins { description = "OpenTelemetry Exporter Common" otelJava.moduleName.set("io.opentelemetry.exporter.internal") +otelJava.osgiOptionalPackages.set(listOf("com.fasterxml.jackson.core", "com.google.common.io", "io.opentelemetry.api.incubator.config")) +// sun.misc, io.grpc, and org.jspecify are not OSGi bundles and have no package versioning; must use unversioned optional. +otelJava.osgiUnversionedOptionalPackages.set(listOf("sun.misc", "io.grpc", "org.jspecify.annotations")) +// This bundle's exporters load sender implementations via SPI. +otelJava.osgiServiceLoaderRequires.set(listOf( + "io.opentelemetry.sdk.common.export.GrpcSenderProvider", + "io.opentelemetry.sdk.common.export.HttpSenderProvider" +)) java { sourceSets { diff --git a/exporters/otlp/all/build.gradle.kts b/exporters/otlp/all/build.gradle.kts index 78616d46933..ad8d370d95c 100644 --- a/exporters/otlp/all/build.gradle.kts +++ b/exporters/otlp/all/build.gradle.kts @@ -9,6 +9,9 @@ apply() description = "OpenTelemetry Protocol (OTLP) Exporters" otelJava.moduleName.set("io.opentelemetry.exporter.otlp") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator.config")) +// io.grpc is not an OSGi bundle and has no package versioning; must use unversioned optional. +otelJava.osgiUnversionedOptionalPackages.set(listOf("io.grpc")) base.archivesName.set("opentelemetry-exporter-otlp") dependencies { diff --git a/exporters/otlp/common/build.gradle.kts b/exporters/otlp/common/build.gradle.kts index 9f97c602ca9..c1d9bf4815a 100644 --- a/exporters/otlp/common/build.gradle.kts +++ b/exporters/otlp/common/build.gradle.kts @@ -10,6 +10,7 @@ plugins { description = "OpenTelemetry Protocol Exporter" otelJava.moduleName.set("io.opentelemetry.exporter.internal.otlp") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) val versions: Map by project dependencies { diff --git a/exporters/sender/jdk/build.gradle.kts b/exporters/sender/jdk/build.gradle.kts index 2bff4e6b2cb..d7545fab832 100644 --- a/exporters/sender/jdk/build.gradle.kts +++ b/exporters/sender/jdk/build.gradle.kts @@ -8,6 +8,7 @@ plugins { description = "OpenTelemetry JDK HttpSender" otelJava.moduleName.set("io.opentelemetry.exporter.sender.jdk.internal") otelJava.minJavaVersionSupported.set(JavaVersion.VERSION_11) +otelJava.osgiServiceLoaderProvides.set(listOf("io.opentelemetry.sdk.common.export.HttpSenderProvider")) dependencies { annotationProcessor("com.google.auto.value:auto-value") diff --git a/exporters/sender/okhttp/build.gradle.kts b/exporters/sender/okhttp/build.gradle.kts index a004fea9034..29f1badf131 100644 --- a/exporters/sender/okhttp/build.gradle.kts +++ b/exporters/sender/okhttp/build.gradle.kts @@ -7,6 +7,13 @@ plugins { description = "OpenTelemetry OkHttp Senders" otelJava.moduleName.set("io.opentelemetry.exporter.sender.okhttp.internal") +otelJava.osgiServiceLoaderProvides.set(listOf( + "io.opentelemetry.sdk.common.export.GrpcSenderProvider", + "io.opentelemetry.sdk.common.export.HttpSenderProvider" +)) +// okhttp3, okio, and org.jspecify.annotations are not OSGi bundles; imports must be optional. +// (org.jspecify.annotations is pulled in by OkHttp's Kotlin-compiled types, not this bundle's code.) +otelJava.osgiUnversionedOptionalPackages.set(listOf("okhttp3", "okio", "org.jspecify.annotations")) dependencies { implementation(project(":exporters:common")) diff --git a/integration-tests/osgi/build.gradle.kts b/integration-tests/osgi/build.gradle.kts index 187383a9332..fcfcebbb16e 100644 --- a/integration-tests/osgi/build.gradle.kts +++ b/integration-tests/osgi/build.gradle.kts @@ -26,71 +26,239 @@ 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")) - - testImplementation("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") +// OSGi test infrastructure shared across all suites. +// Each suite's source set automatically inherits these via registerOsgiSuite(). +val osgiInfraImplementation: Configuration = configurations.create("osgiInfraImplementation") { + isCanBeResolved = false + isCanBeConsumed = false +} +val osgiInfraRuntimeOnly: Configuration = configurations.create("osgiInfraRuntimeOnly") { + isCanBeResolved = false + isCanBeConsumed = false } -val testingBundleTask = tasks.register("testingBundle") { - archiveClassifier.set("testing") - from(sourceSets.test.get().output) - bundle { - // 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) - bnd( - "Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}", - "Import-Package: javax.annotation.*;resolution:=optional;version=\"\${@}\",*" - ) - } +dependencies { + osgiInfraImplementation("org.assertj:assertj-core") + osgiInfraImplementation("org.junit.jupiter:junit-jupiter") + osgiInfraImplementation("org.osgi:org.osgi.test.junit5") + osgiInfraImplementation("org.osgi:org.osgi.test.assertj.framework") + osgiInfraRuntimeOnly("org.junit.platform:junit-platform-launcher") + osgiInfraRuntimeOnly("org.apache.felix:org.apache.felix.framework") + osgiInfraRuntimeOnly("org.apache.aries.spifly:org.apache.aries.spifly.dynamic.bundle") } -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) - // 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 } +/** Typed dependency scope for an OSGi test suite, avoiding stringly-typed invoke() calls. */ +class OsgiSuiteDependencies(private val sourceSet: SourceSet, private val handler: DependencyHandler) { + fun implementation(notation: Any) = handler.add(sourceSet.implementationConfigurationName, notation) + fun runtimeOnly(notation: Any) = handler.add(sourceSet.runtimeOnlyConfigurationName, notation) } -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) - // 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." +/** + * Registers tasks for an OSGi test suite (TestingBundle, GenerateBndrun, Resolve, testOSGi) + * with a corresponding source set under src/test/java. + * + * @param extraRunrequires `bnd.identity` entries added to `-runrequires` to force bundles into the + * Felix runtime that the BND resolver won't pull in automatically. + * @param extraRunsystempackages Package prefixes of non-OSGi classpath libraries to expose via + * `-runsystempackages`. Versions are resolved dynamically from the runtime classpath. + * @param minJavaVersion If set, skips the suite when `testJavaVersion` is below this value and + * targets this version on the runtime classpath for dependency variant selection. + */ +fun registerOsgiSuite( + suiteName: String, + extraRunrequires: List = emptyList(), + extraRunsystempackages: List = emptyList(), + minJavaVersion: Int? = null, + configureDependencies: OsgiSuiteDependencies.() -> Unit = {} +): TaskProvider { + val sourceSet = sourceSets.create("test${suiteName.replaceFirstChar { it.uppercase() }}") + OsgiSuiteDependencies(sourceSet, dependencies).configureDependencies() + + // Inherit shared OSGi test infrastructure + configurations[sourceSet.implementationConfigurationName].extendsFrom(osgiInfraImplementation) + configurations[sourceSet.runtimeOnlyConfigurationName].extendsFrom(osgiInfraRuntimeOnly) + // osgi.core is compile-only (provided by the OSGi container at runtime). extendsFrom does not + // propagate to custom source set compile classpaths, so we add it directly. + dependencies.add(sourceSet.compileOnlyConfigurationName, "org.osgi:osgi.core") + + if (minJavaVersion != null) { + configurations[sourceSet.runtimeClasspathConfigurationName].attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, minJavaVersion) + } + } + + val bsn = "opentelemetry-osgi-testing-$suiteName" + + val bundleTask = tasks.register("${suiteName}TestingBundle") { + archiveClassifier.set("testing-$suiteName") + from(sourceSet.output) + bundle { + // 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(sourceSet.runtimeClasspath) + bnd( + "Bundle-SymbolicName: $bsn", + "Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}" + ) } } + + val runee = "JavaSE-${java.toolchain.languageVersion.get()}" + val inputBndrun = layout.buildDirectory.file("bndrun/$suiteName.bndrun") + val generateBndrunTask = tasks.register("${suiteName}GenerateBndrun") { + inputs.property("bsn", bsn) + inputs.property("runee", runee) + inputs.property("extraRunrequires", extraRunrequires) + outputs.file(inputBndrun) + doLast { + val extraEntries = extraRunrequires.joinToString("") { ",\\\n| bnd.identity;id='$it'" } + inputBndrun.get().asFile.apply { parentFile.mkdirs() }.writeText( + """ + |-tester: biz.aQute.tester.junit-platform + |-runfw: org.apache.felix.framework + |-runee: $runee + | + |-runrequires: \ + | bnd.identity;id='$bsn',\ + | bnd.identity;id='junit-jupiter-engine',\ + | bnd.identity;id='junit-platform-launcher'$extraEntries + """.trimMargin() + ) + } + } + + val resolvedBndrun = layout.buildDirectory.file("$suiteName.bndrun") + + val resolveTask = tasks.register("${suiteName}Resolve") { + dependsOn(bundleTask, generateBndrunTask) + description = "Resolve $suiteName OSGi suite" + group = JavaBasePlugin.VERIFICATION_GROUP + bndrun = inputBndrun.get().asFile + outputBndrun = resolvedBndrun + bundles = files(sourceSet.runtimeClasspath, bundleTask.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 } + if (extraRunsystempackages.isNotEmpty()) { + // Compute runpath/runsystempackages content here (at task registration / configuration time) + // so the doLast closure only captures serializable types (String, RegularFileProperty). + // Capturing SourceSet or the build script's `configurations` property in a doLast is not + // supported by Gradle's configuration cache. + // + // BND identifies non-OSGi JARs by Maven group ID (e.g. OkHttp → "com.squareup.okhttp3"). + // Exception: JARs with no manifest (e.g. Kotlin stdlib) are indexed by artifact name + // normalized to dot notation ("kotlin-stdlib" → "kotlin.stdlib") at version 0.0.0. + // Heuristic: JetBrains artifacts lack OSGi manifests; broaden if other deps behave the same. + fun bndCoordinate(a: org.gradle.api.artifacts.ResolvedArtifact) = if (a.moduleVersion.id.group.startsWith("org.jetbrains")) { + "${a.name.replace('-', '.')};version=0.0.0" + } else { + "${a.moduleVersion.id.group};version=${a.moduleVersion.id.version}" + } + + val resolvedArtifacts = project.configurations[sourceSet.runtimeClasspathConfigurationName] + .resolvedConfiguration.resolvedArtifacts + + val runpathCoords = mutableListOf() + val systemPackageSpecs = mutableListOf() + + for (prefix in extraRunsystempackages) { + val normalizedPrefix = prefix.trimEnd { it.isDigit() }.lowercase() + val matches = resolvedArtifacts.filter { it.name.lowercase().startsWith(normalizedPrefix) } + for (artifact in matches) { + // Export the package at the artifact's actual version; the sender bundle's optional + // Import-Package range must include this version (managed in dependencyManagement). + systemPackageSpecs += "$prefix;version=${artifact.moduleVersion.id.version}" + runpathCoords += bndCoordinate(artifact) + // Kotlin-based libraries (e.g. OkHttp) also need Kotlin stdlib on the framework classpath + if (!artifact.moduleVersion.id.group.startsWith("org.jetbrains")) { + resolvedArtifacts.filter { it.name.startsWith("kotlin-stdlib") } + .forEach { runpathCoords += bndCoordinate(it) } + } + } + } + + // Pre-compute the complete string to append; doLast only captures this String value. + val appendContent = buildString { + val distinctRunpath = runpathCoords.distinct() + if (distinctRunpath.isNotEmpty()) append("\n-runpath: ${distinctRunpath.joinToString(", ")}") + append("\n-runsystempackages: ${systemPackageSpecs.distinct().joinToString(", ")}\n") + } + inputs.property("extraRunsystempackages", extraRunsystempackages) + inputs.property("appendContent", appendContent) + + doLast { + resolvedBndrun.get().asFile.appendText(appendContent) + } + } + } + + return tasks.register("testOSGi${suiteName.replaceFirstChar { it.uppercase() }}") { + description = "OSGi Test $suiteName.bndrun" + group = JavaBasePlugin.VERIFICATION_GROUP + bndrun = resolveTask.flatMap { it.outputBndrun } + bundles = files(sourceSet.runtimeClasspath, bundleTask.get().archiveFile) + if (minJavaVersion != null) { + val testJavaVersion: String? by project + enabled = testJavaVersion == null || testJavaVersion!!.toInt() >= minJavaVersion + } + // BND reports success when zero tests ran (e.g. if bundles failed to start). Fail explicitly. + val testResultsDir = layout.buildDirectory.dir("test-results/$name") + doLast { + check(testResultsDir.get().asFile.listFiles()?.isNotEmpty() == true) { + "No OSGi test results found for suite '$suiteName' — bundles may have failed to start. Check the output above." + } + } + } +} + +// 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 autoconfigure +// - with file configuration + +// Suite: sdk — exercises core SDK OSGi metadata in isolation +val sdkSuiteTask = registerOsgiSuite("sdk") { + implementation(project(":sdk:all")) +} + +// OTLP exporter suites. Each suite uses a specific sender implementation and documents exactly +// which system packages (if any) must be exposed for the sender to function in OSGi. + +// Suite: OTLP HTTP via JDK sender — Java 11+, no non-OSGi system packages required. +// The JDK sender uses java.net.http.HttpClient which is part of the OSGi system bundle. +val otlpHttpJdkSuiteTask = registerOsgiSuite( + "otlpHttpJdk", + extraRunrequires = listOf("opentelemetry-exporter-sender-jdk"), + minJavaVersion = 11, +) { + implementation(project(":sdk:all")) + implementation(project(":exporters:otlp:all")) + runtimeOnly(project(":exporters:sender:jdk")) +} + +// okhttp3 and okio are not OSGi bundles; they must be exposed via -runsystempackages. +val otlpHttpOkHttpSuiteTask = registerOsgiSuite( + "otlpHttpOkHttp", + extraRunrequires = listOf("opentelemetry-exporter-sender-okhttp"), + extraRunsystempackages = listOf("okhttp3", "okio"), +) { + implementation(project(":sdk:all")) + implementation(project(":exporters:otlp:all")) } -tasks.named(LifecycleBasePlugin.CHECK_TASK_NAME) { - dependsOn(testOSGiTask) +// okhttp3 and okio are not OSGi bundles; they must be exposed via -runsystempackages. +val otlpGrpcOkHttpSuiteTask = registerOsgiSuite( + "otlpGrpcOkHttp", + extraRunrequires = listOf("opentelemetry-exporter-sender-okhttp"), + extraRunsystempackages = listOf("okhttp3", "okio"), +) { + implementation(project(":sdk:all")) + implementation(project(":exporters:otlp:all")) } tasks { @@ -98,10 +266,10 @@ tasks { 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. + // 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(testOSGiTask) + dependsOn(sdkSuiteTask, otlpHttpJdkSuiteTask, otlpHttpOkHttpSuiteTask, otlpGrpcOkHttpSuiteTask) } } diff --git a/integration-tests/osgi/src/testOtlpGrpcOkHttp/java/io/opentelemetry/integrationtest/osgi/OtlpGrpcOkHttpTest.java b/integration-tests/osgi/src/testOtlpGrpcOkHttp/java/io/opentelemetry/integrationtest/osgi/OtlpGrpcOkHttpTest.java new file mode 100644 index 00000000000..fe5168e3fd6 --- /dev/null +++ b/integration-tests/osgi/src/testOtlpGrpcOkHttp/java/io/opentelemetry/integrationtest/osgi/OtlpGrpcOkHttpTest.java @@ -0,0 +1,57 @@ +/* + * 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.exporter.otlp.logs.OtlpGrpcLogRecordExporter; +import io.opentelemetry.exporter.otlp.metrics.OtlpGrpcMetricExporter; +import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +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.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +/** + * Verifies OTLP gRPC exporters with OkHttp sender work in OSGi. + * + *

See {@code extraRunsystempackages} in {@code build.gradle.kts} for the system packages that + * must be exposed in the OSGi container for this sender to function. + */ +@ExtendWith(BundleContextExtension.class) +public class OtlpGrpcOkHttpTest { + + @Test + void sdkWithOtlpGrpcExportersInitializes() { + try (OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor( + SimpleSpanProcessor.create(OtlpGrpcSpanExporter.builder().build())) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.create(OtlpGrpcMetricExporter.builder().build())) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor( + SimpleLogRecordProcessor.create( + OtlpGrpcLogRecordExporter.builder().build())) + .build()) + .build()) { + assertThat(sdk).isNotNull(); + } + } +} diff --git a/integration-tests/osgi/src/testOtlpHttpJdk/java/io/opentelemetry/integrationtest/osgi/OtlpHttpJdkTest.java b/integration-tests/osgi/src/testOtlpHttpJdk/java/io/opentelemetry/integrationtest/osgi/OtlpHttpJdkTest.java new file mode 100644 index 00000000000..d9c5519aa4a --- /dev/null +++ b/integration-tests/osgi/src/testOtlpHttpJdk/java/io/opentelemetry/integrationtest/osgi/OtlpHttpJdkTest.java @@ -0,0 +1,51 @@ +/* + * 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.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +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.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +/** Verifies OTLP HTTP exporters with Jdk 11+ HttpClient sender work in OSGi. */ +@ExtendWith(BundleContextExtension.class) +public class OtlpHttpJdkTest { + + @Test + void sdkWithOtlpHttpExportersInitializes() { + OtlpHttpSpanExporter spanExporter = OtlpHttpSpanExporter.builder().build(); + OtlpHttpMetricExporter metricExporter = OtlpHttpMetricExporter.builder().build(); + OtlpHttpLogRecordExporter logExporter = OtlpHttpLogRecordExporter.builder().build(); + try (OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor(SimpleSpanProcessor.create(spanExporter)) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader(PeriodicMetricReader.create(metricExporter)) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor(SimpleLogRecordProcessor.create(logExporter)) + .build()) + .build()) { + assertThat(sdk).isNotNull(); + } + } +} diff --git a/integration-tests/osgi/src/testOtlpHttpOkHttp/java/io/opentelemetry/integrationtest/osgi/OtlpHttpOkHttpTest.java b/integration-tests/osgi/src/testOtlpHttpOkHttp/java/io/opentelemetry/integrationtest/osgi/OtlpHttpOkHttpTest.java new file mode 100644 index 00000000000..4e3d4256045 --- /dev/null +++ b/integration-tests/osgi/src/testOtlpHttpOkHttp/java/io/opentelemetry/integrationtest/osgi/OtlpHttpOkHttpTest.java @@ -0,0 +1,57 @@ +/* + * 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.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; +import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter; +import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; +import io.opentelemetry.sdk.OpenTelemetrySdk; +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.trace.SdkTracerProvider; +import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +/** + * Verifies OTLP HTTP exporters with OkHttp sender work in OSGi. + * + *

See {@code extraRunsystempackages} in {@code build.gradle.kts} for the system packages that + * must be exposed in the OSGi container for this sender to function. + */ +@ExtendWith(BundleContextExtension.class) +public class OtlpHttpOkHttpTest { + + @Test + void sdkWithOtlpHttpExportersInitializes() { + try (OpenTelemetrySdk sdk = + OpenTelemetrySdk.builder() + .setTracerProvider( + SdkTracerProvider.builder() + .addSpanProcessor( + SimpleSpanProcessor.create(OtlpHttpSpanExporter.builder().build())) + .build()) + .setMeterProvider( + SdkMeterProvider.builder() + .registerMetricReader( + PeriodicMetricReader.create(OtlpHttpMetricExporter.builder().build())) + .build()) + .setLoggerProvider( + SdkLoggerProvider.builder() + .addLogRecordProcessor( + SimpleLogRecordProcessor.create( + OtlpHttpLogRecordExporter.builder().build())) + .build()) + .build()) { + assertThat(sdk).isNotNull(); + } + } +} diff --git a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java b/integration-tests/osgi/src/testSdk/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java similarity index 87% rename from integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java rename to integration-tests/osgi/src/testSdk/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java index 7cb6c388491..8cde374d0c5 100644 --- a/integration-tests/osgi/src/test/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java +++ b/integration-tests/osgi/src/testSdk/java/io/opentelemetry/integrationtest/osgi/OpenTelemetryOsgiTest.java @@ -8,6 +8,8 @@ import static org.assertj.core.api.Assertions.assertThat; import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.SdkLoggerProvider; @@ -25,30 +27,24 @@ 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 verifyContextStorage() { + ContextKey testContextKey = ContextKey.named("test"); + try (Scope scope = Context.current().with(testContextKey, "testValue").makeCurrent()) { + assertThat(Context.current().get(testContextKey)).isEqualTo("testValue"); + } } @Test public void vanillaSdkInitializes() { - OpenTelemetrySdk sdk = + try (OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() .setMeterProvider( SdkMeterProvider.builder() @@ -119,12 +115,12 @@ public CompletableResultCode shutdown() { } })) .build()) - .build(); - - assertThat(sdk).isNotNull(); + .build()) { + assertThat(sdk).isNotNull(); - // Verify Context API is available - Context current = Context.current(); - assertThat(current).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 deleted file mode 100644 index 8a148d32d39..00000000000 --- a/integration-tests/osgi/test.bndrun +++ /dev/null @@ -1,8 +0,0 @@ --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-spi/build.gradle.kts b/sdk-extensions/autoconfigure-spi/build.gradle.kts index 32985cdf4e9..bd4e3b00758 100644 --- a/sdk-extensions/autoconfigure-spi/build.gradle.kts +++ b/sdk-extensions/autoconfigure-spi/build.gradle.kts @@ -5,6 +5,7 @@ plugins { description = "OpenTelemetry SDK Auto-configuration SPI" otelJava.moduleName.set("io.opentelemetry.sdk.autoconfigure.spi") +otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator")) dependencies { api(project(":sdk:all")) diff --git a/sdk/trace-shaded-deps/build.gradle.kts b/sdk/trace-shaded-deps/build.gradle.kts index e3a47fb8e6e..f20919eb467 100644 --- a/sdk/trace-shaded-deps/build.gradle.kts +++ b/sdk/trace-shaded-deps/build.gradle.kts @@ -19,7 +19,7 @@ tasks { minimize() exclude("META-INF/maven/**") - relocate("org.jctools", "io.opentelemetry.internal.shaded.jctools") + relocate("org.jctools", "io.opentelemetry.sdk.trace.internal.shaded.jctools") } register("extractShadowJar") { diff --git a/sdk/trace-shaded-deps/src/main/resources/META-INF/native-image/io.opentelemetry/opentelemetry-sdk-trace/reflect-config.json b/sdk/trace-shaded-deps/src/main/resources/META-INF/native-image/io.opentelemetry/opentelemetry-sdk-trace/reflect-config.json index 12d6b7a64b8..59812d53bbe 100644 --- a/sdk/trace-shaded-deps/src/main/resources/META-INF/native-image/io.opentelemetry/opentelemetry-sdk-trace/reflect-config.json +++ b/sdk/trace-shaded-deps/src/main/resources/META-INF/native-image/io.opentelemetry/opentelemetry-sdk-trace/reflect-config.json @@ -1,9 +1,9 @@ [ { "condition": { - "typeReachable": "io.opentelemetry.internal.shaded.jctools.queues.MpscArrayQueueConsumerIndexField" + "typeReachable": "io.opentelemetry.sdk.trace.internal.shaded.jctools.queues.MpscArrayQueueConsumerIndexField" }, - "name": "io.opentelemetry.internal.shaded.jctools.queues.MpscArrayQueueConsumerIndexField", + "name": "io.opentelemetry.sdk.trace.internal.shaded.jctools.queues.MpscArrayQueueConsumerIndexField", "fields": [ { "name": "consumerIndex" @@ -12,9 +12,9 @@ }, { "condition": { - "typeReachable": "io.opentelemetry.internal.shaded.jctools.queues.MpscArrayQueueProducerIndexField" + "typeReachable": "io.opentelemetry.sdk.trace.internal.shaded.jctools.queues.MpscArrayQueueProducerIndexField" }, - "name": "io.opentelemetry.internal.shaded.jctools.queues.MpscArrayQueueProducerIndexField", + "name": "io.opentelemetry.sdk.trace.internal.shaded.jctools.queues.MpscArrayQueueProducerIndexField", "fields": [ { "name": "producerIndex" @@ -23,9 +23,9 @@ }, { "condition": { - "typeReachable": "io.opentelemetry.internal.shaded.jctools.queues.MpscArrayQueueProducerLimitField" + "typeReachable": "io.opentelemetry.sdk.trace.internal.shaded.jctools.queues.MpscArrayQueueProducerLimitField" }, - "name": "io.opentelemetry.internal.shaded.jctools.queues.MpscArrayQueueProducerLimitField", + "name": "io.opentelemetry.sdk.trace.internal.shaded.jctools.queues.MpscArrayQueueProducerLimitField", "fields": [ { "name": "producerLimit"