From f037eaa8b3c04805b74ffb3a37668b546cc3b5c2 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Mon, 29 Sep 2025 21:51:35 -0600 Subject: [PATCH 1/6] tidy up example output --- .../main/java/dev/braintrust/examples/ExperimentExample.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/src/main/java/dev/braintrust/examples/ExperimentExample.java b/examples/src/main/java/dev/braintrust/examples/ExperimentExample.java index faf075a..b343ddd 100644 --- a/examples/src/main/java/dev/braintrust/examples/ExperimentExample.java +++ b/examples/src/main/java/dev/braintrust/examples/ExperimentExample.java @@ -54,8 +54,6 @@ public static void main(String[] args) throws Exception { result -> "vegetable".equals(result) ? 1.0 : 0.0)) .build(); var result = eval.run(); - System.out.println( - "\n\n Example complete! View your data in Braintrust: " - + result.createReportString()); + System.out.println("\n\n" + result.createReportString()); } } From d1293b5428f501c488730484cf9db6a918ac8ead Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 30 Sep 2025 09:48:24 -0600 Subject: [PATCH 2/6] gut out BraintrustLogger impl to proxy to slf4j This is an intermediate step to full slf4j logging --- .../examples/CustomOpenTelemetryExample.java | 5 +- .../braintrust/api/BraintrustApiClient.java | 16 +- .../braintrust/config/BraintrustConfig.java | 4 +- .../otel/ChatCompletionEventsHelper.java | 4 +- .../dev/braintrust/log/BraintrustLogger.java | 252 +----------------- .../trace/BraintrustLogExporter.java | 11 +- .../trace/BraintrustSpanExporter.java | 13 +- .../trace/BraintrustSpanProcessor.java | 43 +-- .../braintrust/trace/BraintrustTracing.java | 19 +- .../java/dev/braintrust/eval/EvalTest.java | 2 +- .../trace/BraintrustTracingTest.java | 2 +- 11 files changed, 74 insertions(+), 297 deletions(-) diff --git a/examples/src/main/java/dev/braintrust/examples/CustomOpenTelemetryExample.java b/examples/src/main/java/dev/braintrust/examples/CustomOpenTelemetryExample.java index bb15a68..8e16397 100644 --- a/examples/src/main/java/dev/braintrust/examples/CustomOpenTelemetryExample.java +++ b/examples/src/main/java/dev/braintrust/examples/CustomOpenTelemetryExample.java @@ -1,7 +1,6 @@ package dev.braintrust.examples; import dev.braintrust.config.BraintrustConfig; -import dev.braintrust.log.BraintrustLogger; import dev.braintrust.trace.BraintrustTracing; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; @@ -88,7 +87,7 @@ private static void registerShutdownHook(OpenTelemetrySdk otel) { .addShutdownHook( new Thread( () -> { - BraintrustLogger.debug( + System.out.println( "Shutting down. Force-Flushing all otel data."); var result = CompletableResultCode.ofAll( @@ -108,7 +107,7 @@ private static void registerShutdownHook(OpenTelemetrySdk otel) { TimeUnit .SECONDS)) .toList()); - BraintrustLogger.debug( + System.out.println( "otel shutdown complete. Flush done: %s, Flush successful: %s" .formatted( result.isDone(), result.isSuccess())); diff --git a/src/main/java/dev/braintrust/api/BraintrustApiClient.java b/src/main/java/dev/braintrust/api/BraintrustApiClient.java index 00adb53..34ecbbe 100644 --- a/src/main/java/dev/braintrust/api/BraintrustApiClient.java +++ b/src/main/java/dev/braintrust/api/BraintrustApiClient.java @@ -170,7 +170,7 @@ private CompletableFuture postAsync( } private CompletableFuture sendAsync(HttpRequest request, Class responseType) { - BraintrustLogger.debug("API Request: {} {}", request.method(), request.uri()); + BraintrustLogger.get().debug("API Request: {} {}", request.method(), request.uri()); return httpClient .sendAsync(request, HttpResponse.BodyHandlers.ofString()) @@ -178,20 +178,22 @@ private CompletableFuture sendAsync(HttpRequest request, Class respons } private T handleResponse(HttpResponse response, Class responseType) { - BraintrustLogger.debug("API Response: {} - {}", response.statusCode(), response.body()); + BraintrustLogger.get() + .debug("API Response: {} - {}", response.statusCode(), response.body()); if (response.statusCode() >= 200 && response.statusCode() < 300) { try { return objectMapper.readValue(response.body(), responseType); } catch (IOException e) { - BraintrustLogger.warn("Failed to parse response body", e); + BraintrustLogger.get().warn("Failed to parse response body", e); throw new ApiException("Failed to parse response body", e); } } else { - BraintrustLogger.warn( - "API request failed with status {}: {}", - response.statusCode(), - response.body()); + BraintrustLogger.get() + .warn( + "API request failed with status {}: {}", + response.statusCode(), + response.body()); throw new ApiException( String.format( "API request failed with status %d: %s", diff --git a/src/main/java/dev/braintrust/config/BraintrustConfig.java b/src/main/java/dev/braintrust/config/BraintrustConfig.java index 4bbfbc3..e18a5d1 100644 --- a/src/main/java/dev/braintrust/config/BraintrustConfig.java +++ b/src/main/java/dev/braintrust/config/BraintrustConfig.java @@ -31,8 +31,8 @@ public final class BraintrustConfig extends BaseConfig { Duration.ofSeconds(getConfig("BRAINTRUST_REQUEST_TIMEOUT", 30)); /** Setting for unit testing. Do not use in production. */ - private final boolean unitTetJavaExportSpansInMemory = - getConfig("BRAINTRUST_TEST_JAVA_EXPORT_SPANS_IN_MEMORY", false); + private final boolean exportSpansInMemoryForUnitTest = + getConfig("BRAINTRUST_JAVA_EXPORT_SPANS_IN_MEMORY_FOR_UNIT_TEST", false); public static BraintrustConfig fromEnvironment() { return of(); diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java index 8f80711..acb773e 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java @@ -76,7 +76,7 @@ public static void emitPromptLogEvents( "braintrust.input_json", JSON_MAPPER.writeValueAsString(content)); } catch (JsonProcessingException e) { - BraintrustLogger.error("Error mapping json", e); + BraintrustLogger.get().error("Error mapping json", e); } body.put("content", Value.of(content)); } @@ -200,7 +200,7 @@ public static void emitCompletionLogEvents( var completionJson = JSON_MAPPER.writeValueAsString(completion); Span.current().setAttribute("braintrust.output_json", completionJson); } catch (JsonProcessingException e) { - BraintrustLogger.error("Error mapping completion json", e); + BraintrustLogger.get().error("Error mapping completion json", e); } for (ChatCompletion.Choice choice : completion.choices()) { ChatCompletionMessage choiceMsg = choice.message(); diff --git a/src/main/java/dev/braintrust/log/BraintrustLogger.java b/src/main/java/dev/braintrust/log/BraintrustLogger.java index 49d7996..a89b620 100644 --- a/src/main/java/dev/braintrust/log/BraintrustLogger.java +++ b/src/main/java/dev/braintrust/log/BraintrustLogger.java @@ -1,11 +1,7 @@ package dev.braintrust.log; -import java.util.concurrent.locks.ReadWriteLock; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.slf4j.event.Level; /** * Centralized logging for Braintrust SDK. Provides consistent logging with SDK-specific control and @@ -13,246 +9,14 @@ */ public final class BraintrustLogger { private static final String LOGGER_NAME = "braintrust"; - private static final String DEBUG_ENV = "BRAINTRUST_DEBUG"; - private static final ReadWriteLock lock = new ReentrantReadWriteLock(); - private static volatile LoggerImplementation implementation; - private static volatile Level minLevel; - - static { - // BT logger will filter log levels so make the impl most permissive - // FIXME: Don't ship this -- we don't want to set common slf4j log properties - System.setProperty("org.slf4j.simpleLogger.defaultLogLevel", "trace"); - // Initialize with default SLF4J implementation - var defaultLogger = LoggerFactory.getLogger(LOGGER_NAME); - implementation = new Slf4jLoggerImplementation(defaultLogger); - - // Check environment for debug mode - var debugEnabled = Boolean.parseBoolean(System.getenv(DEBUG_ENV)); - minLevel = debugEnabled ? Level.DEBUG : Level.INFO; - } - - private BraintrustLogger() { - // Utility class - } - - /** Sets a custom logger implementation. */ - public static void setLogger(LoggerImplementation logger) { - lock.writeLock().lock(); - try { - implementation = logger; - } finally { - lock.writeLock().unlock(); - } - } - - /** Sets the minimum log level. */ - public static void setLevel(Level level) { - lock.writeLock().lock(); - try { - minLevel = level; - } finally { - lock.writeLock().unlock(); - } - } - - /** Enables or disables debug logging. */ - public static void setDebugEnabled(boolean enabled) { - setLevel(enabled ? Level.DEBUG : Level.INFO); - } - - /** Gets the current log level. Package-private for testing. */ - static Level getLevel() { - lock.readLock().lock(); - try { - return minLevel; - } finally { - lock.readLock().unlock(); - } - } - - /** Gets the current logger implementation. Package-private for testing. */ - static LoggerImplementation getLogger() { - lock.readLock().lock(); - try { - return implementation; - } finally { - lock.readLock().unlock(); - } - } - - /** Logs a debug message. */ - public static void debug(String message, Object... args) { - log(Level.DEBUG, message, args); - } - - /** Logs a debug message with lazy evaluation. */ - public static void debug(Supplier messageSupplier) { - log(Level.DEBUG, messageSupplier.get()); - } - - /** Logs an info message. */ - public static void info(String message, Object... args) { - log(Level.INFO, message, args); - } - - /** Logs a warning message. */ - public static void warn(String message, Object... args) { - log(Level.WARN, message, args); - } - - /** Logs a warning message with an exception. */ - public static void warn(String message, Throwable throwable, Object... args) { - lock.readLock().lock(); - try { - if (isEnabled(Level.WARN)) { - implementation.log(Level.WARN, message, throwable, args); - } - } finally { - lock.readLock().unlock(); - } - } - - /** Logs an error message. */ - public static void error(String message, Object... args) { - log(Level.ERROR, message, args); - } - - /** Logs an error message with an exception. */ - public static void error(String message, Throwable throwable, Object... args) { - lock.readLock().lock(); - try { - if (isEnabled(Level.ERROR)) { - implementation.log(Level.ERROR, message, throwable, args); - } - } finally { - lock.readLock().unlock(); - } - } - - private static void log(Level level, String message, Object... args) { - lock.readLock().lock(); - try { - if (isEnabled(level)) { - implementation.log(level, message, null, args); - } - } finally { - lock.readLock().unlock(); - } - } - - private static boolean isEnabled(Level level) { - // In SLF4J Level enum, severity is in reverse order: - // ERROR(0) < WARN(1) < INFO(2) < DEBUG(3) < TRACE(4) - // So we need to check if level <= minLevel - return level.compareTo(minLevel) <= 0; - } - - /** Logger implementation interface for pluggable logging. */ - public interface LoggerImplementation { - void log(Level level, String message, Throwable throwable, Object... args); - } - - /** SLF4J-based implementation. */ - public static class Slf4jLoggerImplementation implements LoggerImplementation { - private final Logger logger; - - public Slf4jLoggerImplementation(Logger logger) { - this.logger = logger; - } - - @Override - public void log(Level level, String message, Throwable throwable, Object... args) { - // Format message with prefix - var prefixedMessage = "braintrust: " + message; - - switch (level) { - case DEBUG -> { - if (throwable != null) { - if (args.length > 0) { - logger.debug(prefixedMessage, args, throwable); - } else { - logger.debug(prefixedMessage, throwable); - } - } else { - logger.debug(prefixedMessage, args); - } - } - case INFO -> { - if (throwable != null) { - if (args.length > 0) { - logger.info(prefixedMessage, args, throwable); - } else { - logger.info(prefixedMessage, throwable); - } - } else { - logger.info(prefixedMessage, args); - } - } - case WARN -> { - if (throwable != null) { - if (args.length > 0) { - logger.warn(prefixedMessage, args, throwable); - } else { - logger.warn(prefixedMessage, throwable); - } - } else { - logger.warn(prefixedMessage, args); - } - } - case ERROR -> { - if (throwable != null) { - if (args.length > 0) { - logger.error(prefixedMessage, args, throwable); - } else { - logger.error(prefixedMessage, throwable); - } - } else { - logger.error(prefixedMessage, args); - } - } - } - } - } - - /** Test implementation that captures log messages. */ - public static class TestLoggerImplementation implements LoggerImplementation { - private final java.util.List entries = - new java.util.concurrent.CopyOnWriteArrayList<>(); - - @Override - public void log(Level level, String message, Throwable throwable, Object... args) { - // Format SLF4J style messages - var formattedMessage = formatMessage(message, args); - entries.add(new LogEntry(level, formattedMessage, throwable)); - } - - private String formatMessage(String message, Object... args) { - if (args == null || args.length == 0) { - return message; - } - - var result = new StringBuilder(); - var parts = message.split("\\{\\}", -1); - - for (int i = 0; i < parts.length; i++) { - result.append(parts[i]); - if (i < args.length && i < parts.length - 1) { - result.append(args[i]); - } - } - - return result.toString(); - } - - public java.util.List getEntries() { - return new java.util.ArrayList<>(entries); - } - - public void clear() { - entries.clear(); - } - - public record LogEntry(Level level, String message, Throwable throwable) {} + /** + * Get or create the braintrust logger + * + *

Note: this calls LoggerFactory which may initialize a global logger. Set your desired + * logging globals before calling this method. + */ + public static Logger get() { + return LoggerFactory.getLogger(LOGGER_NAME); } } diff --git a/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java b/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java index 02493bb..ecc54fc 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java +++ b/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java @@ -60,7 +60,7 @@ private CompletableResultCode exportWithParent(String parent, List= 1024) { - BraintrustLogger.info("Clearing exporter cache. This should not happen"); + BraintrustLogger.get().info("Clearing exporter cache. This should not happen"); exporterCache.clear(); } var exporter = @@ -78,18 +78,19 @@ private CompletableResultCode exportWithParent(String parent, List spa try { // Get or create exporter for this parent if (exporterCache.size() >= 1024) { - BraintrustLogger.info("Clearing exporter cache. This should not happen"); + BraintrustLogger.get().info("Clearing exporter cache. This should not happen"); exporterCache.clear(); } var exporter = @@ -81,15 +81,16 @@ private CompletableResultCode exportWithParent(String parent, List spa // Add x-bt-parent header if we have a parent if (!p.isEmpty()) { exporterBuilder.addHeader("x-bt-parent", p); - BraintrustLogger.debug( - "Created exporter with x-bt-parent: {}", p); + BraintrustLogger.get() + .debug("Created exporter with x-bt-parent: {}", p); } return exporterBuilder.build(); }); - BraintrustLogger.debug("Exporting {} spans with x-bt-parent: {}", spans.size(), parent); - if (config.unitTetJavaExportSpansInMemory()) { + BraintrustLogger.get() + .debug("Exporting {} spans with x-bt-parent: {}", spans.size(), parent); + if (config.exportSpansInMemoryForUnitTest()) { SPANS_EXPORTED.putIfAbsent(parent, new CopyOnWriteArrayList<>()); SPANS_EXPORTED.get(parent).addAll(spans); return CompletableResultCode.ofSuccess(); @@ -97,7 +98,7 @@ private CompletableResultCode exportWithParent(String parent, List spa return exporter.export(spans); } } catch (Exception e) { - BraintrustLogger.error("Failed to export spans", e); + BraintrustLogger.get().error("Failed to export spans", e); return CompletableResultCode.ofFailure(); } } diff --git a/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java b/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java index 616058b..4008593 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java +++ b/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java @@ -54,7 +54,7 @@ public BraintrustSpanProcessor(BraintrustConfig config, SpanProcessor delegate) @Override public void onStart(@NotNull Context parentContext, ReadWriteSpan span) { - BraintrustLogger.debug("OnStart: span={}, parent={}", span.getName(), parentContext); + BraintrustLogger.get().debug("OnStart: span={}, parent={}", span.getName(), parentContext); // Check if span already has a parent attribute if (span.getAttribute(PARENT) == null) { @@ -66,10 +66,11 @@ public void onStart(@NotNull Context parentContext, ReadWriteSpan span) { .ifPresent( parentValue -> { span.setAttribute(PARENT, parentValue); - BraintrustLogger.debug( - "OnStart: set parent {} for span {}", - parentValue, - span.getName()); + BraintrustLogger.get() + .debug( + "OnStart: set parent {} for span {}", + parentValue, + span.getName()); }); } else { btContext @@ -77,16 +78,21 @@ public void onStart(@NotNull Context parentContext, ReadWriteSpan span) { .ifPresent( id -> { span.setAttribute(PARENT, "project_id:" + id); - BraintrustLogger.debug( - "OnStart: set parent project {} from context", id); + BraintrustLogger.get() + .debug( + "OnStart: set parent project {} from context", + id); }); btContext .experimentId() .ifPresent( id -> { span.setAttribute(PARENT, "experiment_id:" + id); - BraintrustLogger.debug( - "OnStart: set parent experiment {} from context", id); + BraintrustLogger.get() + .debug( + "OnStart: set parent experiment {} from" + + " context", + id); }); } } @@ -134,15 +140,16 @@ public Optional getParentContext(String traceId) { private void logSpanDetails(ReadableSpan span) { var spanData = span.toSpanData(); - BraintrustLogger.debug( - "Span completed: name={}, traceId={}, spanId={}, duration={}ms, attributes={}," - + " events={}", - spanData.getName(), - spanData.getTraceId(), - spanData.getSpanId(), - (spanData.getEndEpochNanos() - spanData.getStartEpochNanos()) / 1_000_000, - spanData.getAttributes(), - spanData.getEvents()); + BraintrustLogger.get() + .debug( + "Span completed: name={}, traceId={}, spanId={}, duration={}ms," + + " attributes={}, events={}", + spanData.getName(), + spanData.getTraceId(), + spanData.getSpanId(), + (spanData.getEndEpochNanos() - spanData.getStartEpochNanos()) / 1_000_000, + spanData.getAttributes(), + spanData.getEvents()); } /** Parent context for spans (project or experiment). */ diff --git a/src/main/java/dev/braintrust/trace/BraintrustTracing.java b/src/main/java/dev/braintrust/trace/BraintrustTracing.java index 4b7dfc3..6ba0f81 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustTracing.java +++ b/src/main/java/dev/braintrust/trace/BraintrustTracing.java @@ -61,7 +61,7 @@ public static OpenTelemetry of(@Nonnull BraintrustConfig config, boolean registe .build(); if (registerGlobal) { GlobalOpenTelemetry.set(openTelemetry); - BraintrustLogger.debug("Registered OpenTelemetry globally"); + BraintrustLogger.get().debug("Registered OpenTelemetry globally"); } return openTelemetry; } @@ -90,7 +90,8 @@ public static void enable( final Duration exportInterval = Duration.ofSeconds(5); final int maxQueueSize = 2048; final int maxExportBatchSize = 512; - BraintrustLogger.info("Initializing Braintrust OpenTelemetry with service={}", serviceName); + BraintrustLogger.get() + .info("Initializing Braintrust OpenTelemetry with service={}", serviceName); // Create resource first so BraintrustSpanProcessor can access service.name var resourceBuilder = @@ -127,8 +128,8 @@ public static void enable( .addShutdownHook( new Thread( () -> { - BraintrustLogger.debug( - "Shutting down. Force-Flushing all otel data."); + BraintrustLogger.get() + .debug("Shutting down. Force-Flushing all otel data."); var result = CompletableResultCode.ofAll( // run all flushes in parallel. Should (rarely) @@ -143,10 +144,12 @@ public static void enable( TimeUnit .SECONDS)) .toList()); - BraintrustLogger.debug( - "otel shutdown complete. Flush done: %s, Flush successful: %s" - .formatted( - result.isDone(), result.isSuccess())); + BraintrustLogger.get() + .debug( + "otel shutdown complete. Flush done: %s, Flush successful: %s" + .formatted( + result.isDone(), + result.isSuccess())); })); } diff --git a/src/test/java/dev/braintrust/eval/EvalTest.java b/src/test/java/dev/braintrust/eval/EvalTest.java index 2a1f823..b7b76bc 100644 --- a/src/test/java/dev/braintrust/eval/EvalTest.java +++ b/src/test/java/dev/braintrust/eval/EvalTest.java @@ -31,7 +31,7 @@ public void evalOtelTraceWithProperAttributes() { var config = BraintrustConfig.of( "BRAINTRUST_API_KEY", "foobar", - "BRAINTRUST_TEST_JAVA_EXPORT_SPANS_IN_MEMORY", "true", + "BRAINTRUST_JAVA_EXPORT_SPANS_IN_MEMORY_FOR_UNIT_TEST", "true", "BRAINTRUST_API_URL", "https://api.braintrust.dev", "BRAINTRUST_APP_URL", "https://braintrust.dev", "BRAINTRUST_DEFAULT_PROJECT_NAME", projectName); diff --git a/src/test/java/dev/braintrust/trace/BraintrustTracingTest.java b/src/test/java/dev/braintrust/trace/BraintrustTracingTest.java index d3c3d4f..4b8f76f 100644 --- a/src/test/java/dev/braintrust/trace/BraintrustTracingTest.java +++ b/src/test/java/dev/braintrust/trace/BraintrustTracingTest.java @@ -25,7 +25,7 @@ public static Map> getExportedBraintrustSpans() { private final BraintrustConfig config = BraintrustConfig.of( "BRAINTRUST_API_KEY", "foobar", - "BRAINTRUST_TEST_JAVA_EXPORT_SPANS_IN_MEMORY", "true"); + "BRAINTRUST_JAVA_EXPORT_SPANS_IN_MEMORY_FOR_UNIT_TEST", "true"); @BeforeEach void beforeEach() { From ee500e5b808d5c5b43bdc0c1a139f7488446365c Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 30 Sep 2025 09:48:56 -0600 Subject: [PATCH 3/6] rm dead code in span processor --- .../trace/BraintrustSpanProcessor.java | 102 ------------------ 1 file changed, 102 deletions(-) diff --git a/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java b/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java index 4008593..9834949 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java +++ b/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java @@ -9,9 +9,6 @@ import io.opentelemetry.sdk.trace.ReadWriteSpan; import io.opentelemetry.sdk.trace.ReadableSpan; import io.opentelemetry.sdk.trace.SpanProcessor; -import io.opentelemetry.sdk.trace.data.SpanData; -import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; -import io.opentelemetry.sdk.trace.export.SpanExporter; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -28,21 +25,6 @@ class BraintrustSpanProcessor implements SpanProcessor { public static final AttributeKey PARENT = AttributeKey.stringKey(SdkSpec.Attributes.PARENT); - // Usage metrics - public static final AttributeKey USAGE_PROMPT_TOKENS = - AttributeKey.longKey("braintrust.usage.prompt_tokens"); - public static final AttributeKey USAGE_COMPLETION_TOKENS = - AttributeKey.longKey("braintrust.usage.completion_tokens"); - public static final AttributeKey USAGE_TOTAL_TOKENS = - AttributeKey.longKey("braintrust.usage.total_tokens"); - public static final AttributeKey USAGE_COST = - AttributeKey.doubleKey("braintrust.usage.cost"); - - // Scoring - public static final AttributeKey SCORE = AttributeKey.doubleKey("braintrust.score"); - public static final AttributeKey SCORE_NAME = - AttributeKey.stringKey("braintrust.score.name"); - private final BraintrustConfig config; private final SpanProcessor delegate; private final ConcurrentMap parentContexts = new ConcurrentHashMap<>(); @@ -168,88 +150,4 @@ public static ParentContext experiment(String experimentId) { return new ParentContext(null, experimentId, ParentType.EXPERIMENT); } } - - /** Builder for creating span processors with various configurations. */ - public static final class Builder { - private final BraintrustConfig config; - private SpanExporter exporter; - private boolean enableConsoleLog; - - public Builder(BraintrustConfig config) { - this.config = config; - this.enableConsoleLog = config.enableTraceConsoleLog(); - } - - public Builder withExporter(SpanExporter exporter) { - this.exporter = exporter; - return this; - } - - public Builder enableConsoleLog(boolean enable) { - this.enableConsoleLog = enable; - return this; - } - - public BraintrustSpanProcessor build() { - SpanProcessor baseProcessor = - SpanProcessor.composite(createProcessors().toArray(SpanProcessor[]::new)); - - return new BraintrustSpanProcessor(config, baseProcessor); - } - - private java.util.List createProcessors() { - var processors = new java.util.ArrayList(); - - if (exporter != null) { - processors.add(BatchSpanProcessor.builder(exporter).build()); - } - - if (enableConsoleLog) { - processors.add(createConsoleProcessor()); - } - - return processors; - } - - private SpanProcessor createConsoleProcessor() { - return new SpanProcessor() { - @Override - public void onStart(Context parentContext, ReadWriteSpan span) {} - - @Override - public boolean isStartRequired() { - return false; - } - - @Override - public void onEnd(ReadableSpan span) { - System.out.println(formatSpan(span.toSpanData())); - } - - @Override - public boolean isEndRequired() { - return true; - } - - @Override - public CompletableResultCode shutdown() { - return CompletableResultCode.ofSuccess(); - } - - @Override - public CompletableResultCode forceFlush() { - return CompletableResultCode.ofSuccess(); - } - - private String formatSpan(SpanData span) { - return String.format( - "[%s] %s (duration: %dms, attributes: %s)", - span.getKind(), - span.getName(), - (span.getEndEpochNanos() - span.getStartEpochNanos()) / 1_000_000, - span.getAttributes()); - } - }; - } - } } From 3e054d1323324223c389a1bed3912aa004d23e8a Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 30 Sep 2025 12:21:41 -0600 Subject: [PATCH 4/6] rm BraintrustLogger and just use slf4j This is standard java logging practice. The underlying logger of braintrust was slf4j anyways, so this simplifies things --- .../braintrust/api/BraintrustApiClient.java | 20 ++++----- .../otel/ChatCompletionEventsHelper.java | 7 +-- .../dev/braintrust/log/BraintrustLogger.java | 22 ---------- .../trace/BraintrustLogExporter.java | 13 +++--- .../trace/BraintrustSpanExporter.java | 13 +++--- .../trace/BraintrustSpanProcessor.java | 44 ++++++++----------- .../braintrust/trace/BraintrustTracing.java | 21 ++++----- 7 files changed, 53 insertions(+), 87 deletions(-) delete mode 100644 src/main/java/dev/braintrust/log/BraintrustLogger.java diff --git a/src/main/java/dev/braintrust/api/BraintrustApiClient.java b/src/main/java/dev/braintrust/api/BraintrustApiClient.java index 34ecbbe..50e5f10 100644 --- a/src/main/java/dev/braintrust/api/BraintrustApiClient.java +++ b/src/main/java/dev/braintrust/api/BraintrustApiClient.java @@ -6,7 +6,6 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import dev.braintrust.config.BraintrustConfig; -import dev.braintrust.log.BraintrustLogger; import java.io.IOException; import java.net.URI; import java.net.http.HttpClient; @@ -18,6 +17,7 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; +import lombok.extern.slf4j.Slf4j; public interface BraintrustApiClient { /** Creates or gets a project by name. */ @@ -39,6 +39,7 @@ static BraintrustApiClient of(BraintrustConfig config) { return new HttpImpl(config); } + @Slf4j class HttpImpl implements BraintrustApiClient { private final BraintrustConfig config; private final HttpClient httpClient; @@ -170,7 +171,7 @@ private CompletableFuture postAsync( } private CompletableFuture sendAsync(HttpRequest request, Class responseType) { - BraintrustLogger.get().debug("API Request: {} {}", request.method(), request.uri()); + log.debug("API Request: {} {}", request.method(), request.uri()); return httpClient .sendAsync(request, HttpResponse.BodyHandlers.ofString()) @@ -178,22 +179,20 @@ private CompletableFuture sendAsync(HttpRequest request, Class respons } private T handleResponse(HttpResponse response, Class responseType) { - BraintrustLogger.get() - .debug("API Response: {} - {}", response.statusCode(), response.body()); + log.debug("API Response: {} - {}", response.statusCode(), response.body()); if (response.statusCode() >= 200 && response.statusCode() < 300) { try { return objectMapper.readValue(response.body(), responseType); } catch (IOException e) { - BraintrustLogger.get().warn("Failed to parse response body", e); + log.warn("Failed to parse response body", e); throw new ApiException("Failed to parse response body", e); } } else { - BraintrustLogger.get() - .warn( - "API request failed with status {}: {}", - response.statusCode(), - response.body()); + log.warn( + "API request failed with status {}: {}", + response.statusCode(), + response.body()); throw new ApiException( String.format( "API request failed with status %d: %s", @@ -227,6 +226,7 @@ private static ObjectMapper createObjectMapper() { } /** Implementation for test doubling */ + @Slf4j class InMemoryImpl implements BraintrustApiClient { private final List organizationAndProjectInfos; private final Set experiments = diff --git a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java index acb773e..92b023a 100644 --- a/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java +++ b/src/main/java/dev/braintrust/instrumentation/openai/otel/ChatCompletionEventsHelper.java @@ -20,7 +20,6 @@ import com.openai.models.chat.completions.ChatCompletionSystemMessageParam; import com.openai.models.chat.completions.ChatCompletionToolMessageParam; import com.openai.models.chat.completions.ChatCompletionUserMessageParam; -import dev.braintrust.log.BraintrustLogger; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Value; import io.opentelemetry.api.logs.LogRecordBuilder; @@ -37,7 +36,9 @@ import java.util.Optional; import java.util.stream.Collectors; import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; +@Slf4j final class ChatCompletionEventsHelper { private static final AttributeKey EVENT_NAME = stringKey("event.name"); @@ -76,7 +77,7 @@ public static void emitPromptLogEvents( "braintrust.input_json", JSON_MAPPER.writeValueAsString(content)); } catch (JsonProcessingException e) { - BraintrustLogger.get().error("Error mapping json", e); + log.error("Error mapping json", e); } body.put("content", Value.of(content)); } @@ -200,7 +201,7 @@ public static void emitCompletionLogEvents( var completionJson = JSON_MAPPER.writeValueAsString(completion); Span.current().setAttribute("braintrust.output_json", completionJson); } catch (JsonProcessingException e) { - BraintrustLogger.get().error("Error mapping completion json", e); + log.error("Error mapping completion json", e); } for (ChatCompletion.Choice choice : completion.choices()) { ChatCompletionMessage choiceMsg = choice.message(); diff --git a/src/main/java/dev/braintrust/log/BraintrustLogger.java b/src/main/java/dev/braintrust/log/BraintrustLogger.java deleted file mode 100644 index a89b620..0000000 --- a/src/main/java/dev/braintrust/log/BraintrustLogger.java +++ /dev/null @@ -1,22 +0,0 @@ -package dev.braintrust.log; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Centralized logging for Braintrust SDK. Provides consistent logging with SDK-specific control and - * configuration. - */ -public final class BraintrustLogger { - private static final String LOGGER_NAME = "braintrust"; - - /** - * Get or create the braintrust logger - * - *

Note: this calls LoggerFactory which may initialize a global logger. Set your desired - * logging globals before calling this method. - */ - public static Logger get() { - return LoggerFactory.getLogger(LOGGER_NAME); - } -} diff --git a/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java b/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java index ecc54fc..cce5fdf 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java +++ b/src/main/java/dev/braintrust/trace/BraintrustLogExporter.java @@ -1,7 +1,6 @@ package dev.braintrust.trace; import dev.braintrust.config.BraintrustConfig; -import dev.braintrust.log.BraintrustLogger; import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.logs.data.LogRecordData; @@ -11,11 +10,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import lombok.extern.slf4j.Slf4j; /** * Custom log exporter for Braintrust that adds the x-bt-parent header dynamically based on log * attributes. */ +@Slf4j class BraintrustLogExporter implements LogRecordExporter { private final BraintrustConfig config; @@ -60,7 +61,7 @@ private CompletableResultCode exportWithParent(String parent, List= 1024) { - BraintrustLogger.get().info("Clearing exporter cache. This should not happen"); + log.info("Clearing exporter cache. This should not happen"); exporterCache.clear(); } var exporter = @@ -78,19 +79,17 @@ private CompletableResultCode exportWithParent(String parent, List> SPANS_EXPORTED = new ConcurrentHashMap<>(); @@ -63,7 +64,7 @@ private CompletableResultCode exportWithParent(String parent, List spa try { // Get or create exporter for this parent if (exporterCache.size() >= 1024) { - BraintrustLogger.get().info("Clearing exporter cache. This should not happen"); + log.info("Clearing exporter cache. This should not happen"); exporterCache.clear(); } var exporter = @@ -81,15 +82,13 @@ private CompletableResultCode exportWithParent(String parent, List spa // Add x-bt-parent header if we have a parent if (!p.isEmpty()) { exporterBuilder.addHeader("x-bt-parent", p); - BraintrustLogger.get() - .debug("Created exporter with x-bt-parent: {}", p); + log.debug("Created exporter with x-bt-parent: {}", p); } return exporterBuilder.build(); }); - BraintrustLogger.get() - .debug("Exporting {} spans with x-bt-parent: {}", spans.size(), parent); + log.debug("Exporting {} spans with x-bt-parent: {}", spans.size(), parent); if (config.exportSpansInMemoryForUnitTest()) { SPANS_EXPORTED.putIfAbsent(parent, new CopyOnWriteArrayList<>()); SPANS_EXPORTED.get(parent).addAll(spans); @@ -98,7 +97,7 @@ private CompletableResultCode exportWithParent(String parent, List spa return exporter.export(spans); } } catch (Exception e) { - BraintrustLogger.get().error("Failed to export spans", e); + log.error("Failed to export spans", e); return CompletableResultCode.ofFailure(); } } diff --git a/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java b/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java index 9834949..fb78db0 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java +++ b/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java @@ -1,7 +1,6 @@ package dev.braintrust.trace; import dev.braintrust.config.BraintrustConfig; -import dev.braintrust.log.BraintrustLogger; import dev.braintrust.spec.SdkSpec; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.context.Context; @@ -13,12 +12,14 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import javax.annotation.Nullable; +import lombok.extern.slf4j.Slf4j; import org.jetbrains.annotations.NotNull; /** * Custom span processor that enriches spans with Braintrust-specific attributes. Supports parent * assignment to projects or experiments. */ +@Slf4j class BraintrustSpanProcessor implements SpanProcessor { // Braintrust-specific attributes @@ -36,7 +37,7 @@ public BraintrustSpanProcessor(BraintrustConfig config, SpanProcessor delegate) @Override public void onStart(@NotNull Context parentContext, ReadWriteSpan span) { - BraintrustLogger.get().debug("OnStart: span={}, parent={}", span.getName(), parentContext); + log.debug("OnStart: span={}, parent={}", span.getName(), parentContext); // Check if span already has a parent attribute if (span.getAttribute(PARENT) == null) { @@ -48,11 +49,10 @@ public void onStart(@NotNull Context parentContext, ReadWriteSpan span) { .ifPresent( parentValue -> { span.setAttribute(PARENT, parentValue); - BraintrustLogger.get() - .debug( - "OnStart: set parent {} for span {}", - parentValue, - span.getName()); + log.debug( + "OnStart: set parent {} for span {}", + parentValue, + span.getName()); }); } else { btContext @@ -60,21 +60,14 @@ public void onStart(@NotNull Context parentContext, ReadWriteSpan span) { .ifPresent( id -> { span.setAttribute(PARENT, "project_id:" + id); - BraintrustLogger.get() - .debug( - "OnStart: set parent project {} from context", - id); + log.debug("OnStart: set parent project {} from context", id); }); btContext .experimentId() .ifPresent( id -> { span.setAttribute(PARENT, "experiment_id:" + id); - BraintrustLogger.get() - .debug( - "OnStart: set parent experiment {} from" - + " context", - id); + log.debug("OnStart: set parent experiment {} from context", id); }); } } @@ -122,16 +115,15 @@ public Optional getParentContext(String traceId) { private void logSpanDetails(ReadableSpan span) { var spanData = span.toSpanData(); - BraintrustLogger.get() - .debug( - "Span completed: name={}, traceId={}, spanId={}, duration={}ms," - + " attributes={}, events={}", - spanData.getName(), - spanData.getTraceId(), - spanData.getSpanId(), - (spanData.getEndEpochNanos() - spanData.getStartEpochNanos()) / 1_000_000, - spanData.getAttributes(), - spanData.getEvents()); + log.debug( + "Span completed: name={}, traceId={}, spanId={}, duration={}ms, attributes={}," + + " events={}", + spanData.getName(), + spanData.getTraceId(), + spanData.getSpanId(), + (spanData.getEndEpochNanos() - spanData.getStartEpochNanos()) / 1_000_000, + spanData.getAttributes(), + spanData.getEvents()); } /** Parent context for spans (project or experiment). */ diff --git a/src/main/java/dev/braintrust/trace/BraintrustTracing.java b/src/main/java/dev/braintrust/trace/BraintrustTracing.java index 6ba0f81..184cfc0 100644 --- a/src/main/java/dev/braintrust/trace/BraintrustTracing.java +++ b/src/main/java/dev/braintrust/trace/BraintrustTracing.java @@ -1,7 +1,6 @@ package dev.braintrust.trace; import dev.braintrust.config.BraintrustConfig; -import dev.braintrust.log.BraintrustLogger; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; @@ -23,11 +22,13 @@ import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; /** * Main entry point for Braintrust tracing setup. Provides convenient methods to initialize * OpenTelemetry with Braintrust configuration. */ +@Slf4j public final class BraintrustTracing { private static final String INSTRUMENTATION_NAME = "braintrust-java"; private static final String INSTRUMENTATION_VERSION = "0.0.1"; @@ -61,7 +62,7 @@ public static OpenTelemetry of(@Nonnull BraintrustConfig config, boolean registe .build(); if (registerGlobal) { GlobalOpenTelemetry.set(openTelemetry); - BraintrustLogger.get().debug("Registered OpenTelemetry globally"); + log.debug("Registered OpenTelemetry globally"); } return openTelemetry; } @@ -90,8 +91,7 @@ public static void enable( final Duration exportInterval = Duration.ofSeconds(5); final int maxQueueSize = 2048; final int maxExportBatchSize = 512; - BraintrustLogger.get() - .info("Initializing Braintrust OpenTelemetry with service={}", serviceName); + log.info("Initializing Braintrust OpenTelemetry with service={}", serviceName); // Create resource first so BraintrustSpanProcessor can access service.name var resourceBuilder = @@ -128,8 +128,7 @@ public static void enable( .addShutdownHook( new Thread( () -> { - BraintrustLogger.get() - .debug("Shutting down. Force-Flushing all otel data."); + log.debug("Shutting down. Force-Flushing all otel data."); var result = CompletableResultCode.ofAll( // run all flushes in parallel. Should (rarely) @@ -144,12 +143,10 @@ public static void enable( TimeUnit .SECONDS)) .toList()); - BraintrustLogger.get() - .debug( - "otel shutdown complete. Flush done: %s, Flush successful: %s" - .formatted( - result.isDone(), - result.isSuccess())); + log.debug( + "otel shutdown complete. Flush done: %s, Flush successful: %s" + .formatted( + result.isDone(), result.isSuccess())); })); } From d640c4f59ccd14de76cbf7ba29f92eccb1aa5093 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 30 Sep 2025 14:45:13 -0600 Subject: [PATCH 5/6] update and trim dependencies --- build.gradle | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index c57f871..7cd08cc 100644 --- a/build.gradle +++ b/build.gradle @@ -21,16 +21,13 @@ repositories { } ext { - otelVersion = '1.36.0' + otelVersion = '1.54.1' jacksonVersion = '2.16.1' - junitVersion = '5.10.1' - assertjVersion = '3.25.1' - mockitoVersion = '5.8.0' - slf4jVersion = '2.0.9' + junitVersion = '5.11.4' + slf4jVersion = '2.0.17' } dependencies { - // OpenTelemetry api "io.opentelemetry:opentelemetry-api:${otelVersion}" api "io.opentelemetry:opentelemetry-sdk:${otelVersion}" api "io.opentelemetry:opentelemetry-sdk-trace:${otelVersion}" @@ -38,30 +35,23 @@ dependencies { 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.25.0-alpha" + implementation "io.opentelemetry:opentelemetry-semconv:1.30.1-alpha" - // HTTP Client (Java 11+) - 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 "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}" - // Logging implementation "org.slf4j:slf4j-api:${slf4jVersion}" runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" - // Utilities - implementation 'com.google.code.findbugs:jsr305:3.0.2' // @Nullable annotations implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'com.google.code.findbugs:jsr305:3.0.2' // for @Nullable annotations - // AI SDKs (optional dependencies) - compileOnly 'com.openai:openai-java:2.8.1' // Official OpenAI SDK + compileOnly 'com.openai:openai-java:2.8.1' implementation "io.opentelemetry.instrumentation:opentelemetry-openai-java-1.1:2.19.0-alpha" - // compileOnly 'com.anthropic:anthropic-java:0.1.0' // Not yet available - // Testing testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" - testImplementation "org.assertj:assertj-core:${assertjVersion}" } test { From 9979f5b41b775230eeb5e43135a692d6db83705b Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 30 Sep 2025 15:07:20 -0600 Subject: [PATCH 6/6] feat: add SLF4J logging configuration for Braintrust examples Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) --- examples/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/examples/build.gradle b/examples/build.gradle index a0c5982..da37c7b 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -12,6 +12,8 @@ repositories { mavenCentral() } +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 @@ -33,6 +35,7 @@ task runSimpleOpenTelemetry(type: JavaExec) { description = 'Run the simple OpenTelemetry example' classpath = sourceSets.main.runtimeClasspath mainClass = 'dev.braintrust.examples.SimpleOpenTelemetryExample' + systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel } task runCustomOpenTelemetry(type: JavaExec) { @@ -40,6 +43,7 @@ task runCustomOpenTelemetry(type: JavaExec) { description = 'Run the custom OpenTelemetry example' classpath = sourceSets.main.runtimeClasspath mainClass = 'dev.braintrust.examples.CustomOpenTelemetryExample' + systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel } task runOpenAIInstrumentation(type: JavaExec) { @@ -47,6 +51,7 @@ task runOpenAIInstrumentation(type: JavaExec) { description = 'Run the OpenAI instrumentation example. NOTE: this requires OPENAI_API_KEY to be exported and will make a small call to openai, using your tokens' classpath = sourceSets.main.runtimeClasspath mainClass = 'dev.braintrust.examples.OpenAIInstrumentationExample' + systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel debugOptions { enabled = true port = 5566 @@ -60,6 +65,7 @@ task runExperiment(type: JavaExec) { description = 'Run the experiment example' classpath = sourceSets.main.runtimeClasspath mainClass = 'dev.braintrust.examples.ExperimentExample' + systemProperty 'org.slf4j.simpleLogger.log.dev.braintrust', braintrustLogLevel debugOptions { enabled = true port = 5566