diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index abb4ea6..fb428a2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ on:
jobs:
build:
- # we want to run ubuntu-latest but we'll pin to a specific version so CI is reproducable
+ # we want to run ubuntu-latest but we'll pin to a specific version so workflow is reproducable
runs-on: ubuntu-24.04
steps:
diff --git a/.github/workflows/publish-release-from-tag.yml b/.github/workflows/publish-release-from-tag.yml
index 00e9010..7248f1c 100644
--- a/.github/workflows/publish-release-from-tag.yml
+++ b/.github/workflows/publish-release-from-tag.yml
@@ -16,6 +16,7 @@ on:
jobs:
validate-and-publish:
name: Validate Tag and Publish Release
+ # we want to run ubuntu-latest but we'll pin to a specific version so workflow is reproducable
runs-on: ubuntu-24.04
steps:
- name: Checkout code
diff --git a/build.gradle b/build.gradle
index a5d3047..0ac6555 100644
--- a/build.gradle
+++ b/build.gradle
@@ -6,28 +6,78 @@ plugins {
id("io.freefair.lombok") version "9.0.0-rc2"
}
-// Generate braintrust.properties at build time with smart versioning
-task generateBraintrustProperties {
- description = 'Generate braintrust.properties with smart git-based versioning'
- group = 'build'
+version = generateVersion() // we could cache but not worth the hassle
+group = 'dev.braintrust'
- def outputDir = file("$buildDir/generated/resources")
- def outputFile = file("$outputDir/braintrust.properties")
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(17)
+ vendor = JvmVendorSpec.ADOPTIUM // eclipse JVM
+ }
+ withJavadocJar()
+ withSourcesJar()
+}
- outputs.file outputFile
- outputs.upToDateWhen { false } // Always regenerate to get latest version
+tasks.withType(JavaCompile).configureEach {
+ options.release = 17
+}
- doLast {
- outputDir.mkdirs()
+repositories {
+ mavenCentral()
+}
- def version = generateVersion()
+ext {
+ otelVersion = '1.54.1'
+ jacksonVersion = '2.16.1'
+ junitVersion = '5.11.4'
+ slf4jVersion = '2.0.17'
+}
- outputFile.text = "sdk.version=${version}\n"
+dependencies {
+ api "io.opentelemetry:opentelemetry-api:${otelVersion}"
+ api "io.opentelemetry:opentelemetry-sdk:${otelVersion}"
+ api "io.opentelemetry:opentelemetry-sdk-trace:${otelVersion}"
+ api "io.opentelemetry:opentelemetry-sdk-logs:${otelVersion}"
+ implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}"
+ implementation "io.opentelemetry:opentelemetry-exporter-logging:${otelVersion}"
+ implementation "io.opentelemetry:opentelemetry-sdk-testing:${otelVersion}"
+ implementation "io.opentelemetry:opentelemetry-semconv:1.30.1-alpha"
- logger.info("Generated braintrust.properties with sdk.version=${version}")
- }
+ implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
+ implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}"
+ implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jacksonVersion}"
+
+ implementation "org.slf4j:slf4j-api:${slf4jVersion}"
+ runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}"
+
+ implementation 'org.apache.commons:commons-lang3:3.14.0'
+ implementation 'com.google.code.findbugs:jsr305:3.0.2' // for @Nullable annotations
+
+ testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
+ testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
+ testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0'
+
+ // OAI instrumentation
+ compileOnly 'com.openai:openai-java:2.8.1'
+ testImplementation 'com.openai:openai-java:2.8.1'
+ implementation "io.opentelemetry.instrumentation:opentelemetry-openai-java-1.1:2.19.0-alpha"
}
+/**
+ * Use git to compute the sdk version
+ *
+ * This is written into braintrust.properties at build time and shipped into the distributed sdk jar
+ *
+ * - if we are on a tag (i.e. v0.0.3), use the tag
+ * - otherwise, use $mostRecentTag-$currentCommitSha
+ *
+ * Additionally, if the git workspace is not clean, append -DIRTY to the version
+ *
+ * Examples
+ * - v0.0.3
+ * - v0.0.3-c4af682
+ * - v0.0.3-c4af682-DIRTY
+ */
def generateVersion() {
// Check if workspace is clean
def gitStatusProcess = ['git', 'status', '--porcelain'].execute()
@@ -85,54 +135,26 @@ def generateVersion() {
return version
}
-version = generateVersion() // we could cache but not worth the hassle
-group = 'dev.braintrust'
-
-java {
- sourceCompatibility = JavaVersion.VERSION_17
- targetCompatibility = JavaVersion.VERSION_17
- withJavadocJar()
- withSourcesJar()
-}
-
-repositories {
- mavenCentral()
-}
-
-ext {
- otelVersion = '1.54.1'
- jacksonVersion = '2.16.1'
- junitVersion = '5.11.4'
- slf4jVersion = '2.0.17'
-}
+// Generate braintrust.properties at build time with smart versioning
+task generateBraintrustProperties {
+ description = 'Generate braintrust.properties with smart git-based versioning'
+ group = 'build'
-dependencies {
- api "io.opentelemetry:opentelemetry-api:${otelVersion}"
- api "io.opentelemetry:opentelemetry-sdk:${otelVersion}"
- api "io.opentelemetry:opentelemetry-sdk-trace:${otelVersion}"
- api "io.opentelemetry:opentelemetry-sdk-logs:${otelVersion}"
- implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}"
- implementation "io.opentelemetry:opentelemetry-exporter-logging:${otelVersion}"
- implementation "io.opentelemetry:opentelemetry-sdk-testing:${otelVersion}"
- implementation "io.opentelemetry:opentelemetry-semconv:1.30.1-alpha"
+ def outputDir = file("$buildDir/generated/resources")
+ def outputFile = file("$outputDir/braintrust.properties")
- implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}"
- implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}"
- implementation "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jacksonVersion}"
+ outputs.file outputFile
+ outputs.upToDateWhen { false } // Always regenerate to get latest version
- implementation "org.slf4j:slf4j-api:${slf4jVersion}"
- runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}"
+ doLast {
+ outputDir.mkdirs()
- implementation 'org.apache.commons:commons-lang3:3.14.0'
- implementation 'com.google.code.findbugs:jsr305:3.0.2' // for @Nullable annotations
+ def version = generateVersion()
- compileOnly 'com.openai:openai-java:2.8.1'
- implementation "io.opentelemetry.instrumentation:opentelemetry-openai-java-1.1:2.19.0-alpha"
+ outputFile.text = "sdk.version=${version}\n"
- testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}"
- testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}"
- testImplementation 'com.openai:openai-java:2.8.1'
- testImplementation 'com.github.tomakehurst:wiremock-jre8:2.35.0'
+ logger.info("Generated braintrust.properties with sdk.version=${version}")
+ }
}
test {
@@ -170,7 +192,8 @@ jar {
attributes(
'Implementation-Title': 'Braintrust Java SDK',
'Implementation-Version': version,
- 'Implementation-Vendor': 'Braintrust'
+ 'Implementation-Vendor': 'Braintrust',
+ 'Main-Class': 'dev.braintrust.trace.SDKMain'
)
}
}
@@ -217,6 +240,7 @@ sourceSets {
// Make sure properties are generated before compilation and packaging
compileJava.dependsOn generateBraintrustProperties
processResources.dependsOn generateBraintrustProperties
+sourcesJar.dependsOn generateBraintrustProperties
jar.dependsOn generateBraintrustProperties
// Configure Spotless for code formatting
@@ -238,6 +262,43 @@ spotless {
}
}
+task validateJavaVersion {
+ doLast {
+ def currentVersion = JavaVersion.current()
+ def requiredVersion = JavaVersion.VERSION_17
+
+ if (!currentVersion.isCompatibleWith(requiredVersion)) {
+ throw new GradleException(
+ "This project requires Java ${requiredVersion} or later. " +
+ "Current Java version: ${currentVersion} " +
+ "(${System.getProperty('java.version')}) " +
+ "from ${System.getProperty('java.home')}"
+ )
+ }
+ // println "✓ Using Java ${currentVersion} from ${System.getProperty('java.home')}"
+ }
+}
+
+
+// Task to test the jar by running it
+task testJar(type: Exec) {
+ description = 'Test the jar by running it and fail build if non-zero exit code'
+ group = 'verification'
+ dependsOn jar
+
+ commandLine 'java', '-jar', jar.archiveFile.get().asFile.absolutePath
+
+ doFirst {
+ // println "Testing jar: ${jar.archiveFile.get().asFile.absolutePath}"
+ }
+}
+
+// Run validation before compilation
+compileJava.dependsOn validateJavaVersion
+
+// Run jar test as part of check task
+check.dependsOn testJar
+
// Task to install git hooks
task installGitHooks(type: Exec) {
description = 'Install git hooks for code formatting'
diff --git a/examples/build.gradle b/examples/build.gradle
index da37c7b..5eecf08 100644
--- a/examples/build.gradle
+++ b/examples/build.gradle
@@ -16,14 +16,10 @@ def braintrustLogLevel = System.getenv('BRAINTRUST_LOG_LEVEL') ?: 'info'
dependencies {
implementation project(':')
- // TODO: not sure why I have to do this. I would have expected this to come over in the project root transitive deps
+ // To run otel examples
implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}"
-
- // Official OpenAI Java SDK
+ // to run OAI instrumentation examples
implementation 'com.openai:openai-java:2.8.1'
-
- // OkHttp for HTTP client (required by OpenAI SDK)
- implementation 'com.squareup.okhttp3:okhttp:4.12.0'
}
application {
diff --git a/src/main/java/dev/braintrust/trace/BraintrustTracing.java b/src/main/java/dev/braintrust/trace/BraintrustTracing.java
index 778c3a8..bcf013b 100644
--- a/src/main/java/dev/braintrust/trace/BraintrustTracing.java
+++ b/src/main/java/dev/braintrust/trace/BraintrustTracing.java
@@ -17,7 +17,6 @@
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.resource.attributes.ResourceAttributes;
import java.time.Duration;
-import java.util.Properties;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
@@ -32,7 +31,7 @@ public final class BraintrustTracing {
public static final String PARENT_KEY = "braintrust.parent";
static final String OTEL_SERVICE_NAME = "braintrust-app";
static final String INSTRUMENTATION_NAME = "braintrust-java";
- static final String INSTRUMENTATION_VERSION = loadVersionFromProperties();
+ static final String INSTRUMENTATION_VERSION = SDKMain.loadVersionFromProperties();
/**
* Quick start method that sets up global OpenTelemetry with Braintrust defaults.
@@ -88,12 +87,7 @@ public static void enable(
final Duration exportInterval = Duration.ofSeconds(5);
final int maxQueueSize = 2048;
final int maxExportBatchSize = 512;
- log.info(
- "Initializing Braintrust OpenTelemetry with service={}, instrumentation-name={},"
- + " instrumentation-version={}",
- OTEL_SERVICE_NAME,
- INSTRUMENTATION_NAME,
- INSTRUMENTATION_VERSION);
+ log.info(sdkInfoLogMessage());
// Create resource first so BraintrustSpanProcessor can access service.name
var resourceBuilder =
@@ -161,14 +155,15 @@ public static Tracer getTracer(OpenTelemetry openTelemetry) {
return openTelemetry.getTracer(INSTRUMENTATION_NAME, INSTRUMENTATION_VERSION);
}
- private static String loadVersionFromProperties() {
- try (var is = BraintrustTracing.class.getResourceAsStream("/braintrust.properties")) {
- var props = new Properties();
- props.load(is);
- return props.getProperty("sdk.version");
- } catch (Exception e) {
- throw new RuntimeException("unable to determine sdk version", e);
- }
+ private static String sdkInfoLogMessage() {
+ return "Initializing Braintrust OpenTelemetry with service=%s, instrumentation-name=%s, instrumentation-version=%s, jvm-version=%s, jvm-vendor=%s, jvm-name=%s"
+ .formatted(
+ OTEL_SERVICE_NAME,
+ INSTRUMENTATION_NAME,
+ INSTRUMENTATION_VERSION,
+ System.getProperty("java.runtime.version"),
+ System.getProperty("java.vendor"),
+ System.getProperty("java.vm.name"));
}
private BraintrustTracing() {}
diff --git a/src/main/java/dev/braintrust/trace/SDKMain.java b/src/main/java/dev/braintrust/trace/SDKMain.java
new file mode 100644
index 0000000..143ff87
--- /dev/null
+++ b/src/main/java/dev/braintrust/trace/SDKMain.java
@@ -0,0 +1,29 @@
+package dev.braintrust.trace;
+
+import java.util.Properties;
+
+class SDKMain {
+ /**
+ * Called by the build system to verify internals of the SDK. Prints sdk version to stdout.
+ *
+ *
Be mindful of classloading here. Otel is not shipped in the jar, so referencing otel + * classes directly or indirectly will fail the build with a NoClassDefFound error. + */ + public static void main(String... args) { + var sdkVersion = loadVersionFromProperties(); + if (null == sdkVersion || sdkVersion.isEmpty()) { + throw new RuntimeException("sdk version not found: %s".formatted(sdkVersion)); + } + System.out.println(sdkVersion); + } + + static String loadVersionFromProperties() { + try (var is = SDKMain.class.getResourceAsStream("/braintrust.properties")) { + var props = new Properties(); + props.load(is); + return props.getProperty("sdk.version"); + } catch (Exception e) { + throw new RuntimeException("unable to determine sdk version", e); + } + } +}