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
13 changes: 7 additions & 6 deletions braintrust-java-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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')

Expand Down Expand Up @@ -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/**'
}
Expand Down
5 changes: 2 additions & 3 deletions braintrust-java-agent/internal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String> prefixes) {
for (String prefix : prefixes) {
if (str.startsWith(prefix)) {
Expand Down
Loading