From 3e1c09fdfa6d2d1ba5ea21305770a4a7ee00362f Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 6 Oct 2026 20:38:21 -0600 Subject: [PATCH] fix: put otel incubator api on the bootstrap classpath otel assumes that if its sdk can load the incubator api, its core api can too so we need to make the incubator available to the core api fixes https://github.com/braintrustdata/braintrust-sdk-java/issues/184 --- braintrust-java-agent/build.gradle | 13 ++++--- braintrust-java-agent/internal/build.gradle | 5 +-- .../braintrust/system/AgentBootstrapTest.java | 39 +++++++++++++++++++ 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/braintrust-java-agent/build.gradle b/braintrust-java-agent/build.gradle index 0cbb8b44..726bf3b9 100644 --- a/braintrust-java-agent/build.gradle +++ b/braintrust-java-agent/build.gradle @@ -96,10 +96,9 @@ configurations { // Bootstrap libraries — unshaded, added to bootstrap classpath at runtime via // Instrumentation.appendToBootstrapClassLoaderSearch(). Includes: - // - OTel API (Tracer, Span, Context, etc.) - // - OTel SDK (SdkTracerProvider, SpanProcessor, etc.) - // - OTel SDK autoconfigure (AutoConfiguredOpenTelemetrySdk) - // - OTel SDK autoconfigure SPI (AutoConfigurationCustomizerProvider) + // - OTel API (Tracer, Span, Context, etc.) and incubator API + // The API's no-op implementations must be able to load the incubator types. + // The SDK and autoconfigure remain isolated in BraintrustClassLoader. bootstrapLibs // Agent internals (ByteBuddy, OTLP exporter, OkHttp, instrumentation code) @@ -110,8 +109,10 @@ configurations { dependencies { bootstrap project(':braintrust-java-agent:bootstrap') - // OTel API + SDK + autoconfigure on bootstrap classpath + // Keep both APIs visible to bootstrap-loaded OTel no-op factories. Otherwise the + // isolated ExtendedSdkTracer casts a non-extended no-op builder during shutdown. bootstrapLibs "io.opentelemetry:opentelemetry-api:${otelVersion}" + bootstrapLibs "io.opentelemetry:opentelemetry-api-incubator:${otelVersion}-alpha" internal project(path: ':braintrust-java-agent:internal', configuration: 'shadow') @@ -151,7 +152,7 @@ jar { exclude 'META-INF/maven/**' } - // 2) Include bootstrap libraries as normal .class files (OTel API + SDK + autoconfigure) + // 2) Include bootstrap libraries as normal .class files (OTel API + incubator API) from(provider { configurations.bootstrapLibs.collect { zipTree(it) } }) { exclude 'META-INF/**' } diff --git a/braintrust-java-agent/internal/build.gradle b/braintrust-java-agent/internal/build.gradle index 21fb1c21..8492a122 100644 --- a/braintrust-java-agent/internal/build.gradle +++ b/braintrust-java-agent/internal/build.gradle @@ -26,11 +26,10 @@ dependencies { // ByteBuddy for bytecode manipulation — bundled as .classdata in BraintrustClassLoader implementation "net.bytebuddy:byte-buddy:${byteBuddyVersion}" - // OTel API on the bootstrap classpath at runtime so we compile against them but do NOT bundle them. + // Both OTel APIs are on bootstrap at runtime and excluded from the shadow JAR below. compileOnly "io.opentelemetry:opentelemetry-api:${otelVersion}" + compileOnly "io.opentelemetry:opentelemetry-api-incubator:${otelVersion}-alpha" // OTel SDK + autoconfigure engine — bundled into BraintrustClassLoader. - // These are no longer on the bootstrap classpath. - implementation "io.opentelemetry:opentelemetry-api-incubator:${otelVersion}-alpha" implementation "io.opentelemetry:opentelemetry-sdk:${otelVersion}" implementation "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${otelVersion}" implementation "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:${otelVersion}" diff --git a/braintrust-java-agent/src/test/java/dev/braintrust/system/AgentBootstrapTest.java b/braintrust-java-agent/src/test/java/dev/braintrust/system/AgentBootstrapTest.java index a508c3a5..51eb4483 100644 --- a/braintrust-java-agent/src/test/java/dev/braintrust/system/AgentBootstrapTest.java +++ b/braintrust-java-agent/src/test/java/dev/braintrust/system/AgentBootstrapTest.java @@ -7,6 +7,8 @@ import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.api.trace.TracerProvider; +import io.opentelemetry.context.Context; import java.io.File; import java.util.HashSet; import java.util.List; @@ -221,6 +223,43 @@ void otelAutoconfigureProducesRealTracer() { "Expected a real (recording) span, not a propagated/noop span"); } + @Test + void tracerAfterShutdownPreservesParentWithoutRecording() throws Exception { + // Load the SDK through the actual agent loader, not the application classpath. + // A flat classpath hides the API/incubator visibility mismatch from issue #184. + var providerClass = + BraintrustBridge.getAgentClassLoader() + .loadClass("io.opentelemetry.sdk.trace.SdkTracerProvider"); + var builder = providerClass.getMethod("builder").invoke(null); + var provider = (TracerProvider) builder.getClass().getMethod("build").invoke(builder); + var close = providerClass.getMethod("close"); + boolean closed = false; + try { + Tracer tracer = provider.get("shutdown-regression"); + Span parent = tracer.spanBuilder("before-shutdown").setNoParent().startSpan(); + assertTrue(parent.isRecording()); + var parentContext = Context.root().with(parent); + parent.end(); + + close.invoke(provider); + closed = true; + + Span child = tracer.spanBuilder("after-shutdown").setParent(parentContext).startSpan(); + assertFalse(child.isRecording()); + assertEquals(parent.getSpanContext(), child.getSpanContext()); + child.end(); + + Span root = tracer.spanBuilder("after-shutdown-root").setNoParent().startSpan(); + assertFalse(root.isRecording()); + assertFalse(root.getSpanContext().isValid()); + root.end(); + } finally { + if (!closed) { + close.invoke(provider); + } + } + } + private static boolean startsWithAny(String str, List prefixes) { for (String prefix : prefixes) { if (str.startsWith(prefix)) {