Skip to content

Commit de384b2

Browse files
committed
initial java autoinstrumentation
1 parent a9dfa31 commit de384b2

172 files changed

Lines changed: 10293 additions & 294 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Java plugin, toolchain (Java 17 / Adoptium), options.release, and repositories
2+
// are inherited from the parent's subprojects {} block.
3+
4+
// These are compile-time dependencies so bootstrap classes can reference OTel types.
5+
// At runtime, the actual OTel JARs are bundled as normal .class files in the agent JAR
6+
// and placed on the bootstrap classpath via Instrumentation.appendToBootstrapClassLoaderSearch().
7+
dependencies {
8+
compileOnly "io.opentelemetry:opentelemetry-api:${otelVersion}"
9+
compileOnly "io.opentelemetry:opentelemetry-sdk:${otelVersion}"
10+
compileOnly "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${otelVersion}"
11+
compileOnly "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:${otelVersion}"
12+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dev.braintrust.bootstrap;
2+
3+
import java.util.concurrent.atomic.AtomicInteger;
4+
import java.util.concurrent.atomic.AtomicReference;
5+
6+
/** Globally available bootstrap classpath resource class */
7+
public class BraintrustBridge {
8+
public static final String INSTRUMENTATION_NAME = "braintrust-java";
9+
10+
/**
11+
* Diagnostic utility tracking the number of times braintrust otel has been installed.
12+
*
13+
* <p>In a production app, this should be zero until global otel get() is invoked, then it
14+
* should remain at 1 for the rest of app's lifetime.
15+
*/
16+
public static final AtomicInteger otelInstallCount = new AtomicInteger(0);
17+
18+
private static final AtomicReference<BraintrustClassLoader> agentClassLoaderRef =
19+
new AtomicReference<>();
20+
21+
public static BraintrustClassLoader getAgentClassLoader() {
22+
return agentClassLoaderRef.get();
23+
}
24+
25+
public static void setAgentClassloaderIfAbsent(BraintrustClassLoader classLoader) {
26+
var witness = agentClassLoaderRef.compareAndExchange(null, classLoader);
27+
if (null != witness) {
28+
throw new IllegalStateException("agent classloader must only be set once");
29+
}
30+
}
31+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package dev.braintrust.bootstrap;
2+
3+
import java.io.IOException;
4+
import java.io.InputStream;
5+
import java.net.URL;
6+
import java.security.CodeSource;
7+
import java.security.SecureClassLoader;
8+
import java.security.cert.Certificate;
9+
import java.util.Collections;
10+
import java.util.Enumeration;
11+
import java.util.jar.JarEntry;
12+
import java.util.jar.JarFile;
13+
14+
/**
15+
* A classloader that loads agent-internal classes from {@code .classdata} entries inside the agent
16+
* JAR.
17+
*
18+
* <p>Classes stored under the {@code internal/} prefix with a {@code .classdata} extension are
19+
* invisible to the JVM's default classloading mechanism. This classloader knows how to find them,
20+
* providing full classloader isolation between the agent's internals and the application's
21+
* classpath.
22+
*/
23+
public class BraintrustClassLoader extends SecureClassLoader {
24+
private static final String ENTRY_PREFIX = "internal/";
25+
private static final String CLASS_DATA_SUFFIX = ".classdata";
26+
27+
private final JarFile agentJarFile;
28+
private final CodeSource agentCodeSource;
29+
private final String agentResourcePrefix;
30+
31+
static {
32+
registerAsParallelCapable();
33+
}
34+
35+
/**
36+
* Creates a new BraintrustClassLoader.
37+
*
38+
* @param agentJarURL the URL of the agent JAR file (from the -javaagent path)
39+
* @param parent the parent classloader (typically the system/platform classloader)
40+
*/
41+
public BraintrustClassLoader(URL agentJarURL, ClassLoader parent) throws Exception {
42+
super(parent);
43+
this.agentJarFile = new JarFile(new java.io.File(agentJarURL.toURI()), false);
44+
this.agentCodeSource = new CodeSource(agentJarURL, (Certificate[]) null);
45+
this.agentResourcePrefix = "jar:file:" + agentJarFile.getName() + "!/";
46+
}
47+
48+
@Override
49+
protected Class<?> findClass(String name) throws ClassNotFoundException {
50+
// Convert "dev.braintrust.agent.internal.BraintrustAgent"
51+
// -> "internal/dev/braintrust/agent/internal/BraintrustAgent.classdata"
52+
String entryName = ENTRY_PREFIX + name.replace('.', '/') + CLASS_DATA_SUFFIX;
53+
JarEntry entry = agentJarFile.getJarEntry(entryName);
54+
if (entry == null) {
55+
throw new ClassNotFoundException(name);
56+
}
57+
58+
byte[] classBytes = readEntry(entry, name);
59+
return defineClass(name, classBytes, 0, classBytes.length, agentCodeSource);
60+
}
61+
62+
@Override
63+
protected URL findResource(String name) {
64+
// For .class resource lookups, map to .classdata
65+
String entryName;
66+
if (name.endsWith(".class")) {
67+
entryName =
68+
ENTRY_PREFIX
69+
+ name.substring(0, name.length() - ".class".length())
70+
+ CLASS_DATA_SUFFIX;
71+
} else {
72+
entryName = ENTRY_PREFIX + name;
73+
}
74+
75+
JarEntry entry = agentJarFile.getJarEntry(entryName);
76+
if (entry != null) {
77+
try {
78+
return new URL(agentResourcePrefix + entryName);
79+
} catch (java.net.MalformedURLException e) {
80+
// fall through
81+
}
82+
}
83+
return null;
84+
}
85+
86+
@Override
87+
protected Enumeration<URL> findResources(String name) {
88+
URL resource = findResource(name);
89+
if (resource != null) {
90+
return Collections.enumeration(Collections.singletonList(resource));
91+
}
92+
return Collections.emptyEnumeration();
93+
}
94+
95+
private byte[] readEntry(JarEntry entry, String className) throws ClassNotFoundException {
96+
int size = (int) entry.getSize();
97+
byte[] buf = new byte[size];
98+
try (InputStream in = agentJarFile.getInputStream(entry)) {
99+
int offset = 0;
100+
while (offset < size) {
101+
int bytesRead = in.read(buf, offset, size - offset);
102+
if (bytesRead < 0) {
103+
break;
104+
}
105+
offset += bytesRead;
106+
}
107+
if (offset != size) {
108+
throw new ClassNotFoundException(
109+
className + " (incomplete read: " + offset + "/" + size + " bytes)");
110+
}
111+
return buf;
112+
} catch (IOException e) {
113+
throw new ClassNotFoundException(className, e);
114+
}
115+
}
116+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package dev.braintrust.system;
2+
3+
import dev.braintrust.bootstrap.BraintrustBridge;
4+
import dev.braintrust.bootstrap.BraintrustClassLoader;
5+
import io.opentelemetry.api.trace.*;
6+
import java.io.File;
7+
import java.lang.instrument.Instrumentation;
8+
import java.net.URL;
9+
import java.util.jar.JarFile;
10+
11+
/**
12+
* Braintrust Java Agent entry point.
13+
*
14+
* <p>Minimal code which bootstraps braintrust classloading and instrumentation.
15+
*/
16+
public class AgentBootstrap {
17+
private static final String AGENT_CLASS = "dev.braintrust.agent.BraintrustAgent";
18+
private static final String INSTALLER_METHOD = "install";
19+
20+
static volatile boolean installed = false;
21+
22+
/** Entry point when the agent is loaded at JVM startup via {@code -javaagent}. */
23+
public static void premain(String agentArgs, Instrumentation inst) {
24+
install(agentArgs, inst);
25+
}
26+
27+
/** Entry point when the agent is attached to a running JVM via the Attach API. */
28+
public static void agentmain(String agentArgs, Instrumentation inst) {
29+
install(agentArgs, inst);
30+
}
31+
32+
private static synchronized void install(String agentArgs, Instrumentation inst) {
33+
if (AgentBootstrap.class.getClassLoader() != ClassLoader.getSystemClassLoader()) {
34+
log(
35+
"WARNING: install attempted on non-system classloader. aborting. classloader: "
36+
+ AgentBootstrap.class.getClassLoader());
37+
return;
38+
}
39+
if (installed) {
40+
log("Agent already installed, skipping.");
41+
return;
42+
}
43+
44+
log("Braintrust Java Agent starting...");
45+
46+
if (jvmRunningWithOtelAgent()) {
47+
log(
48+
"ERROR: Braintrust agent is not yet compatible with the OTel javaagent -"
49+
+ " skipping install.");
50+
return;
51+
}
52+
53+
if (jvmRunningWithDatadogOtel()) {
54+
log(
55+
"ERROR: Braintrust agent is not yet compatible with datadog javaagent otel -"
56+
+ " skipping install.");
57+
return;
58+
}
59+
60+
try {
61+
// Locate the agent JAR from our own code source
62+
URL agentJarURL =
63+
AgentBootstrap.class.getProtectionDomain().getCodeSource().getLocation();
64+
File agentJarFile = new File(agentJarURL.toURI());
65+
log("Agent JAR: " + agentJarFile);
66+
67+
// Enable OTel autoconfigure BEFORE adding to bootstrap, so system properties
68+
// are set before anything can trigger GlobalOpenTelemetry.get().
69+
enableOtelSDKAutoconfiguration();
70+
71+
inst.appendToBootstrapClassLoaderSearch(new JarFile(agentJarFile, false));
72+
log("Added agent JAR to bootstrap classpath.");
73+
74+
// Create the isolated braintrust classloader.
75+
// Parent is the platform classloader so agent internals can see:
76+
// - Bootstrap classes (OTel API/SDK added via appendToBootstrapClassLoaderSearch)
77+
// - JDK platform modules (java.net.http, java.sql, etc.)
78+
// but NOT application classes (those are on the system/app classloader).
79+
BraintrustClassLoader btClassLoader =
80+
new BraintrustClassLoader(agentJarURL, ClassLoader.getPlatformClassLoader());
81+
BraintrustBridge.setAgentClassloaderIfAbsent(btClassLoader);
82+
83+
// Load and invoke the real agent installer through the isolated classloader.
84+
Class<?> installerClass = btClassLoader.loadClass(AGENT_CLASS);
85+
installerClass
86+
.getMethod(INSTALLER_METHOD, String.class, Instrumentation.class)
87+
.invoke(null, agentArgs, inst);
88+
log("Braintrust Java Agent installed.");
89+
installed = true;
90+
} catch (Throwable t) {
91+
log("ERROR: Failed to install Braintrust Java Agent: " + t.getMessage());
92+
log(t);
93+
}
94+
}
95+
96+
/**
97+
* Checks whether the OpenTelemetry Java agent is present by looking for its premain class on
98+
* the system classloader. Since {@code -javaagent} JARs are always on the system classpath,
99+
* this works regardless of agent ordering.
100+
*/
101+
private static boolean jvmRunningWithOtelAgent() {
102+
try {
103+
Class.forName(
104+
"io.opentelemetry.javaagent.OpenTelemetryAgent",
105+
false,
106+
ClassLoader.getSystemClassLoader());
107+
return true;
108+
} catch (ClassNotFoundException e) {
109+
return false;
110+
}
111+
}
112+
113+
/**
114+
* Checks whether the Datadog agent is present and configured for OTel integration. Must be
115+
* callable from the system classloader (no DD compile deps).
116+
*/
117+
private static boolean jvmRunningWithDatadogOtel() {
118+
try {
119+
Class.forName("datadog.trace.bootstrap.Agent", false, null);
120+
} catch (ClassNotFoundException e) {
121+
return false;
122+
}
123+
String sysProp = System.getProperty("dd.trace.otel.enabled");
124+
if (sysProp != null) {
125+
return Boolean.parseBoolean(sysProp);
126+
}
127+
String envVar = System.getenv("DD_TRACE_OTEL_ENABLED");
128+
return Boolean.parseBoolean(envVar);
129+
}
130+
131+
/**
132+
* Returns true if the Datadog agent's premain has already executed, meaning it was listed
133+
* before the Braintrust agent in the {@code -javaagent} flags.
134+
*/
135+
static boolean isRunningAfterDatadogAgent() {
136+
// DD's premain appends its jars to the bootstrap classpath, making
137+
// {@code datadog.trace.bootstrap.Agent} loadable from the bootstrap (null)
138+
// classloader. If that class is not found on bootstrap, DD either isn't
139+
// present or hasn't run its premain yet (i.e. BT is first).
140+
try {
141+
Class.forName("datadog.trace.bootstrap.Agent", false, null);
142+
return true;
143+
} catch (ClassNotFoundException e) {
144+
return false;
145+
}
146+
}
147+
148+
private static void enableOtelSDKAutoconfiguration() {
149+
// Enable OTel SDK autoconfiguration. When anyone first calls
150+
// GlobalOpenTelemetry.get(), the SDK will be built using autoconfigure, which
151+
// discovers our BraintrustAutoConfigCustomizer via ServiceLoader and injects the
152+
// Braintrust span processor/exporter into the tracer provider.
153+
setPropertyIfAbsent("otel.java.global-autoconfigure.enabled", "true");
154+
155+
// Set default exporter config. We don't want autoconfigure to try loading
156+
// OTLP exporters from the bootstrap classpath (they live in BraintrustClassLoader).
157+
// Our BraintrustAutoConfigCustomizer adds the real span processor/exporter.
158+
setPropertyIfAbsent("otel.traces.exporter", "none");
159+
setPropertyIfAbsent("otel.metrics.exporter", "none");
160+
setPropertyIfAbsent("otel.logs.exporter", "none");
161+
log("Enabled OTel SDK autoconfiguration.");
162+
}
163+
164+
/** Sets a system property only if it hasn't been set already (respects user overrides). */
165+
private static void setPropertyIfAbsent(String key, String value) {
166+
if (System.getProperty(key) == null) {
167+
System.setProperty(key, value);
168+
}
169+
}
170+
171+
private static void log(String msg) {
172+
System.out.println("[braintrust] " + msg);
173+
}
174+
175+
private static void log(Throwable t) {
176+
t.printStackTrace(System.err);
177+
}
178+
}

0 commit comments

Comments
 (0)