From ab76fa05794c16706b9f7cc551cd7e56120efa91 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Fri, 3 Oct 2025 09:16:10 -0600 Subject: [PATCH 1/2] use java 17 toolchain in gradle build --- .github/workflows/ci.yml | 2 +- .../workflows/publish-release-from-tag.yml | 1 + build.gradle | 156 +++++++++++------- examples/build.gradle | 8 +- .../braintrust/trace/BraintrustTracing.java | 7 +- 5 files changed, 109 insertions(+), 65 deletions(-) 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..9d8d7a0 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 { @@ -217,6 +239,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 +261,27 @@ 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')}" + } +} + +// Run validation before compilation +compileJava.dependsOn validateJavaVersion + // 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..f545f14 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustTracing.java +++ b/src/main/java/dev/braintrust/trace/BraintrustTracing.java @@ -90,10 +90,13 @@ public static void enable( final int maxExportBatchSize = 512; log.info( "Initializing Braintrust OpenTelemetry with service={}, instrumentation-name={}," - + " instrumentation-version={}", + + " instrumentation-version={}, jvm-version={}, jvm-vendor={}, jvm-name={}", OTEL_SERVICE_NAME, INSTRUMENTATION_NAME, - INSTRUMENTATION_VERSION); + INSTRUMENTATION_VERSION, + System.getProperty("java.runtime.version"), + System.getProperty("java.vendor"), + System.getProperty("java.vm.name")); // Create resource first so BraintrustSpanProcessor can access service.name var resourceBuilder = From 57cf3fc93bd6f54a4a7f331312cdf0f58319f8f7 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Fri, 3 Oct 2025 09:46:14 -0600 Subject: [PATCH 2/2] feat: add a main class for jar build sanity checking Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) --- build.gradle | 21 +++++++++++-- .../braintrust/trace/BraintrustTracing.java | 30 +++++++------------ .../java/dev/braintrust/trace/SDKMain.java | 29 ++++++++++++++++++ 3 files changed, 59 insertions(+), 21 deletions(-) create mode 100644 src/main/java/dev/braintrust/trace/SDKMain.java diff --git a/build.gradle b/build.gradle index 9d8d7a0..0ac6555 100644 --- a/build.gradle +++ b/build.gradle @@ -192,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' ) } } @@ -274,14 +275,30 @@ task validateJavaVersion { "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 - println "✓ Using Java ${currentVersion} from ${System.getProperty('java.home')}" + 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/src/main/java/dev/braintrust/trace/BraintrustTracing.java b/src/main/java/dev/braintrust/trace/BraintrustTracing.java index f545f14..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,15 +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={}, jvm-version={}, jvm-vendor={}, jvm-name={}", - OTEL_SERVICE_NAME, - INSTRUMENTATION_NAME, - INSTRUMENTATION_VERSION, - System.getProperty("java.runtime.version"), - System.getProperty("java.vendor"), - System.getProperty("java.vm.name")); + log.info(sdkInfoLogMessage()); // Create resource first so BraintrustSpanProcessor can access service.name var resourceBuilder = @@ -164,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); + } + } +}