diff --git a/CHANGELOG.md b/CHANGELOG.md index 6803da1068eb..ab017ff5bda9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### 📈 Enhancements + +- Published library, API, and SDK extension artifacts now carry OSGi bundle metadata, so they can be + consumed directly in OSGi runtimes without external wrapper bundles. + ([#18995](https://github.com/open-telemetry/opentelemetry-java-instrumentation/pull/18995)) + ### ⚠️ Breaking changes to non-stable APIs - Changed the return type for `JmxTelemetry.start(...)` APIs. diff --git a/conventions/build.gradle.kts b/conventions/build.gradle.kts index 3e5a88eec965..76cbaa28d410 100644 --- a/conventions/build.gradle.kts +++ b/conventions/build.gradle.kts @@ -72,6 +72,8 @@ dependencies { implementation("org.spdx:spdx-gradle-plugin:0.11.0") // When updating, also update dependencyManagement/build.gradle.kts implementation("net.bytebuddy:byte-buddy-gradle-plugin:1.18.10") + // Generates OSGi bundle metadata for published library artifacts (see otel.osgi-conventions) + implementation("biz.aQute.bnd:biz.aQute.bnd.gradle:7.3.0") implementation("gradle.plugin.io.morethan.jmhreport:gradle-jmh-report:0.9.6") implementation("me.champeau.jmh:jmh-gradle-plugin:0.7.3") implementation("net.ltgt.gradle:gradle-errorprone-plugin:5.1.0") diff --git a/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt b/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt index 100184a111b4..fb82aed45dbb 100644 --- a/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt +++ b/conventions/src/main/kotlin/io/opentelemetry/instrumentation/gradle/OtelJavaExtension.kt @@ -6,6 +6,7 @@ package io.opentelemetry.instrumentation.gradle import org.gradle.api.JavaVersion +import org.gradle.api.provider.ListProperty import org.gradle.api.provider.Property abstract class OtelJavaExtension { @@ -14,7 +15,18 @@ abstract class OtelJavaExtension { abstract val maxJavaVersionForTests: Property + // When false, skips OSGi bundle metadata generation for a module that has otel.osgi-conventions + // applied (e.g. a library that can't be a clean bundle). Has no effect on modules that don't + // apply otel.osgi-conventions. + abstract val osgiEnabled: Property + + // Extra packages added to Import-Package as optional imports (resolution:=optional), typically + // corresponding to compileOnly dependencies that are not present at runtime in an OSGi container. + abstract val osgiOptionalPackages: ListProperty + init { minJavaVersionSupported.convention(JavaVersion.VERSION_1_8) + osgiEnabled.convention(true) + osgiOptionalPackages.convention(emptyList()) } } diff --git a/conventions/src/main/kotlin/otel.library-instrumentation.gradle.kts b/conventions/src/main/kotlin/otel.library-instrumentation.gradle.kts index 92e1cbfc5c96..18d7cfdcf0e4 100644 --- a/conventions/src/main/kotlin/otel.library-instrumentation.gradle.kts +++ b/conventions/src/main/kotlin/otel.library-instrumentation.gradle.kts @@ -3,6 +3,7 @@ plugins { id("otel.jacoco-conventions") id("otel.java-conventions") + id("otel.osgi-conventions") id("otel.publish-conventions") } diff --git a/conventions/src/main/kotlin/otel.osgi-conventions.gradle.kts b/conventions/src/main/kotlin/otel.osgi-conventions.gradle.kts new file mode 100644 index 000000000000..cbb21ef51072 --- /dev/null +++ b/conventions/src/main/kotlin/otel.osgi-conventions.gradle.kts @@ -0,0 +1,41 @@ +import io.opentelemetry.instrumentation.gradle.OtelJavaExtension + +// Generates OSGi bundle metadata in the jar manifest so published library artifacts can be consumed +// directly in OSGi runtimes. Apply only to modules published in the io.opentelemetry.instrumentation +// group (library instrumentations, SDK extensions, and the API/annotation modules) - never to +// shadowed javaagent artifacts. + +plugins { + `java-library` + id("biz.aQute.bnd.builder") +} + +// Configured via tasks.named (not afterEvaluate): the action runs when the jar task is realized, +// after each module's build script has set the otelJava properties below. +tasks.named("jar") { + val otelJava = project.the() + if (otelJava.osgiEnabled.get()) { + bundle { + // javax.annotation.* is always an optional import; modules can add more (typically + // corresponding to compileOnly dependencies). The trailing "*" imports everything else. + val optionalPackages = mutableListOf("javax.annotation") + optionalPackages.addAll(otelJava.osgiOptionalPackages.get()) + val importPackages = + optionalPackages.joinToString(",") { "$it.*;resolution:=optional" } + ",*" + + bnd( + mapOf( + "-exportcontents" to "io.opentelemetry.*", + "Import-Package" to importPackages, + // Auto-generate Provide/Require-Capability headers from META-INF/services entries + // (including AutoService-generated providers such as the resources module's + // ResourceProvider) so a ServiceLoader mediator (e.g. SPI Fly) can bridge them. + "-metainf-services" to "auto", + // reproducible builds (https://github.com/bndtools/bnd/issues/3521) + "-noextraheaders" to "true", + "-snapshot" to "SNAPSHOT", + ), + ) + } + } +} diff --git a/conventions/src/main/kotlin/otel.sdk-extension.gradle.kts b/conventions/src/main/kotlin/otel.sdk-extension.gradle.kts index c1390adcf9ea..f5edbbf099b5 100644 --- a/conventions/src/main/kotlin/otel.sdk-extension.gradle.kts +++ b/conventions/src/main/kotlin/otel.sdk-extension.gradle.kts @@ -6,6 +6,7 @@ plugins { id("otel.jacoco-conventions") id("otel.java-conventions") + id("otel.osgi-conventions") id("otel.publish-conventions") } diff --git a/dependencyManagement/build.gradle.kts b/dependencyManagement/build.gradle.kts index 71d0ac706951..e6f2633dfb40 100644 --- a/dependencyManagement/build.gradle.kts +++ b/dependencyManagement/build.gradle.kts @@ -118,7 +118,17 @@ val DEPENDENCIES = listOf( "javax.validation:validation-api:2.0.1.Final", "org.snakeyaml:snakeyaml-engine:2.10", "org.elasticmq:elasticmq-rest-sqs_2.13:1.7.1", - "io.github.netmikey.logunit:logunit-jul:2.0.0" + "io.github.netmikey.logunit:logunit-jul:2.0.0", + + // OSGi runtime verification (see :osgi-test). Versions track opentelemetry-java's osgi tests. + "org.apache.felix:org.apache.felix.framework:7.0.5", + "org.apache.aries.spifly:org.apache.aries.spifly.dynamic.bundle:1.3.7", + "org.osgi:osgi.core:8.0.0", + "org.osgi:org.osgi.test.junit5:1.3.0", + "org.osgi:org.osgi.test.assertj.framework:1.3.0", + "biz.aQute.bnd:biz.aQute.tester.junit-platform:7.3.0", + "org.junit.jupiter:junit-jupiter:5.14.4", + "org.junit.platform:junit-platform-launcher:1.14.4" ) javaPlatform { diff --git a/instrumentation-annotations-support/build.gradle.kts b/instrumentation-annotations-support/build.gradle.kts index 3f208112a8e5..c49e6ccd6f77 100644 --- a/instrumentation-annotations-support/build.gradle.kts +++ b/instrumentation-annotations-support/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("otel.java-conventions") id("otel.nullaway-conventions") id("otel.jacoco-conventions") + id("otel.osgi-conventions") id("otel.publish-conventions") } diff --git a/instrumentation-annotations/build.gradle.kts b/instrumentation-annotations/build.gradle.kts index 1afa98538858..cc33ab30fe31 100644 --- a/instrumentation-annotations/build.gradle.kts +++ b/instrumentation-annotations/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id("otel.java-conventions") id("otel.nullaway-conventions") + id("otel.osgi-conventions") id("otel.publish-conventions") id("otel.animalsniffer-conventions") diff --git a/instrumentation-api-incubator/build.gradle.kts b/instrumentation-api-incubator/build.gradle.kts index 96d124fdd43a..91f8ae80c86a 100644 --- a/instrumentation-api-incubator/build.gradle.kts +++ b/instrumentation-api-incubator/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("otel.java-conventions") id("otel.animalsniffer-conventions") id("otel.jacoco-conventions") + id("otel.osgi-conventions") id("otel.publish-conventions") id("otel.nullaway-conventions") } diff --git a/instrumentation-api/build.gradle.kts b/instrumentation-api/build.gradle.kts index 909c5313ac74..f434acfdd8d9 100644 --- a/instrumentation-api/build.gradle.kts +++ b/instrumentation-api/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("otel.java-conventions") id("otel.animalsniffer-conventions") id("otel.jacoco-conventions") + id("otel.osgi-conventions") id("otel.publish-conventions") id("otel.jmh-conventions") id("otel.nullaway-conventions") diff --git a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts index 5b1dd1b306ec..1aea3714fc3e 100644 --- a/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts +++ b/instrumentation/logback/logback-appender-1.0/library/build.gradle.kts @@ -3,6 +3,12 @@ plugins { id("org.graalvm.buildtools.native") } +otelJava { + // Logstash support is optional; the encoder is a compileOnly dependency and may be absent at + // runtime in an OSGi container. + osgiOptionalPackages.add("net.logstash.logback") +} + dependencies { compileOnly(project(":muzzle")) diff --git a/osgi-test/build.gradle.kts b/osgi-test/build.gradle.kts new file mode 100644 index 000000000000..be433a4ebabc --- /dev/null +++ b/osgi-test/build.gradle.kts @@ -0,0 +1,293 @@ +import aQute.bnd.gradle.Bundle +import aQute.bnd.gradle.Resolve +import aQute.bnd.gradle.TestOSGi +import org.gradle.api.artifacts.ExternalModuleDependency +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import java.util.jar.JarFile + +plugins { + id("otel.java-conventions") +} + +description = "OpenTelemetry Instrumentation OSGi Integration Tests" + +// Verifies that the OSGi bundle metadata generated by otel.osgi-conventions actually resolves and +// runs in a real OSGi container (Apache Felix), with ServiceLoader mediation provided by Aries +// SPI Fly. Mirrors opentelemetry-java's 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 + +// OSGi test infrastructure shared across all suites. Each suite's source set inherits these via +// registerOsgiSuite(). +val osgiInfraImplementation: Configuration = configurations.create("osgiInfraImplementation") { + isCanBeResolved = false + isCanBeConsumed = false +} +val osgiInfraRuntimeOnly: Configuration = configurations.create("osgiInfraRuntimeOnly") { + isCanBeResolved = false + isCanBeConsumed = false +} + +dependencies { + // Tests use JUnit's built-in assertions rather than assertj: assertj imports net.bytebuddy + // mandatorily, and byte-buddy 1.18.10's multi-release jar advertises a JavaSE-24 execution + // environment requirement that the JavaSE-21 OSGi runtime can't satisfy. + osgiInfraImplementation("org.junit.jupiter:junit-jupiter") + osgiInfraImplementation("org.osgi:org.osgi.test.junit5") + 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") +} + +/** 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 compileOnly(notation: Any) = handler.add(sourceSet.compileOnlyConfigurationName, notation) + fun runtimeOnly(notation: Any) = handler.add(sourceSet.runtimeOnlyConfigurationName, notation) + + fun implementation(notation: String, configure: ExternalModuleDependency.() -> Unit) { + (handler.add(sourceSet.implementationConfigurationName, notation) as ExternalModuleDependency).configure() + } +} + +/** + * 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 serviceLoaderProvides SPI types that the testing bundle provides via META-INF/services + * (noop test implementations). Generates Provide-Capability + Require-Capability registrar so + * SPI Fly picks them up. + * @param minJavaVersion If set, skips the suite when `testJavaVersion` is below this value. + * @param resolveOnly When true, the suite stops at BND resolution (proving the bundle's OSGi wiring + * is satisfiable) and does not boot the container. Use when the instrumented library's own OSGi + * bundles have runtime packaging quirks that are provided/handled by the target platform. + */ +fun registerOsgiSuite( + suiteName: String, + extraRunrequires: List = emptyList(), + extraRunsystempackages: List = emptyList(), + serviceLoaderProvides: List = emptyList(), + minJavaVersion: Int? = null, + resolveOnly: Boolean = false, + 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) + val bndArgs = mutableListOf( + "Bundle-SymbolicName: $bsn", + "Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}", + ) + if (serviceLoaderProvides.isNotEmpty()) { + bndArgs.add("Provide-Capability: ${serviceLoaderProvides.joinToString(",") { "osgi.serviceloader;osgi.serviceloader=\"$it\"" }}") + bndArgs.add("Require-Capability: osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\"") + } + bnd(*bndArgs.toTypedArray()) + } + } + + val runee = "JavaSE-${java.toolchain.languageVersion.get()}" + + // opentelemetry-instrumentation-api imports io.opentelemetry.semconv, but the upstream semconv + // jar is not an OSGi bundle, so expose it as a system package for every suite. + val effectiveRunsystempackages = extraRunsystempackages + "io.opentelemetry.semconv|opentelemetry-semconv" + + // Build the "-runpath" / "-runsystempackages" bndrun lines that expose non-OSGi jars (e.g. the + // upstream semconv jar) to the OSGi runtime. Resolved lazily at execution time (configuration-cache + // safe; the lambda captures only effectiveRunsystempackages). These must be in the bndrun *before* + // resolution so the resolver can satisfy mandatory imports of non-OSGi jars (e.g. instrumentation-api's + // mandatory import of io.opentelemetry.semconv). Entries use "packagePrefix|artifactNamePrefix" when + // the package and artifact names differ. + val systemPackagesContent = configurations.named(sourceSet.runtimeClasspathConfigurationName) + .flatMap { it.incoming.artifacts.resolvedArtifacts } + .map { artifacts -> + val entries = effectiveRunsystempackages + val byModule = artifacts.mapNotNull { art -> + (art.id.componentIdentifier as? ModuleComponentIdentifier)?.let { it to art.file } + } + val runpath = mutableListOf() + val systemPackages = mutableListOf() + for (entry in entries) { + val parts = entry.split("|") + val packagePrefix = parts[0] + val artifactPrefix = (if (parts.size > 1) parts[1] else packagePrefix).trimEnd { c -> c.isDigit() }.lowercase() + for ((id, file) in byModule.filter { it.first.module.lowercase().startsWith(artifactPrefix) }) { + val mainAttributes = JarFile(file).use { it.manifest?.mainAttributes } + val bsn = mainAttributes?.getValue("Bundle-SymbolicName")?.substringBefore(';')?.trim() + // The system bundle advertises the package at the jar's real (Maven) version. + systemPackages += "$packagePrefix;version=${id.version}" + // bnd indexes a real OSGi bundle by its Bundle-SymbolicName/Bundle-Version, but a plain jar + // (no manifest headers) by its Maven group id at version 0.0.0. + runpath += if (bsn != null) { + "$bsn;version=${mainAttributes.getValue("Bundle-Version") ?: id.version}" + } else { + "${id.group};version=0.0.0" + } + } + } + buildString { + val distinctRunpath = runpath.distinct() + if (distinctRunpath.isNotEmpty()) append("\n-runpath: ${distinctRunpath.joinToString(", ")}") + if (systemPackages.isNotEmpty()) append("\n-runsystempackages: ${systemPackages.distinct().joinToString(", ")}") + } + } + + 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) + inputs.property("systemPackagesContent", systemPackagesContent) + 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() + systemPackagesContent.get() + "\n", + ) + } + } + + 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 (resolveOnly) { + return resolveTask + } + + 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." + } + } + } +} + +// Suite: api - core instrumentation API + annotations bundles in isolation. +val apiSuiteTask = registerOsgiSuite("api") { + implementation(project(":instrumentation-api")) + implementation(project(":instrumentation-api-incubator")) + implementation(project(":instrumentation-annotations")) +} + +// Suite: apacheHttpClient - a representative library instrumentation bundle, exercised against the +// stock Apache HttpComponents OSGi bundles - the exact same bundles AEM/Sling provide +// (org.apache.httpcomponents.httpclient/httpcore), with commons-logging and Config Admin. +val apacheHttpClientSuiteTask = registerOsgiSuite("apacheHttpClient") { + implementation(project(":instrumentation:apache-httpclient:apache-httpclient-4.3:library")) + // httpclient-osgi/httpcore-osgi are the real OSGi bundles (BSN org.apache.httpcomponents.*) - the + // same ones AEM ships. Pull them non-transitively: their POMs also drag in the plain httpclient/ + // httpcore jars, which are NOT bundles and would shadow the OSGi bundles in the resolver. + implementation("org.apache.httpcomponents:httpclient-osgi:4.5.14") { isTransitive = false } + implementation("org.apache.httpcomponents:httpcore-osgi:4.4.16") { isTransitive = false } + // httpclient imports commons-logging in [1.1,1.3); pin to 1.2 (an OSGi bundle) since + // dependencyManagement otherwise forces 1.3.6, which is outside that range. In AEM this package + // is provided by jcl-over-slf4j. + implementation("commons-logging:commons-logging") { + version { strictly("1.2") } + } + runtimeOnly("org.apache.felix:org.apache.felix.configadmin:1.9.26") +} + +// Suite: logbackAppender - a representative library instrumentation bundle, resolving against +// instrumentation-api and the instrumented library. logback-classic is itself an OSGi bundle. +// Also verifies the bundle resolves WITHOUT Logstash present, proving the net.logstash.logback +// optional-import tuning. +val logbackAppenderSuiteTask = registerOsgiSuite("logbackAppender") { + implementation(project(":instrumentation:logback:logback-appender-1.0:library")) + implementation("ch.qos.logback:logback-classic") +} + +// Suite: resources - the key SPI test. Asserts the AutoService-generated ResourceProvider services +// are mediated by SPI Fly, i.e. the -metainf-services: auto capabilities resolve in OSGi. +val resourcesSuiteTask = registerOsgiSuite( + "resources", + // Nothing imports the resources bundle's packages (the test inspects the OSGi service registry), + // so force it into the runtime. BSN = archivesName from otel.java-conventions. + extraRunrequires = listOf("opentelemetry-resources"), +) { + implementation(project(":instrumentation:resources:library")) + implementation("io.opentelemetry:opentelemetry-sdk") + implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") +} + +tasks { + jar { + enabled = false + } + test { + // Replace junit testing with the testOSGi tasks: clear test actions and depend on all suites, + // so running :test runs every OSGi suite. + actions.clear() + dependsOn( + apiSuiteTask, + apacheHttpClientSuiteTask, + logbackAppenderSuiteTask, + resourcesSuiteTask, + ) + } +} + +// Skip ossIndexAudit on test module. +tasks.named("ossIndexAudit") { + enabled = false +} diff --git a/osgi-test/src/testApacheHttpClient/java/io/opentelemetry/instrumentation/osgi/ApacheHttpClientOsgiTest.java b/osgi-test/src/testApacheHttpClient/java/io/opentelemetry/instrumentation/osgi/ApacheHttpClientOsgiTest.java new file mode 100644 index 000000000000..80b9b3218b0e --- /dev/null +++ b/osgi-test/src/testApacheHttpClient/java/io/opentelemetry/instrumentation/osgi/ApacheHttpClientOsgiTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.osgi; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.instrumentation.apachehttpclient.v4_3.ApacheHttpClientTelemetry; +import org.apache.http.impl.client.CloseableHttpClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +@ExtendWith(BundleContextExtension.class) +class ApacheHttpClientOsgiTest { + + @Test + void telemetryWrapsClientInOsgi() { + ApacheHttpClientTelemetry telemetry = ApacheHttpClientTelemetry.create(OpenTelemetry.noop()); + CloseableHttpClient client = telemetry.createHttpClient(); + assertNotNull(client); + } +} diff --git a/osgi-test/src/testApi/java/io/opentelemetry/instrumentation/osgi/ApiOsgiTest.java b/osgi-test/src/testApi/java/io/opentelemetry/instrumentation/osgi/ApiOsgiTest.java new file mode 100644 index 000000000000..d781a44c9632 --- /dev/null +++ b/osgi-test/src/testApi/java/io/opentelemetry/instrumentation/osgi/ApiOsgiTest.java @@ -0,0 +1,62 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.osgi; + +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.context.Context; +import io.opentelemetry.context.ContextKey; +import io.opentelemetry.context.Scope; +import io.opentelemetry.context.propagation.TextMapGetter; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.opentelemetry.instrumentation.api.instrumenter.Instrumenter; +import io.opentelemetry.instrumentation.api.instrumenter.SpanNameExtractor; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +@ExtendWith(BundleContextExtension.class) +class ApiOsgiTest { + + @Test + void contextStorageWorksInOsgi() { + ContextKey key = ContextKey.named("test"); + try (Scope ignored = Context.current().with(key, "value").makeCurrent()) { + assertEquals("value", Context.current().get(key)); + } + } + + @Test + void instrumenterBuilds() { + SpanNameExtractor nameExtractor = request -> request; + Instrumenter instrumenter = + Instrumenter.builder(OpenTelemetry.noop(), "test", nameExtractor) + .buildServerInstrumenter(NoopGetter.INSTANCE); + assertNotNull(instrumenter); + } + + @Test + void withSpanAnnotationIsAvailable() { + assertEquals("io.opentelemetry.instrumentation.annotations.WithSpan", WithSpan.class.getName()); + } + + private enum NoopGetter implements TextMapGetter { + INSTANCE; + + @Override + public Iterable keys(String carrier) { + return emptyList(); + } + + @Override + public String get(String carrier, String key) { + return null; + } + } +} diff --git a/osgi-test/src/testLogbackAppender/java/io/opentelemetry/instrumentation/osgi/LogbackAppenderOsgiTest.java b/osgi-test/src/testLogbackAppender/java/io/opentelemetry/instrumentation/osgi/LogbackAppenderOsgiTest.java new file mode 100644 index 000000000000..706c7979fab6 --- /dev/null +++ b/osgi-test/src/testLogbackAppender/java/io/opentelemetry/instrumentation/osgi/LogbackAppenderOsgiTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.osgi; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.test.junit5.context.BundleContextExtension; + +@ExtendWith(BundleContextExtension.class) +class LogbackAppenderOsgiTest { + + // Instantiating the appender exercises its class hierarchy and (transitively) confirms that the + // bundle resolved without the optional net.logstash.logback packages present at runtime. + @Test + void appenderInstantiatesWithoutLogstash() { + Appender appender = new OpenTelemetryAppender(); + assertInstanceOf(OpenTelemetryAppender.class, appender); + // logstash-logback-encoder must not be present at runtime for this suite. Check a concrete + // class resource, not the package directory (a jar need not contain directory entries). + assertNull( + getClass().getClassLoader().getResource("net/logstash/logback/marker/Markers.class")); + } +} diff --git a/osgi-test/src/testResources/java/io/opentelemetry/instrumentation/osgi/ResourcesOsgiTest.java b/osgi-test/src/testResources/java/io/opentelemetry/instrumentation/osgi/ResourcesOsgiTest.java new file mode 100644 index 000000000000..55a66103c5c4 --- /dev/null +++ b/osgi-test/src/testResources/java/io/opentelemetry/instrumentation/osgi/ResourcesOsgiTest.java @@ -0,0 +1,34 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.osgi; + +import static org.junit.jupiter.api.Assertions.assertFalse; + +import io.opentelemetry.sdk.autoconfigure.spi.ResourceProvider; +import java.util.Collection; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; +import org.osgi.test.common.annotation.InjectBundleContext; +import org.osgi.test.junit5.context.BundleContextExtension; + +@ExtendWith(BundleContextExtension.class) +class ResourcesOsgiTest { + + @InjectBundleContext BundleContext bundleContext; + + // The resources module declares ResourceProvider implementations via AutoService-generated + // META-INF/services entries. bnd's "-metainf-services: auto" turns those into osgi.serviceloader + // Provide-Capability headers, which the Aries SPI Fly registrar bundle reads to publish the + // providers as OSGi services. Finding them in the service registry proves the chain works. + @Test + void resourceProvidersAreRegisteredViaSpiFly() throws Exception { + Collection> refs = + bundleContext.getServiceReferences(ResourceProvider.class, null); + assertFalse(refs.isEmpty()); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 0d7ed83f2336..7f8d610070c7 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -148,6 +148,7 @@ include(":instrumentation-annotations-support-testing") // misc include(":dependencyManagement") include(":instrumentation-docs") +include(":osgi-test") include(":testing:agent-exporter") include(":testing:agent-for-testing") include(":testing:dependencies-shaded-for-testing")