Summary
wrap_stream_with_span has two related gaps in its span lifecycle management:
- Stream errors are not logged: When the underlying stream yields an
Err(E) item, no error information is recorded on the span. The error is passed through to the consumer silently from the span's perspective.
- Span never ends on cancellation:
SpanCompleteWrapper has no Drop implementation. When the wrapped stream is dropped before exhaustion (e.g. timeout, consumer cancellation, early break), finalize_span is never called — the span stays permanently open with no end time and no aggregated output.
Both gaps mean that failed or cancelled streaming LLM calls produce incomplete, misleading traces in Braintrust.
What is missing
1. Error logging
In src/stream.rs, the logged_stream closure in wrap_stream_with_span only processes Ok items:
// src/stream.rs (inside wrap_stream_with_span)
let logged_stream = stream.then(move |result| {
async move {
if let Ok(ref value) = result { // ← Err items are silently ignored
// ... TTFT, accumulation, periodic flush
}
result
}
});
When the stream returns an Err(E) (e.g. a network error mid-stream), nothing is logged to the span. Other Braintrust SDKs (TypeScript, Python) use try/catch/finally patterns to log error details to the span before re-raising.
2. Span never ends on cancellation
SpanCompleteWrapper only triggers finalization when Poll::Ready(None) is returned from the inner stream:
// src/stream.rs — SpanCompleteWrapper::poll_next
if matches!(result, Poll::Ready(None)) {
if let (Some(span), Some(aggregator)) = (this.span.take(), this.aggregator.take()) {
let fut = Box::pin(finalize_span(span, aggregator));
this.finalize_state = FinalizeState::Finalizing(fut);
// ...
}
}
SpanCompleteWrapper has no Drop implementation. In Rust, dropping a Stream does not poll it to completion. So any of the following scenarios leaves the span permanently open:
- The consumer breaks out of a loop early after receiving enough output
- A request timeout fires and the task owning the stream is cancelled
- The stream is dropped after a mid-stream error (
Err(E)) without being further consumed to None
- Any other scenario where the wrapped stream is dropped before exhaustion
In all these cases finalize_span (which calls span.end() and span.flush()) is never reached. The span has no end time and no final output/usage metrics logged.
Impact
- Errors are invisible: A streaming call that fails halfway through shows no error in Braintrust traces. The partial span looks identical to a span where logging was simply never started.
- Open spans: Spans that are never ended can interfere with cost/duration calculations and clutter the Braintrust UI with permanently-open traces.
- Partial output lost: Any chunks that were accumulated before cancellation or error are never submitted; the span shows no output at all.
Braintrust docs status
supported — Braintrust's tracing documentation states that spans automatically capture "Errors and exceptions." Other Braintrust SDKs implement this for streaming via try/finally (TypeScript) and context-manager exit handlers (Python) to guarantee the span is ended regardless of success, error, or cancellation.
Upstream sources
- Rust
Stream trait: dropping a Stream does not poll it; Drop must be implemented explicitly for cleanup — https://doc.rust-lang.org/std/stream/index.html
SpanCompleteWrapper struct definition: src/stream.rs (search for struct SpanCompleteWrapper)
FinalizeState enum and finalize_span function: src/stream.rs
Local files inspected
src/stream.rs:1026–1113 — wrap_stream_with_span function; logged_stream only handles Ok items
src/stream.rs:1152–1158 — SpanCompleteWrapper struct; no Drop impl
src/stream.rs:1161–1205 — finalize_span async function; only reachable from poll_next on Poll::Ready(None)
src/stream.rs:1207–1261 — SpanCompleteWrapper::poll_next; finalization triggered only by Poll::Ready(None), not by error items or drop
src/span.rs:399–427 — SpanHandle::end and SpanHandle::flush — need to be called to close a span
Summary
wrap_stream_with_spanhas two related gaps in its span lifecycle management:Err(E)item, no error information is recorded on the span. The error is passed through to the consumer silently from the span's perspective.SpanCompleteWrapperhas noDropimplementation. When the wrapped stream is dropped before exhaustion (e.g. timeout, consumer cancellation, early break),finalize_spanis never called — the span stays permanently open with no end time and no aggregated output.Both gaps mean that failed or cancelled streaming LLM calls produce incomplete, misleading traces in Braintrust.
What is missing
1. Error logging
In
src/stream.rs, thelogged_streamclosure inwrap_stream_with_spanonly processesOkitems:When the stream returns an
Err(E)(e.g. a network error mid-stream), nothing is logged to the span. Other Braintrust SDKs (TypeScript, Python) use try/catch/finally patterns to log error details to the span before re-raising.2. Span never ends on cancellation
SpanCompleteWrapperonly triggers finalization whenPoll::Ready(None)is returned from the inner stream:SpanCompleteWrapperhas noDropimplementation. In Rust, dropping aStreamdoes not poll it to completion. So any of the following scenarios leaves the span permanently open:Err(E)) without being further consumed toNoneIn all these cases
finalize_span(which callsspan.end()andspan.flush()) is never reached. The span has no end time and no final output/usage metrics logged.Impact
Braintrust docs status
supported — Braintrust's tracing documentation states that spans automatically capture "Errors and exceptions." Other Braintrust SDKs implement this for streaming via try/finally (TypeScript) and context-manager exit handlers (Python) to guarantee the span is ended regardless of success, error, or cancellation.
Upstream sources
Streamtrait: dropping aStreamdoes not poll it;Dropmust be implemented explicitly for cleanup — https://doc.rust-lang.org/std/stream/index.htmlSpanCompleteWrapperstruct definition:src/stream.rs(search forstruct SpanCompleteWrapper)FinalizeStateenum andfinalize_spanfunction:src/stream.rsLocal files inspected
src/stream.rs:1026–1113—wrap_stream_with_spanfunction;logged_streamonly handlesOkitemssrc/stream.rs:1152–1158—SpanCompleteWrapperstruct; noDropimplsrc/stream.rs:1161–1205—finalize_spanasync function; only reachable frompoll_nextonPoll::Ready(None)src/stream.rs:1207–1261—SpanCompleteWrapper::poll_next; finalization triggered only byPoll::Ready(None), not by error items or dropsrc/span.rs:399–427—SpanHandle::endandSpanHandle::flush— need to be called to close a span