Skip to content

Commit c96bbad

Browse files
committed
wip
1 parent ac55ac7 commit c96bbad

6 files changed

Lines changed: 94 additions & 109 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package dev.braintrust.trace;
2+
3+
import io.opentelemetry.sdk.trace.ReadableSpan;
4+
import java.util.List;
5+
6+
/**
7+
* A filter that decides whether a finalized span should be exported. Samplers are evaluated in
8+
* {@link BraintrustSpanProcessor#onEnd} after all attributes have been set on the span.
9+
*/
10+
interface BraintrustSampler {
11+
/** Returns {@code true} if the span should be exported, {@code false} to discard it. */
12+
boolean sample(ReadableSpan span);
13+
14+
/** Keeps only spans that have at least one attribute with a known AI instrumentation prefix. */
15+
class FilterAISpans implements BraintrustSampler {
16+
static final List<String> AI_ATTR_PREFIXES =
17+
List.of("gen_ai.", "braintrust.", "llm.", "ai.", "traceloop.");
18+
19+
@Override
20+
public boolean sample(ReadableSpan span) {
21+
var attributes = span.toSpanData().getAttributes();
22+
for (var key : attributes.asMap().keySet()) {
23+
var keyName = key.getKey();
24+
// Skip internal attributes injected by the span processor itself
25+
if (keyName.equals(BraintrustTracing.PARENT_KEY)) {
26+
continue;
27+
}
28+
for (var prefix : AI_ATTR_PREFIXES) {
29+
if (keyName.startsWith(prefix)) {
30+
return true;
31+
}
32+
}
33+
}
34+
return false;
35+
}
36+
}
37+
}

