Skip to content

Commit 6958f46

Browse files
grahamcclaude
andcommitted
feat(tracing): add InstrumentedStream span combinator to sqlx-core
Review finding #1 (shared mechanism): tracing 0.1's `Instrumented` only implements `Future`, not `Stream`, so a `#[tracing::instrument]` on an async fn that returns a stream closes its span before the stream is ever polled. Add a small pin-projected `InstrumentedStream` adapter and `InstrumentStream` extension trait that enters a `Span` on every `poll_next`, letting a span cover the full lifetime of a streamed query. pin-project-lite is already in the dependency tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 253e7f7 commit 6958f46

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

sqlx-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ futures-util = { version = "0.3.32", default-features = false, features = ["allo
8888
log = { version = "0.4.18", default-features = false }
8989
memchr = { version = "2.5.0", default-features = false }
9090
percent-encoding = "2.3.0"
91+
pin-project-lite = "0.2.16"
9192
serde = { version = "1.0.219", features = ["derive", "rc"], optional = true }
9293
serde_json = { version = "1.0.142", features = ["raw_value"], optional = true }
9394
toml = { version = "0.8.16", optional = true }

sqlx-core/src/instrument_stream.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! Attach a [`tracing::Span`] to a [`Stream`] so the span stays open for the
2+
//! whole stream rather than just its construction.
3+
4+
use std::pin::Pin;
5+
use std::task::{Context, Poll};
6+
7+
use futures_core::Stream;
8+
use pin_project_lite::pin_project;
9+
use tracing::Span;
10+
11+
pin_project! {
12+
/// A [`Stream`] adapter that enters `span` for the duration of every
13+
/// [`poll_next`](Stream::poll_next).
14+
///
15+
/// A plain `#[tracing::instrument]` on an `async fn` that *returns* a stream
16+
/// only keeps its span entered while the stream is being built; the span is
17+
/// then closed before the caller ever polls the stream. Wrapping the
18+
/// returned stream with this adapter instead keeps the span open across row
19+
/// fetching, so e.g. a `sqlx::query` span measures the whole query rather
20+
/// than just its setup.
21+
pub struct InstrumentedStream<S> {
22+
#[pin]
23+
stream: S,
24+
span: Span,
25+
}
26+
}
27+
28+
impl<S: Stream> Stream for InstrumentedStream<S> {
29+
type Item = S::Item;
30+
31+
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
32+
let this = self.project();
33+
let _entered = this.span.enter();
34+
this.stream.poll_next(cx)
35+
}
36+
37+
fn size_hint(&self) -> (usize, Option<usize>) {
38+
self.stream.size_hint()
39+
}
40+
}
41+
42+
/// Extension trait for attaching a [`Span`] to a [`Stream`].
43+
pub trait InstrumentStream: Stream + Sized {
44+
/// Wrap this stream so that `span` is entered every time it is polled.
45+
///
46+
/// See [`InstrumentedStream`].
47+
fn instrument_stream(self, span: Span) -> InstrumentedStream<Self> {
48+
InstrumentedStream { stream: self, span }
49+
}
50+
}
51+
52+
impl<S: Stream> InstrumentStream for S {}

sqlx-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub mod executor;
6767
pub mod from_row;
6868
pub mod fs;
6969
pub mod io;
70+
pub mod instrument_stream;
7071
pub mod logger;
7172
pub mod net;
7273
pub mod query_as;

0 commit comments

Comments
 (0)