Skip to content

Commit 39ea04e

Browse files
committed
initial java autoinstrumentation
1 parent 13fb038 commit 39ea04e

177 files changed

Lines changed: 10552 additions & 428 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.

.github/workflows/publish-release-from-tag.yml

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -100,19 +100,24 @@ jobs:
100100
# Strip 'v' prefix to get the actual version used by Gradle
101101
VERSION="${TAG#v}"
102102
103-
# Find the built JAR files (version does NOT include 'v' prefix)
104-
MAIN_JAR=$(find build/libs -name "*-${VERSION}.jar" ! -name "*-sources.jar" ! -name "*-javadoc.jar" | head -1)
105-
SOURCES_JAR=$(find build/libs -name "*-${VERSION}-sources.jar" | head -1)
106-
JAVADOC_JAR=$(find build/libs -name "*-${VERSION}-javadoc.jar" | head -1)
103+
# braintrust-sdk artifacts
104+
SDK_MAIN_JAR=$(find braintrust-sdk/build/libs -name "*-${VERSION}.jar" ! -name "*-sources.jar" ! -name "*-javadoc.jar" | head -1)
105+
SDK_SOURCES_JAR=$(find braintrust-sdk/build/libs -name "*-${VERSION}-sources.jar" | head -1)
106+
SDK_JAVADOC_JAR=$(find braintrust-sdk/build/libs -name "*-${VERSION}-javadoc.jar" | head -1)
107107
108-
echo "main-jar=$MAIN_JAR" >> $GITHUB_OUTPUT
109-
echo "sources-jar=$SOURCES_JAR" >> $GITHUB_OUTPUT
110-
echo "javadoc-jar=$JAVADOC_JAR" >> $GITHUB_OUTPUT
108+
# braintrust-java-agent artifact (single fat jar, no sources/javadoc)
109+
AGENT_JAR=$(find braintrust-java-agent/build/libs -name "braintrust-java-agent-${VERSION}.jar" | head -1)
110+
111+
echo "sdk-main-jar=$SDK_MAIN_JAR" >> $GITHUB_OUTPUT
112+
echo "sdk-sources-jar=$SDK_SOURCES_JAR" >> $GITHUB_OUTPUT
113+
echo "sdk-javadoc-jar=$SDK_JAVADOC_JAR" >> $GITHUB_OUTPUT
114+
echo "agent-jar=$AGENT_JAR" >> $GITHUB_OUTPUT
111115
112116
echo "Found artifacts:"
113-
echo " Main JAR: $MAIN_JAR"
114-
echo " Sources JAR: $SOURCES_JAR"
115-
echo " Javadoc JAR: $JAVADOC_JAR"
117+
echo " SDK Main JAR: $SDK_MAIN_JAR"
118+
echo " SDK Sources JAR: $SDK_SOURCES_JAR"
119+
echo " SDK Javadoc JAR: $SDK_JAVADOC_JAR"
120+
echo " Agent JAR: $AGENT_JAR"
116121
117122
- name: Create GitHub Release
118123
run: |
@@ -123,18 +128,16 @@ jobs:
123128
--generate-notes \
124129
--title "Release $TAG"
125130
126-
# Upload artifacts if they exist
127-
if [[ -n "${{ steps.find-artifacts.outputs.main-jar }}" && -f "${{ steps.find-artifacts.outputs.main-jar }}" ]]; then
128-
gh release upload "$TAG" "${{ steps.find-artifacts.outputs.main-jar }}"
129-
fi
130-
131-
if [[ -n "${{ steps.find-artifacts.outputs.sources-jar }}" && -f "${{ steps.find-artifacts.outputs.sources-jar }}" ]]; then
132-
gh release upload "$TAG" "${{ steps.find-artifacts.outputs.sources-jar }}"
133-
fi
134-
135-
if [[ -n "${{ steps.find-artifacts.outputs.javadoc-jar }}" && -f "${{ steps.find-artifacts.outputs.javadoc-jar }}" ]]; then
136-
gh release upload "$TAG" "${{ steps.find-artifacts.outputs.javadoc-jar }}"
137-
fi
131+
# Upload SDK artifacts
132+
for jar in \
133+
"${{ steps.find-artifacts.outputs.sdk-main-jar }}" \
134+
"${{ steps.find-artifacts.outputs.sdk-sources-jar }}" \
135+
"${{ steps.find-artifacts.outputs.sdk-javadoc-jar }}" \
136+
"${{ steps.find-artifacts.outputs.agent-jar }}"; do
137+
if [[ -n "$jar" && -f "$jar" ]]; then
138+
gh release upload "$TAG" "$jar"
139+
fi
140+
done
138141
env:
139142
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
140143
- name: Publish to Sonatype

.pre-commit-config.yaml

Lines changed: 0 additions & 34 deletions
This file was deleted.

Makefile

