Skip to content

Commit 079e331

Browse files
committed
wip
1 parent 0a2044f commit 079e331

12 files changed

Lines changed: 760 additions & 36 deletions

File tree

braintrust-java-agent/bootstrap/src/main/java/dev/braintrust/system/AgentBootstrap.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,13 @@ private static synchronized void install(String agentArgs, Instrumentation inst)
4040
}
4141

4242
log("Braintrust Java Agent starting...");
43-
44-
if (jvmRunningWithOtelAgent()) {
45-
log(
46-
"ERROR: Braintrust agent is not yet compatible with the OTel -javaagent."
47-
+ " aborting install.");
48-
return;
49-
}
5043
if (jvmRunningWithDatadogOtelConfig() && (!isRunningAfterDatadogAgent())) {
5144
log("ERROR: Braintrust agent must run _after_ datadog -javaagent. aborting install.");
5245
return;
5346
}
5447

55-
boolean installOnBootstrap = !jvmRunningWithDatadogOtelConfig();
48+
boolean installOnBootstrap =
49+
!jvmRunningWithDatadogOtelConfig() && !jvmRunningWithOtelAgent();
5650
try {
5751
// Locate the agent JAR from our own code source
5852
URL agentJarURL =

braintrust-java-agent/build.gradle

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,9 @@ configurations {
110110
dependencies {
111111
bootstrap project(':braintrust-java-agent:bootstrap')
112112

113-
// OTel API + SDK + autoconfigure on bootstrap classpath
113+
// OTel API + autoconfigure SPI on bootstrap classpath.
114+
// The SPI is needed so OtelAgentCustomizer (also on bootstrap) can implement
115+
// AutoConfigurationCustomizerProvider without going through the remapper.
114116
bootstrapLibs "io.opentelemetry:opentelemetry-api:${otelVersion}"
115117

116118
internal project(path: ':braintrust-java-agent:internal', configuration: 'shadow')
@@ -119,6 +121,9 @@ dependencies {
119121
testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
120122
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
121123
testImplementation "io.opentelemetry:opentelemetry-api:${otelVersion}"
124+
testImplementation "io.opentelemetry:opentelemetry-sdk:${otelVersion}"
125+
testImplementation "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${otelVersion}"
126+
testImplementation "io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi:${otelVersion}"
122127
testImplementation "org.slf4j:slf4j-simple:${slf4jVersion}"
123128
testCompileOnly project(':braintrust-java-agent:bootstrap')
124129
}
@@ -131,6 +136,7 @@ jar {
131136

132137
archiveBaseName.set('braintrust-java-agent')
133138
archiveClassifier.set('')
139+
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
134140

135141
manifest {
136142
attributes(
@@ -218,6 +224,36 @@ task testGlobalLogger(type: JavaExec) {
218224

219225
test.dependsOn testGlobalLogger
220226

227+
/**
228+
* Verifies that when OTel autoconfigure is on the app classpath, BT's instrumentation of
229+
* AutoConfiguredOpenTelemetrySdkBuilder.build() fires alongside the app's own customizer.
230+
*/
231+
task testOtelAutoConfigureInstrumentation(type: JavaExec) {
232+
dependsOn jar, testClasses
233+
234+
mainClass = 'dev.braintrust.system.OtelAutoConfigureInstrumentationTest'
235+
classpath = sourceSets.test.runtimeClasspath
236+
jvmArgs "-javaagent:${jar.archiveFile.get().asFile.absolutePath}"
237+
environment 'BRAINTRUST_API_KEY', 'bogus'
238+
environment 'OTEL_TRACES_EXPORTER', 'none'
239+
environment 'OTEL_METRICS_EXPORTER', 'none'
240+
environment 'OTEL_LOGS_EXPORTER', 'none'
241+
242+
inputs.files(jar)
243+
inputs.files(sourceSets.test.output)
244+
def markerFile = layout.buildDirectory.file('test-results/otel-autoconfigure-instrumentation-test.passed')
245+
outputs.file(markerFile)
246+
247+
doLast {
248+
markerFile.get().asFile.tap {
249+
parentFile.mkdirs()
250+
text = "passed at ${java.time.Instant.now()}"
251+
}
252+
}
253+
}
254+
255+
test.dependsOn testOtelAutoConfigureInstrumentation
256+
221257
import com.vanniktech.maven.publish.JavadocJar
222258
import com.vanniktech.maven.publish.JavaLibrary
223259
import com.vanniktech.maven.publish.SonatypeHost
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package dev.braintrust.agent;
2+
3+
import static net.bytebuddy.matcher.ElementMatchers.named;
4+
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
5+
6+
import com.google.auto.service.AutoService;
7+
import dev.braintrust.bootstrap.BraintrustBridge;
8+
import dev.braintrust.instrumentation.InstrumentationModule;
9+
import dev.braintrust.instrumentation.TypeInstrumentation;
10+
import dev.braintrust.instrumentation.TypeTransformer;
11+
import java.util.List;
12+
import net.bytebuddy.asm.Advice;
13+
import net.bytebuddy.description.type.TypeDescription;
14+
import net.bytebuddy.matcher.ElementMatcher;
15+
16+
/**
17+
* Intercepts {@code AutoConfiguredOpenTelemetrySdkBuilder.build()} to inject the Braintrust {@code
18+
* AutoConfigurationCustomizerProvider} into every autoconfigure SDK build — including the one
19+
* performed by the OTel Java agent.
20+
*
21+
* <p>This means Braintrust does not need to register an SPI file visible to the OTel extension
22+
* classloader; instead it hooks directly into the builder before {@code build()} executes.
23+
*/
24+
@AutoService(InstrumentationModule.class)
25+
public class AutoConfiguredSdkBuilderInstrumentationModule extends InstrumentationModule {
26+
27+
public AutoConfiguredSdkBuilderInstrumentationModule() {
28+
super("autoconfigure-sdk-builder");
29+
}
30+
31+
@Override
32+
public List<TypeInstrumentation> typeInstrumentations() {
33+
return List.of(new SdkBuilderTypeInstrumentation());
34+
}
35+
36+
static class SdkBuilderTypeInstrumentation implements TypeInstrumentation {
37+
@Override
38+
public ElementMatcher<TypeDescription> typeMatcher() {
39+
return named(
40+
"io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdkBuilder");
41+
}
42+
43+
@Override
44+
public void transform(TypeTransformer transformer) {
45+
transformer.applyAdviceToMethod(
46+
named("build").and(takesArguments(0)),
47+
AutoConfiguredSdkBuilderInstrumentationModule.class.getName() + "$BuildAdvice");
48+
}
49+
}
50+
51+
@SuppressWarnings("unused")
52+
public static class BuildAdvice {
53+
@Advice.OnMethodEnter
54+
public static void onEnter(@Advice.This Object builder) {
55+
if (true) return; // TODO FIXME
56+
ClassLoader btCl = BraintrustBridge.getAgentClassLoader();
57+
if (btCl == null) {
58+
// shouldn't happen, but just in case.
59+
System.err.println(
60+
"[braintrust] WARNING: sdk builder instrumentation applied, but braintrust"
61+
+ " bootstrap classpath is not present. Skipping instrumentation.");
62+
return;
63+
}
64+
65+
try {
66+
Class<?> agentClass = btCl.loadClass("dev.braintrust.agent.BraintrustAgent");
67+
Object agentInstance = agentClass.getDeclaredConstructor().newInstance();
68+
agentClass
69+
.getMethod(
70+
"customize",
71+
Class.forName(
72+
"io.opentelemetry.sdk.autoconfigure.spi"
73+
+ ".AutoConfigurationCustomizer",
74+
false,
75+
builder.getClass().getClassLoader()))
76+
.invoke(agentInstance, builder);
77+
} catch (Exception e) {
78+
System.err.println(
79+
"[braintrust] WARNING: failed to inject BraintrustAgent into"
80+
+ " AutoConfiguredOpenTelemetrySdkBuilder.build(): "
81+
+ e);
82+
}
83+
}
84+
}
85+
}

braintrust-java-agent/internal/src/main/java/dev/braintrust/agent/BraintrustAgent.java

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,15 @@
11
package dev.braintrust.agent;
22

3-
import com.google.auto.service.AutoService;
43
import dev.braintrust.Braintrust;
54
import dev.braintrust.agent.dd.BTInterceptor;
6-
import dev.braintrust.bootstrap.BraintrustBridge;
75
import dev.braintrust.bootstrap.BraintrustClassLoader;
86
import dev.braintrust.instrumentation.Instrumenter;
9-
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizer;
10-
import io.opentelemetry.sdk.autoconfigure.spi.AutoConfigurationCustomizerProvider;
11-
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
12-
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
137
import java.lang.instrument.Instrumentation;
148
import lombok.extern.slf4j.Slf4j;
159

1610
/** The real agent installation logic */
1711
@Slf4j
18-
@AutoService(AutoConfigurationCustomizerProvider.class)
19-
public class BraintrustAgent implements AutoConfigurationCustomizerProvider {
12+
public class BraintrustAgent {
2013

2114
/** Called reflectively from AgentBootstrap premain. */
2215
public static void install(String agentArgs, Instrumentation inst) {
@@ -38,25 +31,6 @@ public static void install(String agentArgs, Instrumentation inst) {
3831
}
3932
}
4033

41-
@Override
42-
public void customize(AutoConfigurationCustomizer autoConfiguration) {
43-
autoConfiguration.addTracerProviderCustomizer(
44-
((sdkTracerProviderBuilder, configProperties) -> {
45-
if (!BraintrustBridge.otelInstallCount.compareAndSet(0, 1)) {
46-
log.warn(
47-
"otel install invoked more than once. This should not happen."
48-
+ " Bailing.");
49-
return sdkTracerProviderBuilder;
50-
}
51-
var loggerBuilder = SdkLoggerProvider.builder();
52-
var meterBuilder = SdkMeterProvider.builder();
53-
Braintrust.get()
54-
.openTelemetryEnable(
55-
sdkTracerProviderBuilder, loggerBuilder, meterBuilder);
56-
return sdkTracerProviderBuilder;
57-
}));
58-
}
59-
6034
/** Checks whether the Datadog agent is present and configured for OTel integration */
6135
private static boolean ddApiOnBootstrapClasspath() {
6236
try {

braintrust-java-agent/internal/src/main/java/dev/braintrust/agent/GlobalOpenTelemetryInstrumentationModule.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,17 @@ static boolean onEnter() {
6161

6262
@Advice.OnMethodExit
6363
static void onExit(@Advice.Return(readOnly = false) OpenTelemetry result) {
64+
try {
65+
Class<?> openTelemetrySdkAutoConfiguration =
66+
Class.forName(
67+
"io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk");
68+
// if we make it here, otel sdk autoconfigure is on the system classpath and we've
69+
// applied instrumentation via our auto configure sdk builder module. So there's
70+
// nothing to do.
71+
return;
72+
} catch (ClassNotFoundException e) {
73+
// this is fine, we'll use our otel sdk
74+
}
6475
// almost identical to the original method, but load autoconfigure out of the braintrust
6576
// classloader
6677
ClassLoader braintrustClassLoader = BraintrustBridge.getAgentClassLoader();
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
plugins {
2+
id 'java'
3+
}
4+
5+
dependencies {
6+
// OTel API on the app classpath so the smoke test can call GlobalOpenTelemetry.get().
7+
// At runtime the OTel agent intercepts this and bridges to the agent's shaded SDK.
8+
implementation "io.opentelemetry:opentelemetry-api:${otelVersion}"
9+
// Protobuf for parsing OTLP trace export payloads in the mock collector
10+
implementation 'io.opentelemetry.proto:opentelemetry-proto:1.5.0-alpha'
11+
runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}"
12+
}
13+
14+
// ── Configuration to resolve the OTel Java agent JAR from Maven Central ──
15+
configurations {
16+
otelAgent
17+
}
18+
dependencies {
19+
otelAgent 'io.opentelemetry.javaagent:opentelemetry-javaagent:2.16.0'
20+
}
21+
22+
def braintrustAgentJar = project(':braintrust-java-agent').tasks.named('jar')
23+
def braintrustSdkExtJar = project(':braintrust-sdk').tasks.named('otelExtensionJar')
24+
25+
/**
26+
* Smoke test: Braintrust agent as -javaagent, SDK JAR as OTel extension.
27+
*
28+
* - BT agent attaches first (ByteBuddy instrumentation, bootstrap setup)
29+
* - OTel agent attaches second, discovers braintrust-sdk.jar as an extension via
30+
* -Dotel.javaagent.extensions. The SDK JAR's OtelAutoConfig SPI provider is picked
31+
* up by the OTel agent's autoconfigure machinery and wires BT into the SDK.
32+
*
33+
* Two mock OTLP collectors capture exports:
34+
* 18240 — OTel collector (otel.exporter.otlp.endpoint)
35+
* 18241 — BT backend (BRAINTRUST_API_URL / OtelAutoConfig)
36+
*/
37+
task smokeTestBTAgentFirst(type: JavaExec) {
38+
dependsOn braintrustAgentJar, braintrustSdkExtJar, classes
39+
40+
mainClass = 'dev.braintrust.smoketest.otelagent.OtelAgentSmokeTest'
41+
classpath = sourceSets.main.runtimeClasspath
42+
43+
def btAgent = braintrustAgentJar.map { it.archiveFile.get().asFile.absolutePath }
44+
def sdkExtJar = braintrustSdkExtJar.map { it.archiveFile.get().asFile.absolutePath }
45+
def otelAgentJar = configurations.otelAgent.singleFile
46+
47+
jvmArgs = [
48+
"-javaagent:${otelAgentJar.absolutePath}",
49+
"-Dotel.javaagent.extensions=${sdkExtJar.get()}",
50+
"-javaagent:${btAgent.get()}",
51+
// Point OTel OTLP exporter at our mock collector (HTTP/1.1 — mock doesn't support HTTP/2)
52+
"-Dotel.exporter.otlp.endpoint=http://127.0.0.1:18240",
53+
"-Dotel.exporter.otlp.protocol=http/protobuf",
54+
"-Dotel.traces.exporter=otlp",
55+
"-Dotel.metrics.exporter=none",
56+
"-Dotel.logs.exporter=none",
57+
"-Dotel.service.name=bt-otel-smoke-test",
58+
// Port config for the smoke test process itself
59+
"-Dbraintrust.smoketest.otel.mock.port=18240",
60+
"-Dbraintrust.smoketest.bt.mock.port=18241",
61+
// "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5566",
62+
]
63+
64+
// Prevent host environment from leaking into the test process
65+
environment.remove('OTEL_EXPORTER_OTLP_ENDPOINT')
66+
environment.remove('OTEL_TRACES_EXPORTER')
67+
environment.remove('OTEL_METRICS_EXPORTER')
68+
environment.remove('OTEL_LOGS_EXPORTER')
69+
70+
environment 'BRAINTRUST_API_KEY', 'bogus'
71+
environment 'BRAINTRUST_API_URL', 'http://127.0.0.1:18241'
72+
environment 'BRAINTRUST_FILTER_AI_SPANS', 'true'
73+
74+
// ── Caching ──
75+
inputs.files(braintrustAgentJar)
76+
inputs.files(braintrustSdkExtJar)
77+
inputs.files(configurations.otelAgent)
78+
inputs.files(sourceSets.main.output)
79+
def markerFile = layout.buildDirectory.file('test-results/otel-agent-first-smoke-test.passed')
80+
outputs.file(markerFile)
81+
82+
doLast {
83+
markerFile.get().asFile.tap {
84+
parentFile.mkdirs()
85+
text = "passed at ${java.time.Instant.now()}"
86+
}
87+
}
88+
}
89+
90+
task smokeTestOtelAgentOnly(type: JavaExec) {
91+
dependsOn braintrustAgentJar, braintrustSdkExtJar, classes
92+
93+
mainClass = 'dev.braintrust.smoketest.otelagent.OtelAgentSmokeTest'
94+
classpath = sourceSets.main.runtimeClasspath
95+
96+
def btAgent = braintrustAgentJar.map { it.archiveFile.get().asFile.absolutePath }
97+
def sdkExtJar = braintrustSdkExtJar.map { it.archiveFile.get().asFile.absolutePath }
98+
def otelAgentJar = configurations.otelAgent.singleFile
99+
100+
jvmArgs = [
101+
"-javaagent:${otelAgentJar.absolutePath}",
102+
// Point OTel OTLP exporter at our mock collector (HTTP/1.1 — mock doesn't support HTTP/2)
103+
"-Dotel.exporter.otlp.endpoint=http://127.0.0.1:18242",
104+
"-Dotel.exporter.otlp.protocol=http/protobuf",
105+
"-Dotel.traces.exporter=otlp",
106+
"-Dotel.metrics.exporter=none",
107+
"-Dotel.logs.exporter=none",
108+
"-Dotel.service.name=bt-otel-smoke-test",
109+
// Port config for the smoke test process itself
110+
"-Dbraintrust.smoketest.otel.mock.port=18242",
111+
// "-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5566",
112+
]
113+
114+
// Prevent host environment from leaking into the test process
115+
environment.remove('OTEL_EXPORTER_OTLP_ENDPOINT')
116+
environment.remove('OTEL_TRACES_EXPORTER')
117+
environment.remove('OTEL_METRICS_EXPORTER')
118+
environment.remove('OTEL_LOGS_EXPORTER')
119+
120+
// ── Caching ──
121+
inputs.files(braintrustAgentJar)
122+
inputs.files(braintrustSdkExtJar)
123+
inputs.files(configurations.otelAgent)
124+
inputs.files(sourceSets.main.output)
125+
def markerFile = layout.buildDirectory.file('test-results/otel-agent-only-smoke-test.passed')
126+
outputs.file(markerFile)
127+
128+
doLast {
129+
markerFile.get().asFile.tap {
130+
parentFile.mkdirs()
131+
text = "passed at ${java.time.Instant.now()}"
132+
}
133+
}
134+
}
135+
136+
test.dependsOn smokeTestBTAgentFirst, smokeTestOtelAgentOnly

0 commit comments

Comments
 (0)