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
1 change: 1 addition & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ repositories {
}

dependencies {
implementation("biz.aQute.bnd:biz.aQute.bnd.gradle:7.3.0")
implementation("com.diffplug.spotless:spotless-plugin-gradle:8.8.0")
implementation("ru.vyarus:gradle-animalsniffer-plugin:2.0.1")
implementation("me.champeau.gradle:japicmp-gradle-plugin:0.4.6")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@

package io.opentelemetry.gradle

import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property

abstract class OtelJavaExtension {
abstract val moduleName: Property<String>

/** Whether to generate OSGi bundle metadata. Enabled by default. */
abstract val osgiEnabled: Property<Boolean>

/** Extra packages imported as optional (resolution:=optional), e.g. compileOnly deps. */
abstract val osgiOptionalPackages: ListProperty<String>
}
34 changes: 34 additions & 0 deletions buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ plugins {
eclipse
idea

id("biz.aQute.bnd.builder")
id("otel.spotless-conventions")
}

val otelJava = extensions.create<OtelJavaExtension>("otelJava")
otelJava.osgiEnabled.convention(true)
otelJava.osgiOptionalPackages.convention(emptyList())

java {
toolchain {
Expand All @@ -29,6 +32,13 @@ checkstyle {
configProperties["rootDir"] = rootDir
}

// normalize timestamps and file ordering in jars, making the outputs (including OSGi
// manifests) reproducible. see open-telemetry/opentelemetry-java#4488
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
isReproducibleFileOrder = true
}

val testJavaVersion = gradle.startParameter.projectProperties.get("testJavaVersion")?.let(JavaVersion::toVersion)

tasks {
Expand Down Expand Up @@ -85,6 +95,30 @@ tasks {
}
}

afterEvaluate {
if (otelJava.osgiEnabled.get()) {
named<Jar>("jar") {
// Configure OSGi metadata. semconv has no SPI / ServiceLoader needs, so this is the
// trimmed form of opentelemetry-java's otel.java-conventions OSGi config.
bundle {
// Modules may declare optional imports (typically compileOnly deps). The trailing
// "*" lets BND auto-import everything else (e.g. io.opentelemetry.api.*).
val optionalPackages = otelJava.osgiOptionalPackages.get()
val optionalImports =
optionalPackages.joinToString(",") { "$it.*;resolution:=optional;version=\"\${@}\"" }
val importPackages = if (optionalImports.isEmpty()) "*" else "$optionalImports,*"

bnd(
mapOf(
"-exportcontents" to "io.opentelemetry.*",
"Import-Package" to importPackages,
),
)
}
}
}
}

withType<Jar>().configureEach {
inputs.property("moduleName", otelJava.moduleName)

Expand Down
125 changes: 125 additions & 0 deletions osgi-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import aQute.bnd.gradle.Bundle
import aQute.bnd.gradle.Resolve
import aQute.bnd.gradle.TestOSGi

plugins {
id("otel.java-conventions")
}

description = "OpenTelemetry Semantic Conventions OSGi Integration Tests"
otelJava.moduleName.set("io.opentelemetry.semconv.integration.tests.osgi")
// This module runs OSGi tests against the published semconv bundles; it does not publish an OSGi
// bundle itself.
otelJava.osgiEnabled.set(false)

// The semconv bundles Import-Package io.opentelemetry.api.common, so resolution needs an
// opentelemetry-api that is itself a proper OSGi bundle. OSGi metadata was added to the core
// artifacts in opentelemetry-java#8417, well after the 1.33.0 compileOnly baseline pinned in
// :dependencyManagement, so we force a recent version on this test module's classpath only.
val osgiOtelApiVersion = "1.63.0"

configurations.configureEach {
resolutionStrategy {
force("io.opentelemetry:opentelemetry-api:$osgiOtelApiVersion")
}
}

dependencies {
testImplementation(project(":semconv"))
testImplementation(project(":semconv-incubating"))
// Provides the io.opentelemetry.api.* packages the semconv bundles import, as an OSGi bundle.
testImplementation("io.opentelemetry:opentelemetry-api:$osgiOtelApiVersion")

testImplementation("org.osgi:org.osgi.test.junit5:1.3.0")
// Provided by the OSGi framework at runtime.
testCompileOnly("org.osgi:osgi.core:8.0.0")

testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("org.apache.felix:org.apache.felix.framework:7.0.5")
// opentelemetry-common (pulled in transitively by opentelemetry-api) declares
// Require-Capability: osgi.extender=osgi.serviceloader.processor, so the container needs Aries
// SPI Fly to resolve even though semconv itself has no ServiceLoader providers.
testRuntimeOnly("org.apache.aries.spifly:org.apache.aries.spifly.dynamic.bundle:1.3.7")
}

// The testing bundle (our JUnit tests + Test-Cases header) is booted inside a real Felix container
// via bnd's Bundle -> Resolve -> TestOSGi task chain, modeled on opentelemetry-java's
// integration-tests/osgi module.
val bsn = "opentelemetry-semconv-osgi-testing"
val runee = "JavaSE-${java.toolchain.languageVersion.get()}"
val testRuntimeClasspath = sourceSets.test.get().runtimeClasspath

val testingBundle = tasks.register<Bundle>("testingBundle") {
archiveClassifier.set("testing")
from(sourceSets.test.get().output)
bundle {
// BND analyses compileClasspath by default; use the runtime classpath so testImplementation
// deps (junit-jupiter, assertj) are visible and Test-Cases gets populated.
classpath(testRuntimeClasspath)
bnd(
"Bundle-SymbolicName: $bsn",
"Test-Cases: \${classes;HIERARCHY_INDIRECTLY_ANNOTATED;org.junit.platform.commons.annotation.Testable;CONCRETE}",
)
}
}

val inputBndrun = layout.buildDirectory.file("bndrun/test.bndrun")
val generateBndrun = tasks.register("generateBndrun") {
// Local copies so the doLast closure captures only serializable values (String, Provider), not
// the enclosing Kotlin build-script object (required by the configuration cache).
val bndrunFile = inputBndrun
val bndrunContent =
"""
|-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'
""".trimMargin()
inputs.property("content", bndrunContent)
outputs.file(bndrunFile)
doLast {
bndrunFile.get().asFile.apply { parentFile.mkdirs() }.writeText(bndrunContent)
}
}

val resolvedBndrun = layout.buildDirectory.file("test.bndrun")
val resolve = tasks.register<Resolve>("resolve") {
dependsOn(testingBundle, generateBndrun)
description = "Resolve the semconv OSGi test suite"
group = JavaBasePlugin.VERIFICATION_GROUP
bndrun = inputBndrun.get().asFile
outputBndrun = resolvedBndrun
bundles = files(testRuntimeClasspath, testingBundle.get().archiveFile)
// The resolved bndrun embeds an absolute path to the input, so it is not safe to share via cache.
outputs.cacheIf { false }
}

tasks.register<TestOSGi>("testOsgi") {
dependsOn(resolve)
description = "Run the semconv OSGi test suite inside an Apache Felix container"
group = JavaBasePlugin.VERIFICATION_GROUP
bndrun = resolvedBndrun
bundles = files(testRuntimeClasspath, testingBundle.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/$name")
doLast {
check(testResultsDir.get().asFile.listFiles()?.isNotEmpty() == true) {
"No OSGi test results found — bundles may have failed to start. Check the output above."
}
}
}

tasks {
named<Jar>("jar") {
enabled = false
}
named<Test>("test") {
// Replace plain JUnit execution with the in-container OSGi test.
actions.clear()
dependsOn("testOsgi")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.semconv.integrationtest.osgi;

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

import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.semconv.SchemaUrls;
import io.opentelemetry.semconv.ServiceAttributes;
import io.opentelemetry.semconv.incubating.ServiceIncubatingAttributes;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.osgi.test.junit5.context.BundleContextExtension;

/**
* Boots the semconv and semconv-incubating bundles inside a real OSGi (Apache Felix) container and
* exercises their exported packages. If either bundle's generated OSGi metadata is wrong (missing
* exports, unsatisfiable imports), the bundle fails to resolve and these tests never run — which
* the build treats as a failure.
*/
@ExtendWith(BundleContextExtension.class)
public class SemconvOsgiTest {

@Test
public void stableAttributesAreUsable() {
AttributeKey<String> serviceName = ServiceAttributes.SERVICE_NAME;
assertThat(serviceName.getKey()).isEqualTo("service.name");
assertThat(SchemaUrls.V1_41_1).isEqualTo("https://opentelemetry.io/schemas/1.41.1");
}

@Test
public void incubatingAttributesAreUsable() {
AttributeKey<String> instanceId = ServiceIncubatingAttributes.SERVICE_INSTANCE_ID;
assertThat(instanceId.getKey()).isEqualTo("service.instance.id");
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,4 @@ rootProject.name = "semantic-conventions-java"
include(":dependencyManagement")
include(":semconv-incubating")
include(":semconv")
include(":osgi-test")
Loading