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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import dev.braintrust.Braintrust;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.exporter.otlp.http.logs.OtlpHttpLogRecordExporter;
import io.opentelemetry.exporter.otlp.http.metrics.OtlpHttpMetricExporter;
import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter;
Expand Down Expand Up @@ -53,11 +57,18 @@ public static void main(String[] args) throws Exception {
var braintrust = Braintrust.get();
braintrust.openTelemetryEnable(tracerBuilder, loggerBuilder, meterBuilder);

// context propagation is required only if you wish to see distributed traces in Braintrust
var contextPropagator =
ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance()));
var openTelemetry =
OpenTelemetrySdk.builder()
.setTracerProvider(tracerBuilder.build())
.setLoggerProvider(loggerBuilder.build())
.setMeterProvider(meterBuilder.build())
.setPropagators(contextPropagator)
.build();
GlobalOpenTelemetry.set(openTelemetry);
registerShutdownHook(openTelemetry);
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/dev/braintrust/BraintrustUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import dev.braintrust.api.BraintrustApiClient;
import java.net.URI;
import java.net.URISyntaxException;
import javax.annotation.Nonnull;

public class BraintrustUtils {
/** construct a URI to link to a specific braintrust project within an org */
Expand All @@ -26,4 +27,19 @@ public static URI createProjectURI(
throw new RuntimeException(e);
}
}

static Parent parseParent(@Nonnull String parentStr) {
String[] parts = parentStr.split(":");
if (parts.length != 2) {
throw new IllegalArgumentException("Invalid parent format: " + parentStr);
}
return new Parent(parts[0], parts[1]);
}

/** Represents a parsed parent with type and ID. */
public record Parent(String type, String id) {
public String toParentValue() {
return type + ":" + id;
}
}
}
55 changes: 54 additions & 1 deletion src/main/java/dev/braintrust/trace/BraintrustContext.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
package dev.braintrust.trace;

import dev.braintrust.BraintrustUtils;
import io.opentelemetry.api.baggage.Baggage;
import io.opentelemetry.api.baggage.BaggageBuilder;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.ContextKey;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;

