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)) {