Lines changed: 0 additions & 63 deletions
This file was deleted.
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: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package dev.braintrust.bootstrap;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
import java.io.InputStream;
6+
import java.net.URL;
7+
import java.security.CodeSource;
8+
import java.security.SecureClassLoader;
9+
import java.security.cert.Certificate;
10+
import java.util.Collections;
11+
import java.util.Enumeration;
12+
import java.util.jar.JarEntry;
13+
import java.util.jar.JarFile;
14+
15+
/**
16+
* A classloader that loads agent-internal classes from {@code .classdata} entries inside the agent
17+
* JAR.
18+
*
19+
* <p>Classes stored under the {@code internal/} prefix with a {@code .classdata} extension are
20+
* invisible to the JVM's default classloading mechanism. This classloader knows how to find them,
21+
* providing full classloader isolation between the agent's internals and the application's
22+
* classpath.
23+
*/
24+
public class BraintrustClassLoader extends SecureClassLoader {
25+
private static final String ENTRY_PREFIX = "internal/";
26+
private static final String CLASS_DATA_SUFFIX = ".classdata";
27+
28+
private final JarFile agentJarFile;
29+
private final CodeSource agentCodeSource;
30+
private final String agentResourcePrefix;
31+
32+
static {
33+
registerAsParallelCapable();
34+
}
35+
36+
/**
37+
* Creates a new BraintrustClassLoader.
38+
*
39+
* @param agentJarURL the URL of the agent JAR file (from the -javaagent path)
40+
* @param parent the parent classloader (typically the system/platform classloader)
41+
*/
42+
public BraintrustClassLoader(URL agentJarURL, ClassLoader parent) throws Exception {
43+
super(parent);
44+
this.agentJarFile = new JarFile(new java.io.File(agentJarURL.toURI()), false);
45+
this.agentCodeSource = new CodeSource(agentJarURL, (Certificate[]) null);
46+
this.agentResourcePrefix = "jar:file:" + agentJarFile.getName() + "!/";
47+
}
48+
49+
@Override
50+
protected Class<?> findClass(String name) throws ClassNotFoundException {
51+
// Convert "dev.braintrust.agent.internal.BraintrustAgent"
52+
// -> "internal/dev/braintrust/agent/internal/BraintrustAgent.classdata"
53+
String entryName = ENTRY_PREFIX + name.replace('.', '/') + CLASS_DATA_SUFFIX;
54+
JarEntry entry = agentJarFile.getJarEntry(entryName);
55+
if (entry == null) {
56+
throw new ClassNotFoundException(name);
57+
}
58+
59+
byte[] classBytes = readEntry(entry, name);
60+
return defineClass(name, classBytes, 0, classBytes.length, agentCodeSource);
61+
}
62+
63+
@Override
64+
protected URL findResource(String name) {
65+
// For .class resource lookups, map to .classdata
66+
String entryName;
67+
if (name.endsWith(".class")) {
68+
entryName =
69+
ENTRY_PREFIX
70+
+ name.substring(0, name.length() - ".class".length())
71+
+ CLASS_DATA_SUFFIX;
72+
} else {
73+
entryName = ENTRY_PREFIX + name;
74+
}
75+
76+
JarEntry entry = agentJarFile.getJarEntry(entryName);
77+
if (entry != null) {
78+
try {
79+
return new URL(agentResourcePrefix + entryName);
80+
} catch (java.net.MalformedURLException e) {
81+
// fall through
82+
}
83+
}
84+
return null;
85+
}
86+
87+
@Override
88+
protected Enumeration<URL> findResources(String name) {
89+
URL resource = findResource(name);
90+
if (resource != null) {
91+
return Collections.enumeration(Collections.singletonList(resource));
92+
}
93+
return Collections.emptyEnumeration();
94+
}
95+
96+
private byte[] readEntry(JarEntry entry, String className) throws ClassNotFoundException {
97+
int size = (int) entry.getSize();
98+
if (size < 0) {
99+
try (InputStream in = agentJarFile.getInputStream(entry);
100+
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
101+
byte[] buf = new byte[8192];
102+
int bytesRead;
103+
while ((bytesRead = in.read(buf)) >= 0) {
104+
out.write(buf, 0, bytesRead);
105+
}
106+
return out.toByteArray();
107+
} catch (IOException e) {
108+
throw new ClassNotFoundException(className, e);
109+
}
110+
}
111+
byte[] buf = new byte[size];
112+
try (InputStream in = agentJarFile.getInputStream(entry)) {
113+
int offset = 0;
114+
while (offset < size) {
115+
int bytesRead = in.read(buf, offset, size - offset);
116+
if (bytesRead < 0) {
117+
break;
118+
}
119+
offset += bytesRead;
120+
}
121+
if (offset != size) {
122+
throw new ClassNotFoundException(
123+
className + " (incomplete read: " + offset + "/" + size + " bytes)");
124+
}
125+
return buf;
126+
} catch (IOException e) {
127+
throw new ClassNotFoundException(className, e);
128+
}
129+
}
130+
}

0 commit comments

Comments
 (0)