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
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.9.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
@@ -0,0 +1,29 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.gradle

/** Shared version constants used across build scripts and convention plugins. */
object OtelVersions {
/**
* The minimum opentelemetry-api version the semconv modules are built against. It is the
* compileOnly baseline pinned in :dependencyManagement and the lower bound of the OSGi
* Import-Package range emitted for io.opentelemetry.api.* by otel.java-conventions. Keep it as
* the single source of truth for both.
*/
const val OTEL_API_BASELINE = "1.33.0"

/**
* OSGi version range for io.opentelemetry.api.* imports: the baseline up to (but excluding) the
* next major, e.g. "[1.33,2)". bnd can't infer this itself because the pinned opentelemetry-api
* baseline isn't a bnd bundle, so we set it explicitly.
*/
val OTEL_API_OSGI_RANGE: String
get() {
val floor = OTEL_API_BASELINE.substringBeforeLast('.') // e.g. 1.33
val nextMajor = OTEL_API_BASELINE.substringBefore('.').toInt() + 1 // e.g. 2
return "[$floor,$nextMajor)"
}
}
24 changes: 24 additions & 0 deletions buildSrc/src/main/kotlin/otel.java-conventions.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io.opentelemetry.gradle.OtelJavaExtension
import io.opentelemetry.gradle.OtelVersions
import org.gradle.api.tasks.testing.logging.TestExceptionFormat

plugins {
Expand All @@ -8,6 +9,7 @@ plugins {
eclipse
idea

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

Expand All @@ -29,6 +31,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 +94,21 @@ tasks {
}
}

named<Jar>("jar") {
// Configure OSGi metadata. BND auto-detects imports, but can't infer a version range for
// io.opentelemetry.api.* because the pinned compileOnly opentelemetry-api baseline isn't a bnd
// bundle, so it would import with no range and wire to any version (incl. a future 2.x). Pin it
// to the range derived from the baseline that :dependencyManagement already requires.
bundle {
bnd(
mapOf(
"-exportcontents" to "io.opentelemetry.*",
"Import-Package" to "io.opentelemetry.api.*;version=\"${OtelVersions.OTEL_API_OSGI_RANGE}\",*",
),
)
}
}

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

Expand Down
8 changes: 6 additions & 2 deletions dependencyManagement/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import io.opentelemetry.gradle.OtelVersions

plugins {
`java-platform`
}
Expand All @@ -14,7 +16,9 @@ dependencies {

constraints {
// pinned to: avoid churn, for conservative api version requirement,
// and because opentelemetry-api is a compileOnly dependency
api("io.opentelemetry:opentelemetry-api:1.33.0")
// and because opentelemetry-api is a compileOnly dependency.
// OtelVersions.OTEL_API_BASELINE is the single source of truth: otel.java-conventions derives
// the OSGi Import-Package range for io.opentelemetry.api.* from the same value.
api("io.opentelemetry:opentelemetry-api:${OtelVersions.OTEL_API_BASELINE}")
}
}
122 changes: 122 additions & 0 deletions osgi-test/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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")

// 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")
Comment thread
trask marked this conversation as resolved.
Loading