braintrust-sdk/src/main/java/dev/braintrust/trace/BraintrustSpanProcessor.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import io.opentelemetry.sdk.trace.ReadWriteSpan;
88
import io.opentelemetry.sdk.trace.ReadableSpan;
99
import io.opentelemetry.sdk.trace.SpanProcessor;
10+
import java.util.List;
1011
import java.util.Optional;
1112
import java.util.concurrent.ConcurrentHashMap;
1213
import java.util.concurrent.ConcurrentMap;
@@ -26,11 +27,21 @@ public class BraintrustSpanProcessor implements SpanProcessor {
2627

2728
private final BraintrustConfig config;
2829
private final SpanProcessor delegate;
30+
private final List<BraintrustSampler> samplers;
2931
private final ConcurrentMap<String, ParentContext> parentContexts = new ConcurrentHashMap<>();
3032

3133
BraintrustSpanProcessor(BraintrustConfig config, SpanProcessor delegate) {
3234
this.config = config;
3335
this.delegate = delegate;
36+
this.samplers = buildSamplers(config);
37+
}
38+
39+
private static List<BraintrustSampler> buildSamplers(BraintrustConfig config) {
40+
var samplers = new java.util.ArrayList<BraintrustSampler>();
41+
if (config.filterAISpans()) {
42+
samplers.add(new BraintrustSampler.FilterAISpans());
43+
}
44+
return List.copyOf(samplers);
3445
}
3546

3647
@Override
@@ -93,12 +104,21 @@ public void onEnd(ReadableSpan span) {
93104
if (config.debug()) {
94105
logSpanDetails(span);
95106
}
107+
for (var sampler : samplers) {
108+
if (!sampler.sample(span)) {
109+
log.debug(
110+
"Span filtered by {}: {}",
111+
sampler.getClass().getSimpleName(),
112+
span.getName());
113+
return;
114+
}
115+
}
96116
delegate.onEnd(span);
97117
}
98118

99119
@Override
100120
public boolean isEndRequired() {
101-
return delegate.isEndRequired();
121+
return true;
102122
}
103123

104124
@Override

braintrust-sdk/src/main/java/dev/braintrust/trace/BraintrustTracing.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,6 @@ public static void enable(
117117
.setMaxExportBatchSize(maxExportBatchSize)
118118
.build());
119119
tracerProviderBuilder.addResource(resource).addSpanProcessor(spanProcessor);
120-
if (config.filterAISpans()) {
121-
tracerProviderBuilder.setSampler(Samplers.SAMPLE_AI_SPANS_ONLY);
122-
}
123120
// logs
124121
var logProcessor =
125122
BatchLogRecordProcessor.builder(new BraintrustLogExporter(config))

braintrust-sdk/src/main/java/dev/braintrust/trace/Samplers.java

Lines changed: 0 additions & 39 deletions
This file was deleted.

braintrust-sdk/src/test/java/dev/braintrust/trace/BraintrustTracingTest.java

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33
import static org.junit.jupiter.api.Assertions.*;
44

55
import dev.braintrust.TestHarness;
6+
import dev.braintrust.UnitTestSpanExporter;
7+
import dev.braintrust.config.BraintrustConfig;
68
import io.opentelemetry.api.common.AttributeKey;
79
import io.opentelemetry.api.trace.Tracer;
10+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
11+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
12+
import java.util.concurrent.TimeUnit;
813
import org.junit.jupiter.api.BeforeEach;
914
import org.junit.jupiter.api.Test;
1015

@@ -66,25 +71,42 @@ void spanProcessorAddsParentFromConfig() {
6671

6772
@Test
6873
void filterAISpansDropsNonAISpans() {
69-
testHarness = TestHarness.setup(configBuilder -> configBuilder.filterAISpans(true));
74+
// Build a tracer where the BraintrustSpanProcessor wraps a test exporter,
75+
// so the filter in onEnd() gates what the exporter receives.
76+
var config =
77+
BraintrustConfig.builder()
78+
.apiKey("test-key")
79+
.apiUrl("http://localhost:1234")
80+
.filterAISpans(true)
81+
.build();
82+
83+
var spanExporter = new UnitTestSpanExporter();
84+
var processor =
85+
new BraintrustSpanProcessor(config, SimpleSpanProcessor.create(spanExporter));
86+
var tracerProvider = SdkTracerProvider.builder().addSpanProcessor(processor).build();
87+
var tracer = tracerProvider.get("test");
88+
89+
// Non-AI span — should be filtered out
90+
var nonAiSpan = tracer.spanBuilder("http-call").startSpan();
91+
nonAiSpan.setAttribute("http.method", "GET");
92+
nonAiSpan.end();
7093

71-
var tracer = testHarness.openTelemetry().getTracer("unit test");
94+
// Span with no attributes — should be filtered out
95+
var emptySpan = tracer.spanBuilder("empty-span").startSpan();
96+
emptySpan.end();
7297

73-
// Emit a non-AI span — should be dropped by the sampler
74-
var nonAiSpan =
75-
tracer.spanBuilder("http-call")
76-
.setAttribute(AttributeKey.stringKey("http.method"), "GET")
77-
.startSpan();
78-
nonAiSpan.end();
98+
// Substring non-match — should be filtered out
99+
var substringSpan = tracer.spanBuilder("not-ai").startSpan();
100+
substringSpan.setAttribute("zbraintrust.foo", "bar");
101+
substringSpan.end();
79102

80-
// Emit an AI span — should pass the sampler
81-
var aiSpan =
82-
tracer.spanBuilder("llm-call")
83-
.setAttribute(AttributeKey.stringKey("gen_ai.model"), "gpt-4")
84-
.startSpan();
103+
// AI span with attribute set AFTER startSpan — should pass the filter
104+
var aiSpan = tracer.spanBuilder("llm-call").startSpan();
105+
aiSpan.setAttribute("gen_ai.model", "gpt-4");
85106
aiSpan.end();
86107

87-
var spans = testHarness.awaitExportedSpans();
108+
tracerProvider.forceFlush().join(5, TimeUnit.SECONDS);
109+
var spans = spanExporter.getFinishedSpanItems();
88110

89111
assertEquals(1, spans.size(), "Only the AI span should be exported");
90112
assertEquals("llm-call", spans.get(0).getName());

braintrust-sdk/src/test/java/dev/braintrust/trace/SamplersTest.java

Lines changed: 0 additions & 52 deletions
This file was deleted.

0 commit comments

Comments
 (0)