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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.braintrust.bootstrap;

import java.net.URL;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

Expand All @@ -22,10 +23,14 @@ public static BraintrustClassLoader getAgentClassLoader() {
return agentClassLoaderRef.get();
}

public static void setAgentClassLoaderIfAbsent(BraintrustClassLoader classLoader) {
var witness = agentClassLoaderRef.compareAndExchange(null, classLoader);
public static BraintrustClassLoader createBraintrustClassLoader(
URL agentJarURL, ClassLoader btClassLoaderParent) throws Exception {
BraintrustClassLoader btClassLoader =
new BraintrustClassLoader(agentJarURL, btClassLoaderParent);
var witness = agentClassLoaderRef.compareAndExchange(null, btClassLoader);
if (null != witness) {
throw new IllegalStateException("agent classloader must only be set once");
}
return btClassLoader;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package dev.braintrust.system;

import dev.braintrust.bootstrap.BraintrustBridge;
import dev.braintrust.bootstrap.BraintrustClassLoader;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.jar.JarFile;

/**
Expand Down Expand Up @@ -44,18 +43,16 @@ private static synchronized void install(String agentArgs, Instrumentation inst)

if (jvmRunningWithOtelAgent()) {
log(
"ERROR: Braintrust agent is not yet compatible with the OTel javaagent -"
+ " skipping install.");
"ERROR: Braintrust agent is not yet compatible with the OTel -javaagent."
+ " aborting install.");
return;
}

if (jvmRunningWithDatadogOtel()) {
log(
"ERROR: Braintrust agent is not yet compatible with datadog javaagent otel -"
+ " skipping install.");
if (jvmRunningWithDatadogOtelConfig() && (!isRunningAfterDatadogAgent())) {
log("ERROR: Braintrust agent must run _after_ datadog -javaagent. aborting install.");
return;
}

boolean installOnBootstrap = !jvmRunningWithDatadogOtelConfig();
try {
// Locate the agent JAR from our own code source
URL agentJarURL =
Expand All @@ -67,19 +64,21 @@ private static synchronized void install(String agentArgs, Instrumentation inst)
// are set before anything can trigger GlobalOpenTelemetry.get().
enableOtelSDKAutoconfiguration();

inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarFile, false));
log("Added agent JAR to bootstrap classpath.");

// Create the isolated braintrust classloader.
// Parent is the platform classloader so agent internals can see:
// - Bootstrap classes (OTel API/SDK added via appendToBootstrapClassLoaderSearch)
// - JDK platform modules (java.net.http, java.sql, etc.)
// but NOT application classes (those are on the system/app classloader).
BraintrustClassLoader btClassLoader =
new BraintrustClassLoader(agentJarURL, ClassLoader.getPlatformClassLoader());
BraintrustBridge.setAgentClassLoaderIfAbsent(btClassLoader);
ClassLoader btClassLoaderParent;
if (installOnBootstrap) {
inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarFile, false));
btClassLoaderParent = ClassLoader.getPlatformClassLoader();
log("Added agent JAR to bootstrap classpath.");
} else {
btClassLoaderParent =
new URLClassLoader(
new URL[] {agentJarFile.toURI().toURL()},
ClassLoader.getPlatformClassLoader());
log("skipping bootstrap classpath setup");
}

// Load and invoke the real agent installer through the isolated classloader.
ClassLoader btClassLoader = createBTClassLoader(agentJarURL, btClassLoaderParent);
Class<?> installerClass = btClassLoader.loadClass(AGENT_CLASS);
installerClass
.getMethod(INSTALLER_METHOD, String.class, Instrumentation.class)
Expand All @@ -92,6 +91,16 @@ private static synchronized void install(String agentArgs, Instrumentation inst)
}
}

private static ClassLoader createBTClassLoader(URL agentJarURL, ClassLoader btClassLoaderParent)
throws Exception {
// NOTE: not caching because we only invoke this once
var bridgeClass =
btClassLoaderParent.loadClass("dev.braintrust.bootstrap.BraintrustBridge");
var createMethod =
bridgeClass.getMethod("createBraintrustClassLoader", URL.class, ClassLoader.class);
return (ClassLoader) createMethod.invoke(null, agentJarURL, btClassLoaderParent);
}

/**
* Checks whether the OpenTelemetry Java agent is present by looking for its premain class on
* the system classloader. Since {@code -javaagent} JARs are always on the system classpath,
Expand All @@ -109,16 +118,8 @@ private static boolean jvmRunningWithOtelAgent() {
}
}

/**
* Checks whether the Datadog agent is present and configured for OTel integration. Must be
* callable from the system classloader (no DD compile deps).
*/
private static boolean jvmRunningWithDatadogOtel() {
try {
Class.forName("datadog.trace.bootstrap.Agent", false, null);
} catch (ClassNotFoundException e) {
return false;
}
/** Checks whether the Datadog agent is present and configured for OTel integration */
private static boolean jvmRunningWithDatadogOtelConfig() {
String sysProp = System.getProperty("dd.trace.otel.enabled");
if (sysProp != null) {
return Boolean.parseBoolean(sysProp);
Expand All @@ -131,7 +132,7 @@ private static boolean jvmRunningWithDatadogOtel() {
* Returns true if the Datadog agent's premain has already executed, meaning it was listed
* before the Braintrust agent in the {@code -javaagent} flags.
*/
static boolean isRunningAfterDatadogAgent() {
private static boolean isRunningAfterDatadogAgent() {
// DD's premain appends its jars to the bootstrap classpath, making
// {@code datadog.trace.bootstrap.Agent} loadable from the bootstrap (null)
// classloader. If that class is not found on bootstrap, DD either isn't
Expand Down
3 changes: 3 additions & 0 deletions braintrust-java-agent/internal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ dependencies {
// These are the heavy deps that stay in BraintrustClassLoader, NOT on bootstrap.
implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}"

// for dd compat mode
compileOnly 'com.datadoghq:dd-trace-api:1.60.1'

// Test dependencies
testImplementation project(':braintrust-java-agent:bootstrap')
testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.auto.service.AutoService;
import dev.braintrust.Braintrust;
import dev.braintrust.agent.dd.BTInterceptor;
import dev.braintrust.bootstrap.BraintrustBridge;
import dev.braintrust.bootstrap.BraintrustClassLoader;
import dev.braintrust.instrumentation.Instrumenter;
Expand All @@ -21,7 +22,8 @@ public class BraintrustAgent implements AutoConfigurationCustomizerProvider {
public static void install(String agentArgs, Instrumentation inst) {
if (!(BraintrustAgent.class.getClassLoader() instanceof BraintrustClassLoader)) {
throw new IllegalStateException(
"Braintrust agent can only run on a braintrust classloader");
"Braintrust agent can only run on a braintrust classloader: "
+ BraintrustAgent.class.getClassLoader());
}
log.info(
"invoked on classloader: {}",
Expand All @@ -31,6 +33,9 @@ public static void install(String agentArgs, Instrumentation inst) {
// Fail fast if there are any issues with the Braintrust SDK
Braintrust.get();
Instrumenter.install(inst, BraintrustAgent.class.getClassLoader());
if (jvmRunningWithDatadogOtelConfig() && ddApiOnBootstrapClasspath()) {
BTInterceptor.install();
}
}

@Override
Expand All @@ -51,4 +56,24 @@ public void customize(AutoConfigurationCustomizer autoConfiguration) {
return sdkTracerProviderBuilder;
}));
}

/** Checks whether the Datadog agent is present and configured for OTel integration */
private static boolean ddApiOnBootstrapClasspath() {
try {
BraintrustAgent.class.getClassLoader().loadClass("datadog.trace.api.GlobalTracer");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}

/** Checks whether the Datadog agent is present and configured for OTel integration */
private static boolean jvmRunningWithDatadogOtelConfig() {
String sysProp = System.getProperty("dd.trace.otel.enabled");
if (sysProp != null) {
return Boolean.parseBoolean(sysProp);
}
String envVar = System.getenv("DD_TRACE_OTEL_ENABLED");
return Boolean.parseBoolean(envVar);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package dev.braintrust.agent.dd;

import datadog.trace.api.GlobalTracer;
import datadog.trace.api.interceptor.MutableSpan;
import datadog.trace.api.interceptor.TraceInterceptor;
import dev.braintrust.Braintrust;
import dev.braintrust.trace.BraintrustTracing;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.data.SpanData;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class BTInterceptor implements TraceInterceptor {
private static final AtomicBoolean installed = new AtomicBoolean(false);

public static void install() {
if (!installed.compareAndExchange(false, true)) {
try {
if (!DDSpanConverter.initialize()) {
log.warn(
"failed to initialize DD span converter. Braintrust traces will not be"
+ " reported.");
return;
}
var tbBuilder =
SdkTracerProvider.builder().setIdGenerator(OverridableIdGenerator.INSTANCE);
Braintrust.get()
.openTelemetryEnable(
tbBuilder, SdkLoggerProvider.builder(), SdkMeterProvider.builder());
final var traceProvider = tbBuilder.build();
var interceptor = new BTInterceptor(999, traceProvider);
if (!GlobalTracer.get().addTraceInterceptor(interceptor)) {
log.warn(
"trace interceptor install failed due to conflicting priorities."
+ " Braintrust traces will not be reported.");
return;
}
log.info("trace interceptor successfully installed");
} catch (Exception e) {
log.warn(
"trace interceptor install failed. Braintrust traces will not be reported.",
e);
// Don't reset the flag. We don't want to try again.
}
}
}

private final int priority;
private final Tracer tracer;

private BTInterceptor(int priority, SdkTracerProvider traceProvider) {
this.priority = priority;
this.tracer = BraintrustTracing.getTracer(traceProvider);
}

@Override
public int priority() {
return priority;
}

@Override
public Collection<? extends MutableSpan> onTraceComplete(
Collection<? extends MutableSpan> trace) {
try {
List<SpanData> spanDataList = DDSpanConverter.convertTrace(List.copyOf(trace));
DDSpanConverter.replayTrace(tracer, spanDataList);
} catch (Exception e) {
log.debug("failed to replay traces", e);
}
return trace;
}
}
Loading
Loading