Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.all;

import static org.assertj.core.api.Assertions.assertThat;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import org.junit.jupiter.api.Test;

/**
* Verifies that every OSGi bundle whose {@code META-INF/services/} directory registers SPI
* implementations also declares the corresponding {@code Provide-Capability:
* osgi.serviceloader;osgi.serviceloader="<spi>"} in its manifest.
*/
class OsgiServiceLoaderManifestTest {

@Test
void allOsgiBundlesAdvertiseTheirServiceLoaderRegistrations() throws IOException {
List<String> lines = Files.readAllLines(Path.of(System.getenv("ARTIFACTS_AND_JARS")));
// violations: "<baseName>: META-INF/services/<spi> not in Provide-Capability"
List<String> violations = new ArrayList<>();

for (String line : lines) {
String[] parts = line.split(":", 2);
String baseName = parts[0];
String absolutePath = parts[1];

try (JarFile jar = new JarFile(new File(absolutePath))) {
Manifest manifest = jar.getManifest();
if (manifest == null) {
continue;
}
Attributes mainAttrs = manifest.getMainAttributes();

// Only check OSGi bundles.
String bundleManifestVersion = mainAttrs.getValue("Bundle-ManifestVersion");
if (bundleManifestVersion == null) {
continue;
}

// Collect all SPI interface names from META-INF/services/.
List<String> registeredSpis = new ArrayList<>();
jar.stream()
.map(JarEntry::getName)
.filter(name -> name.startsWith("META-INF/services/") && !name.endsWith("/"))
.forEach(name -> registeredSpis.add(name.substring("META-INF/services/".length())));

if (registeredSpis.isEmpty()) {
continue;
}

// Parse Provide-Capability for osgi.serviceloader entries.
String provideCapability = mainAttrs.getValue("Provide-Capability");
List<String> advertisedSpis = parseOsgiServiceLoaderCapabilities(provideCapability);

for (String spi : registeredSpis) {
if (!advertisedSpis.contains(spi)) {
violations.add(baseName + ": META-INF/services/" + spi + " not in Provide-Capability");
}
}
}
}

assertThat(violations)
.as(
"OSGi bundles with META-INF/services registrations missing from Provide-Capability.\n"
+ "Add the missing SPI to osgiServiceLoaderProvides in the module's build.gradle.kts.")
.isEmpty();
}

/**
* Parses the {@code Provide-Capability} manifest header and returns all {@code
* osgi.serviceloader} service type names.
*
* <p>Example: {@code osgi.serviceloader;osgi.serviceloader="com.example.Foo",
* osgi.serviceloader;osgi.serviceloader="com.example.Bar"} → {@code ["com.example.Foo",
* "com.example.Bar"]}
*/
private static List<String> parseOsgiServiceLoaderCapabilities(String provideCapability) {
List<String> result = new ArrayList<>();
if (provideCapability == null || provideCapability.isEmpty()) {
return result;
}
// JarFile already unfolds line-folded headers. Split into individual capability clauses
// on commas immediately followed by an OSGi namespace (osgi.*).
String[] clauses = provideCapability.split(",(?=\\s*osgi\\.)");
for (String clause : clauses) {
clause = clause.trim();
if (!clause.startsWith("osgi.serviceloader")) {
continue;
}
// Extract osgi.serviceloader="<value>"
int eq = clause.indexOf("osgi.serviceloader=\"");
if (eq < 0) {
continue;
}
int start = eq + "osgi.serviceloader=\"".length();
int end = clause.indexOf('"', start);
if (end > start) {
result.add(clause.substring(start, end));
}
}
return result;
}
}
6 changes: 6 additions & 0 deletions exporters/logging-otlp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ plugins {

description = "OpenTelemetry Protocol JSON Logging Exporters"
otelJava.moduleName.set("io.opentelemetry.exporter.logging.otlp")
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider",
))

dependencies {
implementation(project(":sdk:trace"))
Expand Down
6 changes: 6 additions & 0 deletions exporters/logging/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ plugins {

description = "OpenTelemetry - Logging Exporter"
otelJava.moduleName.set("io.opentelemetry.exporter.logging")
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider",
))

dependencies {
api(project(":sdk:all"))
Expand Down
6 changes: 6 additions & 0 deletions exporters/otlp/all/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ otelJava.moduleName.set("io.opentelemetry.exporter.otlp")
otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator.config"))
// io.grpc and org.jspecify.annotations are not OSGi bundles; must use unversioned optional.
otelJava.osgiUnversionedOptionalPackages.set(listOf("io.grpc", "org.jspecify.annotations"))
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.metrics.ConfigurableMetricExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.logs.ConfigurableLogRecordExporterProvider",
"io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider",
))
base.archivesName.set("opentelemetry-exporter-otlp")

dependencies {
Expand Down
4 changes: 4 additions & 0 deletions exporters/prometheus/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ plugins {

description = "OpenTelemetry Prometheus Exporter"
otelJava.moduleName.set("io.opentelemetry.exporter.prometheus")
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider",
"io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider",
))

dependencies {
api(project(":sdk:metrics"))
Expand Down
3 changes: 3 additions & 0 deletions exporters/sender/grpc-managed-channel/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ plugins {

description = "OpenTelemetry gRPC Upstream Sender"
otelJava.moduleName.set("io.opentelemetry.exporter.sender.grpc.managedchannel.internal")
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.common.export.GrpcSenderProvider",
))

dependencies {
annotationProcessor("com.google.auto.value:auto-value")
Expand Down
3 changes: 3 additions & 0 deletions exporters/zipkin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ plugins {

description = "OpenTelemetry - Zipkin Exporter"
otelJava.moduleName.set("io.opentelemetry.exporter.zipkin")
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSpanExporterProvider",
))