/**
* Used to identify the braintrust parent for spans and experiments. SDK users probably don't want
* to use this and instead should use {@link BraintrustTracing} or {@link dev.braintrust.eval.Eval}
*/
@Slf4j
public final class BraintrustContext {
private static final ContextKey<BraintrustContext> KEY = ContextKey.named("braintrust-context");

Expand All @@ -28,7 +33,55 @@ private BraintrustContext(@Nullable String projectId, @Nullable String experimen
public static Context ofExperiment(@Nonnull String experimentId, @Nonnull Span span) {
Objects.requireNonNull(experimentId);
Objects.requireNonNull(span);
return Context.current().with(span).with(KEY, new BraintrustContext(null, experimentId));
Context ctx =
Context.current().with(span).with(KEY, new BraintrustContext(null, experimentId));
return setParentInBaggage(ctx, "experiment_id", experimentId);
}

/**
* Sets the parent in baggage for distributed tracing.
*
* <p>Baggage propagates automatically via W3C headers when propagators are configured, allowing
* parent context to flow across process boundaries.
*
* @param ctx the context to update
* @param parentType the type of parent (e.g., "experiment_id", "project_name")
* @param parentId the ID of the parent
* @return updated context with baggage set
*/
static Context setParentInBaggage(
@Nonnull Context ctx, @Nonnull String parentType, @Nonnull String parentId) {
try {
String parentValue = (new BraintrustUtils.Parent(parentType, parentId)).toParentValue();
Baggage existingBaggage = Baggage.fromContext(ctx);
BaggageBuilder builder = existingBaggage.toBuilder();
builder.put(BraintrustTracing.PARENT_KEY, parentValue);
return ctx.with(builder.build());
} catch (Exception e) {
log.warn("Failed to set parent in baggage: {}", e.getMessage(), e);
return ctx;
}
}

/**
* Retrieves the parent value from baggage for distributed tracing.
*
* <p>This method checks the OpenTelemetry Baggage for the braintrust.parent attribute. This is
* used as a fallback when parent information is not available in the Context (e.g., when
* crossing process boundaries).
*
* @param ctx the context to check
* @return the parent value if present in baggage (format: "type:id")
*/
static Optional<String> getParentFromBaggage(@Nonnull Context ctx) {
try {
Baggage baggage = Baggage.fromContext(ctx);
String parentValue = baggage.getEntryValue(BraintrustTracing.PARENT_KEY);
return Optional.ofNullable(parentValue).filter(s -> !s.isEmpty());
} catch (Exception e) {
log.warn("Failed to get parent from baggage: {}", e.getMessage(), e);
return Optional.empty();
}
}

/** Retrieves a BraintrustContext from the given Context. */
Expand Down
30 changes: 20 additions & 10 deletions src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,16 +42,26 @@ public void onStart(@NotNull Context parentContext, ReadWriteSpan span) {
// Check if parent context has Braintrust attributes first
var btContext = BraintrustContext.fromContext(parentContext);
if (btContext == null) {
// Get parent from the config if otel doesn't have it
config.getBraintrustParentValue()
.ifPresent(
parentValue -> {
span.setAttribute(PARENT, parentValue);
log.debug(
"OnStart: set parent {} for span {}",
parentValue,
span.getName());
});
// Check baggage for distributed tracing (cross-process parent propagation)
var parentFromBaggage = BraintrustContext.getParentFromBaggage(parentContext);
if (parentFromBaggage.isPresent()) {
span.setAttribute(PARENT, parentFromBaggage.get());
log.debug(
"OnStart: set parent {} from baggage for span {}",
parentFromBaggage.get(),
span.getName());
} else {
// Get parent from the config if otel doesn't have it
config.getBraintrustParentValue()
.ifPresent(
parentValue -> {
span.setAttribute(PARENT, parentValue);
log.debug(
"OnStart: set parent {} for span {}",
parentValue,
span.getName());
});
}
} else {
btContext
.projectId()
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/dev/braintrust/trace/BraintrustTracing.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import dev.braintrust.config.BraintrustConfig;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.common.CompletableResultCode;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
Expand Down Expand Up @@ -54,12 +58,18 @@ public static OpenTelemetry of(@Nonnull BraintrustConfig config, boolean registe
var tracerBuilder = SdkTracerProvider.builder();
var loggerBuilder = SdkLoggerProvider.builder();
var meterBuilder = SdkMeterProvider.builder();
var contextPropagator =
ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance()));
enable(config, tracerBuilder, loggerBuilder, meterBuilder);
var openTelemetry =
OpenTelemetrySdk.builder()
.setTracerProvider(tracerBuilder.build())
.setLoggerProvider(loggerBuilder.build())
.setMeterProvider(meterBuilder.build())
.setPropagators(contextPropagator)
.build();
if (registerGlobal) {
GlobalOpenTelemetry.set(openTelemetry);
Expand Down
25 changes: 25 additions & 0 deletions src/test/java/dev/braintrust/BraintrustUtilsTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dev.braintrust;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import dev.braintrust.api.BraintrustApiClient;
import java.net.URI;
Expand All @@ -18,4 +19,28 @@ public void testBuildProjectUri() {
URI.create("http://someserver:3009/app/some%20org/p/some%20project"),
BraintrustUtils.createProjectURI("http://someserver:3009/", orgAndProject));
}

@Test
void testParseParent() {
var parsed1 = BraintrustUtils.parseParent("experiment_id:abc123");
assertEquals("experiment_id", parsed1.type());
assertEquals("abc123", parsed1.id());

var parsed2 = BraintrustUtils.parseParent("project_name:my-project");
assertEquals("project_name", parsed2.type());
assertEquals("my-project", parsed2.id());

assertThrows(
Exception.class,
() -> BraintrustUtils.parseParent("invalid-no-colon"),
"Should throw on invalid format");
assertThrows(
Exception.class,
() -> BraintrustUtils.parseParent("invalid:too:many:colons"),
"Should throw on invalid format");
assertThrows(
Exception.class,
() -> BraintrustUtils.parseParent(""),
"Should throw on empty string");
}
}
10 changes: 10 additions & 0 deletions src/test/java/dev/braintrust/TestHarness.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import dev.braintrust.config.BraintrustConfig;
import dev.braintrust.prompt.BraintrustPromptLoader;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.api.baggage.propagation.W3CBaggagePropagator;
import io.opentelemetry.api.trace.propagation.W3CTraceContextPropagator;
import io.opentelemetry.context.propagation.ContextPropagators;
import io.opentelemetry.context.propagation.TextMapPropagator;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.logs.SdkLoggerProvider;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
Expand Down Expand Up @@ -81,11 +85,17 @@ private TestHarness(@Nonnull Braintrust braintrust) {
braintrust.openTelemetryEnable(tracerBuilder, loggerBuilder, meterBuilder);
// Add the in-memory span exporter for testing
tracerBuilder.addSpanProcessor(SimpleSpanProcessor.create(this.spanExporter));
var contextPropagator =
ContextPropagators.create(
TextMapPropagator.composite(
W3CTraceContextPropagator.getInstance(),
W3CBaggagePropagator.getInstance()));
var openTelemetry =
OpenTelemetrySdk.builder()
.setTracerProvider(tracerBuilder.build())
.setLoggerProvider(loggerBuilder.build())
.setMeterProvider(meterBuilder.build())
.setPropagators(contextPropagator)
.build();
this.openTelemetry = openTelemetry;
}
Expand Down
Loading