Skip to content
Merged
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;
}
}
5 changes: 3 additions & 2 deletions exporters/common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ 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"))
otelJava.osgiOptionalPackages.set(listOf("com.fasterxml.jackson.core", "com.google.common.io", "io.opentelemetry.api.incubator.config", "io.opentelemetry.sdk.autoconfigure.spi"))
// 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.
Expand Down Expand Up @@ -59,7 +59,7 @@ if (javaVersion >= JavaVersion.VERSION_1_9) {
val versions: Map<String, String> by project
dependencies {
api(project(":api:all"))
api(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":sdk-extensions:autoconfigure-spi"))

compileOnly(project(":api:incubator"))
compileOnly(project(":sdk:common"))
Expand Down Expand Up @@ -96,6 +96,7 @@ testing {
implementation(project(":exporters:sender:jdk"))
implementation(project(":exporters:sender:okhttp"))
implementation(project(":exporters:sender:grpc-managed-channel"))
implementation(project(":sdk:common"))
}
targets {
all {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.exporter.internal;

import java.net.URI;
import java.net.URISyntaxException;

/**
* Utilities for validating exporter endpoints.
*
* <p>This class is internal and is hence not for public use. Its APIs are unstable and can change
* at any time.
*/
public final class EndpointUtil {

/** Validate an exporter endpoint. */
public static URI validateEndpoint(String endpoint) {
URI uri;
try {
uri = new URI(endpoint);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid endpoint, must be a URL: " + endpoint, e);
}

if (uri.getScheme() == null
|| (!uri.getScheme().equals("http") && !uri.getScheme().equals("https"))) {
throw new IllegalArgumentException(
"Invalid endpoint, must start with http:// or https://: " + uri);
}
return uri;
}

private EndpointUtil() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
import io.opentelemetry.sdk.metrics.export.AggregationTemporalitySelector;
import io.opentelemetry.sdk.metrics.export.DefaultAggregationSelector;
import io.opentelemetry.sdk.metrics.internal.aggregator.AggregationUtil;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Locale;
import java.util.function.Consumer;

Expand All @@ -29,23 +27,6 @@
*/
public final class ExporterBuilderUtil {

/** Validate OTLP endpoint. */
public static URI validateEndpoint(String endpoint) {
URI uri;
try {
uri = new URI(endpoint);
} catch (URISyntaxException e) {
throw new IllegalArgumentException("Invalid endpoint, must be a URL: " + endpoint, e);
}

if (uri.getScheme() == null
|| (!uri.getScheme().equals("http") && !uri.getScheme().equals("https"))) {
throw new IllegalArgumentException(
"Invalid endpoint, must start with http:// or https://: " + uri);
}
return uri;
}

/** Invoke the {@code memoryModeConsumer} with the configured {@link MemoryMode}. */
public static void configureExporterMemoryMode(
ConfigProperties config, Consumer<MemoryMode> memoryModeConsumer) {
Expand Down
10 changes: 9 additions & 1 deletion exporters/logging-otlp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ plugins {

description = "OpenTelemetry Protocol JSON Logging Exporters"
otelJava.moduleName.set("io.opentelemetry.exporter.logging.otlp")
otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator", "io.opentelemetry.sdk.autoconfigure.spi"))
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 All @@ -15,12 +22,13 @@ dependencies {

implementation(project(":exporters:otlp:common"))
compileOnly(project(":api:incubator"))
implementation(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":sdk-extensions:autoconfigure-spi"))

implementation("com.fasterxml.jackson.core:jackson-core")

testImplementation(project(":api:incubator"))
testImplementation(project(":sdk:testing"))
testImplementation(project(":sdk-extensions:autoconfigure-spi"))

testImplementation("com.google.guava:guava")
testImplementation("org.skyscreamer:jsonassert")
Expand Down
9 changes: 8 additions & 1 deletion exporters/logging/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ plugins {

description = "OpenTelemetry - Logging Exporter"
otelJava.moduleName.set("io.opentelemetry.exporter.logging")
otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator", "io.opentelemetry.sdk.autoconfigure.spi"))
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"))

implementation(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":api:incubator"))

testImplementation(project(":sdk:testing"))
Expand Down
11 changes: 9 additions & 2 deletions exporters/otlp/all/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@ apply<io.opentelemetry.gradle.OtelVersionClassPlugin>()

description = "OpenTelemetry Protocol (OTLP) Exporters"
otelJava.moduleName.set("io.opentelemetry.exporter.otlp")
otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator.config"))
otelJava.osgiOptionalPackages.set(listOf("io.opentelemetry.api.incubator.config", "io.opentelemetry.sdk.autoconfigure.spi"))
// 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 All @@ -21,7 +27,7 @@ dependencies {

implementation(project(":exporters:otlp:common"))
implementation(project(":exporters:sender:okhttp"))
implementation(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":sdk-extensions:autoconfigure-spi"))

compileOnly(project(":api:incubator"))

Expand All @@ -30,6 +36,7 @@ dependencies {
compileOnly("io.grpc:grpc-stub")

testImplementation(project(":exporters:otlp:testing-internal"))
testImplementation(project(":sdk-extensions:autoconfigure-spi"))
testImplementation("com.linecorp.armeria:armeria-junit5")
testImplementation("com.linecorp.armeria:armeria-grpc-protocol")
testImplementation("io.grpc:grpc-stub")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.common.ComponentLoader;
import io.opentelemetry.exporter.internal.ExporterBuilderUtil;
import io.opentelemetry.exporter.internal.EndpointUtil;
import io.opentelemetry.exporter.internal.TlsConfigHelper;
import io.opentelemetry.sdk.common.InternalTelemetryVersion;
import io.opentelemetry.sdk.common.export.Compressor;
Expand Down Expand Up @@ -99,7 +99,7 @@ public GrpcExporterBuilder setConnectTimeout(Duration timeout) {
}

public GrpcExporterBuilder setEndpoint(String endpoint) {
this.endpoint = ExporterBuilderUtil.validateEndpoint(endpoint);
this.endpoint = EndpointUtil.validateEndpoint(endpoint);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.metrics.MeterProvider;
import io.opentelemetry.common.ComponentLoader;
import io.opentelemetry.exporter.internal.ExporterBuilderUtil;
import io.opentelemetry.exporter.internal.EndpointUtil;
import io.opentelemetry.exporter.internal.SenderUtil;
import io.opentelemetry.exporter.internal.TlsConfigHelper;
import io.opentelemetry.sdk.common.InternalTelemetryVersion;
Expand Down Expand Up @@ -72,7 +72,7 @@ public final class HttpExporterBuilder {

public HttpExporterBuilder(
StandardComponentId.ExporterType exporterType, String defaultEndpoint) {
this(exporterType, ExporterBuilderUtil.validateEndpoint(defaultEndpoint));
this(exporterType, EndpointUtil.validateEndpoint(defaultEndpoint));
}

HttpExporterBuilder(StandardComponentId.ExporterType exporterType, URI endpoint) {
Expand All @@ -91,7 +91,7 @@ public HttpExporterBuilder setConnectTimeout(Duration duration) {
}

public HttpExporterBuilder setEndpoint(String endpoint) {
this.endpoint = ExporterBuilderUtil.validateEndpoint(endpoint);
this.endpoint = EndpointUtil.validateEndpoint(endpoint);
return this;
}

Expand Down
8 changes: 7 additions & 1 deletion exporters/prometheus/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ plugins {

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

dependencies {
api(project(":sdk:metrics"))

compileOnly(project(":api:incubator"))
implementation(project(":exporters:common"))
implementation(project(":sdk-extensions:autoconfigure-spi"))
compileOnly(project(":sdk-extensions:autoconfigure-spi"))
implementation("io.prometheus:prometheus-metrics-exporter-httpserver") {
exclude(group = "io.prometheus", module = "prometheus-metrics-exposition-formats")
}
Expand All @@ -23,6 +28,7 @@ dependencies {
annotationProcessor("com.google.auto.value:auto-value")

testImplementation(project(":sdk:testing"))
testImplementation(project(":sdk-extensions:autoconfigure-spi"))
testImplementation("io.opentelemetry.proto:opentelemetry-proto")
testImplementation("com.sun.net.httpserver:http")
testImplementation("com.google.guava:guava")
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
Loading
Loading