dependencies {
api(project(":sdk:all"))
Expand Down
5 changes: 5 additions & 0 deletions extensions/trace-propagators/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ plugins {

description = "OpenTelemetry Extension : Trace Propagators"
otelJava.moduleName.set("io.opentelemetry.extension.trace.propagation")
otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator"))
otelJava.osgiServiceLoaderProvides.set(listOf(
"io.opentelemetry.sdk.autoconfigure.spi.ConfigurablePropagatorProvider",
"io.opentelemetry.sdk.autoconfigure.spi.internal.ComponentProvider",
))

dependencies {
api(project(":api:all"))
Expand Down
72 changes: 66 additions & 6 deletions integration-tests/osgi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ fun registerOsgiSuite(
suiteName: String,
extraRunrequires: List<String> = emptyList(),
extraRunsystempackages: List<String> = emptyList(),
// SPI types that the testing bundle provides via META-INF/services (noop test implementations).
// Generates Provide-Capability + Require-Capability registrar so SPI Fly picks them up.
serviceLoaderProvides: List<String> = emptyList(),
minJavaVersion: Int? = null,
configureDependencies: OsgiSuiteDependencies.() -> Unit = {}
): TaskProvider<TestOSGi> {
Expand Down Expand Up @@ -84,10 +87,15 @@ fun registerOsgiSuite(
// @Testable annotation to populate Test-Cases). Without this, testImplementation dependencies
// like junit-jupiter are invisible to BND, causing Test-Cases to be empty and 0 tests to run.
classpath(sourceSet.runtimeClasspath)
bnd(
val bndArgs = mutableListOf(
"Bundle-SymbolicName: $bsn",
"Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}"
)
if (serviceLoaderProvides.isNotEmpty()) {
bndArgs.add("Provide-Capability: ${serviceLoaderProvides.joinToString(",") { "osgi.serviceloader;osgi.serviceloader=\"$it\"" }}")
bndArgs.add("Require-Capability: osgi.extender;filter:=\"(osgi.extender=osgi.serviceloader.registrar)\"")
}
bnd(*bndArgs.toTypedArray())
}
}

Expand Down Expand Up @@ -204,10 +212,6 @@ fun registerOsgiSuite(
// bundle which includes those, then mask the fact that OSGi fails when using a bundle without those
// until opentelemetry-api OSGi configuration is updated to indicate that they are optional.

// TODO (jack-berg): Add additional test bundles with dependency combinations reflecting popular use cases:
// - with autoconfigure
// - with file configuration

// Suite: sdk — exercises core SDK OSGi metadata in isolation
val sdkSuiteTask = registerOsgiSuite("sdk") {
implementation(project(":sdk:all"))
Expand Down Expand Up @@ -248,6 +252,55 @@ val otlpGrpcOkHttpSuiteTask = registerOsgiSuite(
implementation(project(":exporters:otlp:all"))
}

// Autoconfigure suites.

// Suite: autoconfigure with OTLP + JDK sender. Exercises the full SPI loading chain across all
// SPI types.
val autoconfigureSuiteTask = registerOsgiSuite(
"autoconfigure",
extraRunrequires = listOf(
"opentelemetry-exporter-sender-jdk",
"opentelemetry-exporter-otlp",
"opentelemetry-extension-trace-propagators",
),
// Some SPIs have implementations in project modules. Others do not. To verify the ones without implementation, we provide noop implementations here.
serviceLoaderProvides = listOf(
"io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider",
"io.opentelemetry.sdk.autoconfigure.spi.traces.ConfigurableSamplerProvider",
"io.opentelemetry.sdk.autoconfigure.spi.internal.ConfigurableMetricReaderProvider",
),
minJavaVersion = 11,
) {
implementation(project(":sdk:all"))
implementation(project(":sdk-extensions:autoconfigure"))
implementation(project(":exporters:otlp:all"))
implementation(project(":extensions:trace-propagators"))
runtimeOnly(project(":exporters:sender:jdk"))
}

// Suite: autoconfigure + declarative-config. Same as above but with declarative-config on the
// classpath, additionally exercising declarative-config bundle OSGi metadata.
val autoconfigureDeclarativeConfigSuiteTask = registerOsgiSuite(
"autoconfigureDeclarativeConfig",
extraRunrequires = listOf(
"opentelemetry-exporter-sender-jdk",
"opentelemetry-exporter-otlp",
"opentelemetry-extension-trace-propagators",
"opentelemetry-sdk-extension-declarative-config",
),
serviceLoaderProvides = listOf(
"io.opentelemetry.sdk.autoconfigure.declarativeconfig.DeclarativeConfigurationCustomizerProvider",
),
minJavaVersion = 11,
) {
implementation(project(":sdk:all"))
implementation(project(":sdk-extensions:autoconfigure"))
implementation(project(":sdk-extensions:declarative-config"))
implementation(project(":exporters:otlp:all"))
implementation(project(":extensions:trace-propagators"))
runtimeOnly(project(":exporters:sender:jdk"))
}

tasks {
jar {
enabled = false
Expand All @@ -256,7 +309,14 @@ tasks {
// We need to replace junit testing with the testOSGi tasks, so we clear test actions and add
// dependencies on all suite tasks. As a result, running :test runs all OSGi suites.
actions.clear()
dependsOn(sdkSuiteTask, otlpHttpJdkSuiteTask, otlpHttpOkHttpSuiteTask, otlpGrpcOkHttpSuiteTask)
dependsOn(
sdkSuiteTask,
otlpHttpJdkSuiteTask,
otlpHttpOkHttpSuiteTask,
otlpGrpcOkHttpSuiteTask,
autoconfigureSuiteTask,
autoconfigureDeclarativeConfigSuiteTask
)
}
}

Expand Down
Loading
Loading