Skip to content

[bot] wrap_stream_with_span does not log stream errors or end the span on early cancellation #57

@braintrust-bot

Description

@braintrust-bot

Summary

wrap_stream_with_span has two related gaps in its span lifecycle management:

  1. 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.
  2. 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–1113wrap_stream_with_span function; logged_stream only handles Ok items
  • src/stream.rs:1152–1158SpanCompleteWrapper struct; no Drop impl
  • src/stream.rs:1161–1205finalize_span async function; only reachable from poll_next on Poll::Ready(None)
  • src/stream.rs:1207–1261SpanCompleteWrapper::poll_next; finalization triggered only by Poll::Ready(None), not by error items or drop
  • src/span.rs:399–427SpanHandle::end and SpanHandle::flush — need to be called to close a span

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions