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
6 changes: 3 additions & 3 deletions braintrust-java-agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ subprojects {
}
}

// Add muzzle generation task and muzzle check plugin to all instrumentation subprojects.
// Add muzzle generation task and muzzle check plugin to smoke-test test-instrumentation.
// (Instrumentation subprojects under :braintrust-sdk are configured in braintrust-sdk/build.gradle.)
subprojects { subproject ->
subproject.afterEvaluate {
if (subproject.path.startsWith(':braintrust-java-agent:instrumentation:')
|| subproject.path == ':braintrust-java-agent:smoke-test:test-instrumentation') {
if (subproject.path == ':braintrust-java-agent:smoke-test:test-instrumentation') {
def instrumentationApi = project(':braintrust-java-agent:instrumenter')

// Bootstrap classes (e.g. BraintrustBridge) are on the bootstrap classpath at runtime.
Expand Down

This file was deleted.

15 changes: 9 additions & 6 deletions braintrust-java-agent/internal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,18 @@ dependencies {
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

// Auto-discover all instrumentation subprojects and add them as implementation dependencies.
// Any subproject under :braintrust-java-agent:instrumentation is automatically bundled
// into the shadow JAR, and its META-INF/services entries are merged.
project(':braintrust-java-agent').subprojects.each { subproject ->
if (subproject.path.startsWith(':braintrust-java-agent:instrumentation:')
|| subproject.path == ':braintrust-java-agent:smoke-test:test-instrumentation') {
// Auto-discover instrumentation subprojects from braintrust-sdk and add them as implementation
// dependencies. These get bundled into the shadow JAR with merged META-INF/services entries.
project(':braintrust-sdk').subprojects.each { subproject ->
if (subproject.path.startsWith(':braintrust-sdk:instrumentation:')) {
dependencies.add('implementation', subproject)
}
}
// Also include test-instrumentation for smoke tests
// TODO: don't bundle this instrumentation in the agent jar. Instead create a flag to look for instrumentation on the system classpath
if (findProject(':braintrust-java-agent:smoke-test:test-instrumentation') != null) {
dependencies.add('implementation', project(':braintrust-java-agent:smoke-test:test-instrumentation'))
}

test {
useJUnitPlatform()
Expand Down
150 changes: 129 additions & 21 deletions braintrust-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,86 @@ repositories {
mavenCentral()
}

def langchainVersion = '1.9.1'
// Shared config for instrumentation subprojects
subprojects {
apply plugin: 'java'
apply plugin: 'dev.braintrust.muzzle'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
vendor = JvmVendorSpec.ADOPTIUM
}
}

tasks.withType(JavaCompile).configureEach {
options.release = 17
}

repositories {
mavenCentral()
}
}

// Add muzzle generation task and muzzle check plugin to all instrumentation subprojects.
subprojects { subproject ->
subproject.afterEvaluate {
if (subproject.path.startsWith(':braintrust-sdk:instrumentation:')) {
def instrumentationApi = project(':braintrust-java-agent:instrumenter')

// Bootstrap classes (e.g. BraintrustBridge) are on the bootstrap classpath at runtime.
// Make them available at compile time so instrumentation modules can reference them.
dependencies.add('compileOnly', project(':braintrust-java-agent:bootstrap'))

// --- $Muzzle side-class generation (compile-time) ---

// Configuration for the muzzle generator classpath
configurations.maybeCreate('muzzleGenerator')
dependencies.add('muzzleGenerator', instrumentationApi)
dependencies.add('muzzleGenerator', 'net.bytebuddy:byte-buddy:1.17.5')

task generateMuzzle(type: JavaExec) {
dependsOn compileJava, instrumentationApi.tasks.named('compileJava')
description = 'Generates $Muzzle side-classes for InstrumentationModule subclasses'
group = 'build'

// Classpath: compiled instrumentation classes + instrumenter + bytebuddy + deps
classpath = files(
sourceSets.main.output.classesDirs,
configurations.muzzleGenerator,
configurations.compileClasspath
)
mainClass = 'dev.braintrust.instrumentation.muzzle.MuzzleGenerator'
args = [sourceSets.main.java.classesDirectory.get().asFile.absolutePath]
}

// Run muzzle generation after compileJava, before jar
tasks.named('classes').configure { dependsOn generateMuzzle }

// Run muzzle checks as part of the check lifecycle
tasks.named('check').configure { dependsOn 'muzzle' }
}
}
}

// Configuration for embedding instrumentation subproject classes into the SDK JAR
configurations {
// Non-transitive: we only want the instrumentation subproject JARs themselves,
// not their transitive dependencies (which include braintrust-sdk itself).
embed {
transitive = false
}
}

// Auto-discover instrumentation subprojects and add them to the embed configuration.
// This must be done after project evaluation so the subprojects are resolved.
afterEvaluate {
project.subprojects.each { sub ->
if (sub.path.startsWith(':braintrust-sdk:instrumentation:')) {
dependencies.add('embed', sub)
}
}
}

dependencies {
api "io.opentelemetry:opentelemetry-api:${otelVersion}"
Expand All @@ -40,7 +119,6 @@ dependencies {

implementation "org.slf4j:slf4j-api:${slf4jVersion}"

implementation 'org.apache.commons:commons-lang3:3.14.0'
implementation 'com.google.code.findbugs:jsr305:3.0.2' // for @Nullable annotations

implementation "com.github.spullara.mustache.java:compiler:0.9.14"
Expand All @@ -53,27 +131,44 @@ dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
testImplementation 'org.wiremock:wiremock:3.13.1'

// OAI instrumentation
compileOnly 'com.openai:openai-java:2.8.1'
testImplementation 'com.openai:openai-java:2.8.1'

// Anthropic Instrumentation
compileOnly "com.anthropic:anthropic-java:2.8.1"
testImplementation "com.anthropic:anthropic-java:2.8.1"

// Google GenAI Instrumentation
compileOnly "com.google.genai:google-genai:1.20.0"
testImplementation "com.google.genai:google-genai:1.20.0"

// LangChain4j Instrumentation
compileOnly "dev.langchain4j:langchain4j:${langchainVersion}"
compileOnly "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
compileOnly "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j-http-client:${langchainVersion}"
testImplementation "dev.langchain4j:langchain4j-open-ai:${langchainVersion}"

}

// Merge META-INF/services files from embedded instrumentation JARs.
// This ensures ServiceLoader can discover InstrumentationModule implementations.
task mergeEmbedServices {
dependsOn configurations.embed
def mergedDir = layout.buildDirectory.dir("merged-services")
outputs.dir mergedDir

doLast {
def servicesMap = [:]
configurations.embed.each { file ->
if (file.isFile()) {
zipTree(file).matching { include 'META-INF/services/**' }.visit { details ->
if (!details.isDirectory()) {
def serviceName = details.relativePath.toString()
if (!servicesMap.containsKey(serviceName)) {
servicesMap[serviceName] = new LinkedHashSet<String>()
}
servicesMap[serviceName].addAll(details.file.readLines().findAll { it.trim() })
}
}
}
}
def outDir = mergedDir.get().asFile
outDir.deleteDir()
servicesMap.each { path, lines ->
def outFile = new File(outDir, path)
outFile.parentFile.mkdirs()
outFile.text = lines.join('\n') + '\n'
}
}
}

jar.dependsOn mergeEmbedServices
jar.from(layout.buildDirectory.dir("merged-services"))

// Generate braintrust.properties at build time with smart versioning
task generateBraintrustProperties {
description = 'Generate braintrust.properties with smart git-based versioning'
Expand Down Expand Up @@ -129,6 +224,19 @@ jar {
'Main-Class': 'dev.braintrust.SDKMain'
)
}

// Embed instrumentation subproject classes into the SDK JAR.
// Use dependsOn to ensure instrumentation jars are built first.
dependsOn configurations.embed

from({
configurations.embed.collect { it.isDirectory() ? it : zipTree(it) }
}) {
// Exclude META-INF from embedded jars (we handle service files separately below)
exclude 'META-INF/**'
}

duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}

import com.vanniktech.maven.publish.JavadocJar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ muzzle {
}

dependencies {
implementation project(':braintrust-java-agent:instrumenter')
compileOnly project(':braintrust-java-agent:instrumenter')
implementation "io.opentelemetry:opentelemetry-api:${otelVersion}"
implementation 'com.google.code.findbugs:jsr305:3.0.2' // for @Nullable annotations
implementation "org.slf4j:slf4j-api:${slf4jVersion}"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.braintrust.instrumentation.anthropic;

import com.anthropic.client.AnthropicClient;
import io.opentelemetry.api.OpenTelemetry;

/** Braintrust Anthropic client instrumentation. */
public final class BraintrustAnthropic {

/** Instrument Anthropic client with Braintrust traces. */
public static AnthropicClient wrap(OpenTelemetry openTelemetry, AnthropicClient client) {
return dev.braintrust.instrumentation.anthropic.v2_2_0.BraintrustAnthropic.wrap(
openTelemetry, client);
}
}
Loading
Loading