From 31a54c71f27e92932430a150eb5ec600481000b6 Mon Sep 17 00:00:00 2001 From: Sean McArthur Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 1/8] feat(rt/tracing): introduce `CurrentSpanExecutor` this commit introduces a `CurrentSpanExecutor`, and a unit test demonstrating that it works as expected. this is a wrapper implementing `hyper::rt::Execute`, which propagates tracing spans to its spawned tasks. Co-authored-by: katelyn martin Signed-off-by: katelyn martin --- src/rt/mod.rs | 5 +++ src/rt/tracing.rs | 94 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/rt/tracing.rs diff --git a/src/rt/mod.rs b/src/rt/mod.rs index 71363ccd..71a9d09d 100644 --- a/src/rt/mod.rs +++ b/src/rt/mod.rs @@ -1,5 +1,10 @@ //! Runtime utilities +#[cfg(feature = "tracing")] +mod tracing; +#[cfg(feature = "tracing")] +pub use self::tracing::CurrentSpanExecutor; + #[cfg(feature = "client-legacy")] mod io; #[cfg(feature = "client-legacy")] diff --git a/src/rt/tracing.rs b/src/rt/tracing.rs new file mode 100644 index 00000000..acb27e8f --- /dev/null +++ b/src/rt/tracing.rs @@ -0,0 +1,94 @@ +use hyper::rt::Executor; +use tracing::instrument::{Instrument, Instrumented}; + +/// An executor that propagates the current tracing span to its futures. +/// +/// The span is captured when [`execute`](Executor::execute) is called, and is +/// entered each time the future is polled or dropped. Execution is delegated to +/// the wrapped executor, without requiring a particular runtime. +/// +/// Requires the `tracing` feature. +/// +/// # Example +/// +/// ``` +/// # #[cfg(feature = "tokio")] +/// # { +/// use hyper_util::rt::{TokioExecutor, CurrentSpanExecutor}; +/// +/// let executor = CurrentSpanExecutor::new(TokioExecutor::new()); +/// # } +/// ``` +#[derive(Clone, Copy, Debug, Default)] +pub struct CurrentSpanExecutor { + inner: E, +} + +impl CurrentSpanExecutor { + /// Wrap an executor to propagate the current tracing span to its futures. + pub fn new(inner: E) -> Self { + Self { inner } + } +} + +impl Executor for CurrentSpanExecutor +where + E: Executor>, + F: Future, +{ + fn execute(&self, future: F) { + self.inner.execute(future.in_current_span()); + } +} + +#[cfg(test)] +mod tests { + use super::CurrentSpanExecutor; + use hyper::rt::Executor; + use std::{cell::RefCell, future::poll_fn, pin::Pin, task::Poll}; + + #[derive(Default)] + struct DeferredExecutor<'a> { + future: RefCell + 'a>>>>, + } + + impl<'a, F: Future + 'a> Executor for &DeferredExecutor<'a> { + fn execute(&self, future: F) { + *self.future.borrow_mut() = Some(Box::pin(future)); + } + } + + #[test] + fn propagates_span_from_execute_on_each_poll() { + let _subscriber = tracing::subscriber::set_default(tracing_subscriber::registry()); + let construction_span = tracing::info_span!("construction"); + let execution_span = tracing::info_span!("execution"); + let polling_span = tracing::info_span!("polling"); + assert!(execution_span.id().is_some()); + + // Borrowing a local executor and future also checks that the wrapper + // does not impose Send or 'static bounds on the inner executor. + let polls = RefCell::new(0); + let inner = DeferredExecutor::default(); + let executor = construction_span.in_scope(|| CurrentSpanExecutor::new(&inner)); + execution_span.in_scope(|| { + executor.execute(poll_fn(|_| { + assert_eq!(tracing::Span::current().id(), execution_span.id()); + *polls.borrow_mut() += 1; + if *polls.borrow() == 1 { + Poll::Pending + } else { + Poll::Ready(()) + } + })); + }); + + let _entered = polling_span.enter(); + let mut task = tokio_test::task::spawn(inner.future.borrow_mut().take().unwrap()); + assert!(task.poll().is_pending()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert!(task.poll().is_ready()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert_eq!(*polls.borrow(), 2); + } +} From 87887176adaccbdb70893be7f150d97499f09413 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Fri, 18 Sep 2026 00:00:00 +0000 Subject: [PATCH 2/8] feat(rt/tracing): introduce `WithSpanExecutor` see https://github.com/hyperium/hyper-util/pull/322 for more information. this commit introduces an additional `Executor` implementation to accompany the `CurrentSpanExecutor` that executes spawned futures within the current span at time of execution. this would provide an alternative for users that wish to provide tracing information, but do not want to run background futures in the current span, which can interfere with some observability systems. Signed-off-by: katelyn martin --- src/rt/mod.rs | 2 +- src/rt/tracing.rs | 92 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/rt/mod.rs b/src/rt/mod.rs index 71a9d09d..28f0c2b8 100644 --- a/src/rt/mod.rs +++ b/src/rt/mod.rs @@ -3,7 +3,7 @@ #[cfg(feature = "tracing")] mod tracing; #[cfg(feature = "tracing")] -pub use self::tracing::CurrentSpanExecutor; +pub use self::tracing::{CurrentSpanExecutor, WithSpanExecutor}; #[cfg(feature = "client-legacy")] mod io; diff --git a/src/rt/tracing.rs b/src/rt/tracing.rs index acb27e8f..f6176446 100644 --- a/src/rt/tracing.rs +++ b/src/rt/tracing.rs @@ -1,5 +1,8 @@ use hyper::rt::Executor; -use tracing::instrument::{Instrument, Instrumented}; +use tracing::{ + Span, + instrument::{Instrument, Instrumented}, +}; /// An executor that propagates the current tracing span to its futures. /// @@ -24,6 +27,33 @@ pub struct CurrentSpanExecutor { inner: E, } +/// An executor that propagates a provided tracing span to its futures. +/// +/// The span provided to this executor is entered each time the future is +/// polled or dropped. Execution is delegated to the wrapped executor, without +/// requiring a particular runtime. +/// +/// Requires the `tracing` feature. +/// +/// # Example +/// +/// ``` +/// # #[cfg(feature = "tokio")] +/// # { +/// use hyper_util::rt::{TokioExecutor, WithSpanExecutor}; +/// +/// let span = tracing::info_span!("example"); +/// let executor = WithSpanExecutor::new(TokioExecutor::new(), span); +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct WithSpanExecutor { + inner: E, + span: Span, +} + +// ===== impl CurrentSpanExecutor ===== + impl CurrentSpanExecutor { /// Wrap an executor to propagate the current tracing span to its futures. pub fn new(inner: E) -> Self { @@ -41,9 +71,28 @@ where } } +// ===== impl WithSpanExecutor ===== + +impl WithSpanExecutor { + /// Wrap an executor to propagate the provided tracing span to its futures. + pub fn new(inner: E, span: Span) -> Self { + Self { inner, span } + } +} + +impl Executor for WithSpanExecutor +where + E: Executor>, + F: Future, +{ + fn execute(&self, future: F) { + self.inner.execute(future.instrument(self.span.clone())); + } +} + #[cfg(test)] mod tests { - use super::CurrentSpanExecutor; + use super::{CurrentSpanExecutor, WithSpanExecutor}; use hyper::rt::Executor; use std::{cell::RefCell, future::poll_fn, pin::Pin, task::Poll}; @@ -59,7 +108,7 @@ mod tests { } #[test] - fn propagates_span_from_execute_on_each_poll() { + fn current_span_executor_propagates_span_from_execute_on_each_poll() { let _subscriber = tracing::subscriber::set_default(tracing_subscriber::registry()); let construction_span = tracing::info_span!("construction"); let execution_span = tracing::info_span!("execution"); @@ -91,4 +140,41 @@ mod tests { assert_eq!(tracing::Span::current().id(), polling_span.id()); assert_eq!(*polls.borrow(), 2); } + + #[test] + fn with_span_executor_propagates_given_span_on_each_poll() { + let _subscriber = tracing::subscriber::set_default(tracing_subscriber::registry()); + let construction_span = tracing::info_span!("construction"); + let execution_span = tracing::info_span!("execution"); + let polling_span = tracing::info_span!("polling"); + let with_span = tracing::info_span!("with"); + assert!(execution_span.id().is_some()); + + // Borrowing a local executor and future also checks that the wrapper + // does not impose Send or 'static bounds on the inner executor. + let polls = RefCell::new(0); + let inner = DeferredExecutor::default(); + let executor = + construction_span.in_scope(|| WithSpanExecutor::new(&inner, with_span.clone())); + execution_span.in_scope(|| { + executor.execute(poll_fn(|_| { + // Execution happens within the given span. + assert_eq!(tracing::Span::current().id(), with_span.id()); + *polls.borrow_mut() += 1; + if *polls.borrow() == 1 { + Poll::Pending + } else { + Poll::Ready(()) + } + })); + }); + + let _entered = polling_span.enter(); + let mut task = tokio_test::task::spawn(inner.future.borrow_mut().take().unwrap()); + assert!(task.poll().is_pending()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert!(task.poll().is_ready()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert_eq!(*polls.borrow(), 2); + } } From 3ea7c5b1a86995ebefa470948a5b267715a55164 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 3/8] feat(rt/tracing): `WithSpanExecutor::current()` this propagates the construction span. Signed-off-by: katelyn martin --- src/rt/tracing.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/rt/tracing.rs b/src/rt/tracing.rs index f6176446..bf40e10a 100644 --- a/src/rt/tracing.rs +++ b/src/rt/tracing.rs @@ -78,6 +78,19 @@ impl WithSpanExecutor { pub fn new(inner: E, span: Span) -> Self { Self { inner, span } } + + /// Wrap an executor to propagate the current tracing span to its futures. + /// + /// This will instrument futures with the span that is active at the call-site of _this_ + /// function. Use [`CurrentSpanExecutor`] if you would prefer to propagate the current span + /// when [`Executor::execute()`] is called, rather than span that is active when initializating + /// the executor. + pub fn current(inner: E) -> Self { + Self { + inner, + span: Span::current(), + } + } } impl Executor for WithSpanExecutor @@ -177,4 +190,39 @@ mod tests { assert_eq!(tracing::Span::current().id(), polling_span.id()); assert_eq!(*polls.borrow(), 2); } + + #[test] + fn with_span_executor_current_propagates_construction_span() { + let _subscriber = tracing::subscriber::set_default(tracing_subscriber::registry()); + let construction_span = tracing::info_span!("construction"); + let execution_span = tracing::info_span!("execution"); + let polling_span = tracing::info_span!("polling"); + assert!(execution_span.id().is_some()); + + // Borrowing a local executor and future also checks that the wrapper + // does not impose Send or 'static bounds on the inner executor. + let polls = RefCell::new(0); + let inner = DeferredExecutor::default(); + let executor = construction_span.in_scope(|| WithSpanExecutor::current(&inner)); + execution_span.in_scope(|| { + executor.execute(poll_fn(|_| { + // Execution happens within the given span. + assert_eq!(tracing::Span::current().id(), construction_span.id()); + *polls.borrow_mut() += 1; + if *polls.borrow() == 1 { + Poll::Pending + } else { + Poll::Ready(()) + } + })); + }); + + let _entered = polling_span.enter(); + let mut task = tokio_test::task::spawn(inner.future.borrow_mut().take().unwrap()); + assert!(task.poll().is_pending()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert!(task.poll().is_ready()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert_eq!(*polls.borrow(), 2); + } } From 8648d7dbf436564608c63f1af795f8c5a6cbb4a0 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 4/8] feat(rt/tracing): introduce `MkSpanExecutor` Signed-off-by: katelyn martin --- Cargo.toml | 1 + src/rt/mod.rs | 2 +- src/rt/tracing.rs | 252 +++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 252 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2e9a1686..9024e5c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ tokio = { version = "1", features = ["macros", "test-util", "signal", "net", "io tokio-test = "0.4" tower-test = "0.4" pretty_env_logger = "0.5" +tracing-core = "0.1.29" tracing-subscriber = { version = "0.3", default-features = false, features = ["registry", "std"] } [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dev-dependencies] diff --git a/src/rt/mod.rs b/src/rt/mod.rs index 28f0c2b8..fe867f57 100644 --- a/src/rt/mod.rs +++ b/src/rt/mod.rs @@ -3,7 +3,7 @@ #[cfg(feature = "tracing")] mod tracing; #[cfg(feature = "tracing")] -pub use self::tracing::{CurrentSpanExecutor, WithSpanExecutor}; +pub use self::tracing::{CurrentSpanExecutor, MkSpanExecutor, WithSpanExecutor}; #[cfg(feature = "client-legacy")] mod io; diff --git a/src/rt/tracing.rs b/src/rt/tracing.rs index bf40e10a..2bca7ea5 100644 --- a/src/rt/tracing.rs +++ b/src/rt/tracing.rs @@ -52,6 +52,54 @@ pub struct WithSpanExecutor { span: Span, } +/// An executor that uses a callback to propagate tracing span to its futures. +/// +/// The callback is invoked each time a future is spawned, creating a span that +/// will be entered each time that future is polled or dropped. Execution is +/// delegated to the wrapped executor, without requiring a particular runtime. +/// +/// Requires the `tracing` feature. +/// +/// # Example +/// +/// Spawned tasks can be marked as "following from" the execution context. +/// +/// See [`tracing::Span::follows_from()`] for more information about +/// indicating causal relationships between spans. +/// +/// ``` +/// # #[cfg(feature = "tokio")] +/// # { +/// use hyper_util::rt::{MkSpanExecutor, TokioExecutor}; +/// use tracing::{info_span, Span}; +/// +/// let mk = || { +/// let span = info_span!("example"); +/// span.follows_from(Span::current()); +/// span +/// }; +/// let executor = MkSpanExecutor::new(TokioExecutor::new(), mk); +/// # } +/// ``` +/// +/// Spawned tasks can be marked as children of the execution context. +/// +/// ``` +/// # #[cfg(feature = "tokio")] +/// # { +/// use hyper_util::rt::{MkSpanExecutor, TokioExecutor}; +/// use tracing::{info_span, Span}; +/// +/// let mk = || info_span!(parent: Span::current(), "example"); +/// let executor = MkSpanExecutor::new(TokioExecutor::new(), mk); +/// # } +/// ``` +#[derive(Clone, Debug)] +pub struct MkSpanExecutor { + inner: E, + mk: F, +} + // ===== impl CurrentSpanExecutor ===== impl CurrentSpanExecutor { @@ -103,11 +151,38 @@ where } } +// ===== impl MkSpanExecutor ===== + +impl MkSpanExecutor { + /// Wrap an executor that creates new spans to instrument spawned futures. + pub fn new(inner: E, mk: F) -> Self { + Self { inner, mk } + } +} + +impl Executor for MkSpanExecutor +where + E: Executor>, + F: Fn() -> Span, + Fut: Future, +{ + fn execute(&self, future: Fut) { + let span = (self.mk)(); + self.inner.execute(future.instrument(span)); + } +} + #[cfg(test)] mod tests { - use super::{CurrentSpanExecutor, WithSpanExecutor}; + use super::{CurrentSpanExecutor, MkSpanExecutor, WithSpanExecutor}; use hyper::rt::Executor; - use std::{cell::RefCell, future::poll_fn, pin::Pin, task::Poll}; + use std::{ + cell::RefCell, + future::poll_fn, + pin::Pin, + sync::{Arc, Mutex}, + task::Poll, + }; #[derive(Default)] struct DeferredExecutor<'a> { @@ -225,4 +300,177 @@ mod tests { assert_eq!(tracing::Span::current().id(), polling_span.id()); assert_eq!(*polls.borrow(), 2); } + + #[test] + fn mk_span_executor_current_propagates_child_span() { + let _subscriber = tracing::subscriber::set_default(tracing_subscriber::registry()); + let construction_span = tracing::info_span!("construction"); + let execution_span = tracing::info_span!("execution"); + let polling_span = tracing::info_span!("polling"); + assert!(execution_span.id().is_some()); + + // A callback that creates a new child of the given span. + let mk = || tracing::info_span!(parent: tracing::Span::current(), "child"); + + // Borrowing a local executor and future also checks that the wrapper + // does not impose Send or 'static bounds on the inner executor. + let polls = RefCell::new(0); + let inner = DeferredExecutor::default(); + let executor = construction_span.in_scope(|| MkSpanExecutor::new(&inner, mk)); + execution_span.in_scope(|| { + executor.execute(poll_fn(|_| { + // Execution happens within the created child span. + let span = tracing::Span::current(); + assert_eq!(span.metadata().unwrap().name(), "child"); + *polls.borrow_mut() += 1; + if *polls.borrow() == 1 { + Poll::Pending + } else { + Poll::Ready(()) + } + })); + }); + + let _entered = polling_span.enter(); + let mut task = tokio_test::task::spawn(inner.future.borrow_mut().take().unwrap()); + assert!(task.poll().is_pending()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert!(task.poll().is_ready()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert_eq!(*polls.borrow(), 2); + } + + /// A subscriber that records causal `follows_from` relationships. + struct FollowsFromSubscriber { + inner: S, + follows_from: Arc>>, + } + + /// A tuple representing a causal relationship between two spans. + /// + /// This means that the span with the former id followed from the span with the latter id. + type FollowsFrom = (tracing::span::Id, tracing::span::Id); + + impl FollowsFromSubscriber { + fn new(inner: S) -> Self { + Self { + inner, + follows_from: Default::default(), + } + } + + /// Returns a reference to the set of relationships observed. + fn follows_from(&self) -> Arc>> { + Arc::clone(&self.follows_from) + } + } + + impl tracing::Subscriber for FollowsFromSubscriber + where + S: tracing::Subscriber, + { + fn record_follows_from(&self, span: &tracing::span::Id, follows: &tracing::span::Id) { + self.follows_from + .lock() + .unwrap() + .push((span.clone(), follows.clone())); + self.inner.record_follows_from(span, follows); + } + + fn current_span(&self) -> tracing_core::span::Current { + self.inner.current_span() + } + + // Other methods delegate to `inner`... + + fn enabled(&self, metadata: &tracing::Metadata<'_>) -> bool { + self.inner.enabled(metadata) + } + + fn enter(&self, span: &tracing::span::Id) { + self.inner.enter(span); + } + + fn event(&self, event: &tracing::Event<'_>) { + self.inner.event(event); + } + + fn exit(&self, span: &tracing::span::Id) { + self.inner.exit(span); + } + + fn new_span(&self, span: &tracing::span::Attributes<'_>) -> tracing::span::Id { + self.inner.new_span(span) + } + + fn record(&self, span: &tracing::span::Id, values: &tracing::span::Record<'_>) { + self.inner.record(span, values); + } + } + + #[test] + fn mk_span_executor_current_propagates_causal_span_relationships() { + // Use a subscriber that records `follows_from` relationships. + let subscriber = FollowsFromSubscriber::new(tracing_subscriber::registry()); + let relationships = subscriber.follows_from(); + let _subscriber = tracing::subscriber::set_default(subscriber); + + let construction_span = tracing::info_span!("construction"); + let execution_a_span = tracing::info_span!("execution_a"); + let execution_b_span = tracing::info_span!("execution_b"); + let polling_span = tracing::info_span!("polling"); + + // A callback that creates a span that `follows_from` the execution span. + let mk = || { + let span = tracing::info_span!("spawned"); + span.follows_from(tracing::Span::current()); + span + }; + + let polls = RefCell::new(0); + let inner = DeferredExecutor::default(); + let executor = construction_span.in_scope(|| MkSpanExecutor::new(&inner, mk)); + execution_a_span.in_scope(|| { + executor.execute(poll_fn(|_| { + // Execution happens within the created child span. + let span = tracing::Span::current(); + assert_eq!(span.metadata().unwrap().name(), "spawned"); + *polls.borrow_mut() += 1; + if *polls.borrow() == 1 { + Poll::Pending + } else { + Poll::Ready(()) + } + })); + }); + + let _entered = polling_span.enter(); + let mut task = tokio_test::task::spawn(inner.future.borrow_mut().take().unwrap()); + assert!(task.poll().is_pending()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert_eq!(relationships.lock().unwrap().len(), 1); + assert!(task.poll().is_ready()); + assert_eq!(tracing::Span::current().id(), polling_span.id()); + assert_eq!(*polls.borrow(), 2); + assert_eq!(relationships.lock().unwrap().len(), 1); + + execution_b_span.in_scope(|| { + executor.execute(poll_fn(|_| { + let span = tracing::Span::current(); + assert_eq!(span.metadata().unwrap().name(), "spawned"); + Poll::Ready(()) + })); + }); + + let _entered = polling_span.enter(); + let mut task = tokio_test::task::spawn(inner.future.borrow_mut().take().unwrap()); + assert!(task.poll().is_ready()); + + // The first task followed from the `execution_a` span. The second task + // followed from the `execution_b` span. + let relationships = relationships.lock().unwrap(); + assert_eq!(relationships.len(), 2); + assert_eq!(relationships[0].1, execution_a_span.id().unwrap()); + assert_eq!(relationships[1].1, execution_b_span.id().unwrap()); + } } From df2818131e757605bc52a2dfacef0cdc1def64c1 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 5/8] doc(rt/tracing): module-level documentation Signed-off-by: katelyn martin --- src/rt/tracing.rs | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/rt/tracing.rs b/src/rt/tracing.rs index 2bca7ea5..feda5031 100644 --- a/src/rt/tracing.rs +++ b/src/rt/tracing.rs @@ -1,3 +1,73 @@ +//! Runtime components for use with [`tracing`]. +//! +//! This module provides [`Executor`] implementations that configure +//! instrumentation of spawned futures. These [`Executor`]s can propagate +//! tracing [`Span`]s to futures spawned onto the async runtime. See the +//! crate-level documentation of [`tracing`] for [more information] about spans. +//! +//! # Choosing an [`Executor`]. +//! +//! Hyper spawns [`Future`]s onto an [`Executor`], to avoid tightly coupling +//! APIs to any particular async runtime. This includes background tasks that +//! might help service I/O for the lifetime of a connection, for example. +//! +//! Some [`Subscriber`][tracing::subscriber] implementations have different +//! semantics regarding the lifecycle of [`Span`]s. Integrations with +//! OpenTelemetry collectors, for example, might not emit the events within +//! the context of a span until it is closed. Conversely, subscribers that +//! print traces to the terminal may not have to contend with these details when +//! instrumenting long-lived tasks that run in the background. +//! +//! This module provides different executors to help pass tracing context in +//! the manner appropriate for your application. For most typical applications, +//! [`CurrentSpanExecutor`] should suffice. +//! +//! # Examples +//! +//! Run spawned tasks within a provided span. +//! +//! ``` +//! # #[cfg(feature = "tokio")] +//! # { +//! use hyper_util::rt::{TokioExecutor, WithSpanExecutor}; +//! +//! let span = tracing::info_span!("example"); +//! let executor = WithSpanExecutor::new(TokioExecutor::new(), span); +//! # } +//! ``` +//! +//! Run spawned tasks within the current span when [`Executor::execute()`] is +//! called. +//! +//! ``` +//! # #[cfg(feature = "tokio")] +//! # { +//! use hyper_util::rt::{TokioExecutor, CurrentSpanExecutor}; +//! +//! let executor = CurrentSpanExecutor::new(TokioExecutor::new()); +//! # } +//! ``` +//! +//! Run spawned tasks within distinct spans that are marked as following from +//! the active span when [`Executor::execute()`] is called. +//! +//! ``` +//! # #[cfg(feature = "tokio")] +//! # { +//! use hyper_util::rt::{MkSpanExecutor, TokioExecutor}; +//! use tracing::{info_span, Span}; +//! +//! let mk = || { +//! let span = info_span!("example"); +//! span.follows_from(Span::current()); +//! span +//! }; +//! let executor = MkSpanExecutor::new(TokioExecutor::new(), mk); +//! # } +//! ``` +//! +//! [more information]: tracing#spans-1 + use hyper::rt::Executor; use tracing::{ Span, From b9c8385c64d74d9d5a4c7d97088bdcb350f20c8b Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 6/8] feat(rt): `tracing` submodule is public now that we have added documentation to this submodule with information about selecting an executor, along with examples, we should make this submodule public so that users can see it. Signed-off-by: katelyn martin --- src/rt/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rt/mod.rs b/src/rt/mod.rs index fe867f57..ce1c5b71 100644 --- a/src/rt/mod.rs +++ b/src/rt/mod.rs @@ -1,7 +1,7 @@ //! Runtime utilities #[cfg(feature = "tracing")] -mod tracing; +pub mod tracing; #[cfg(feature = "tracing")] pub use self::tracing::{CurrentSpanExecutor, MkSpanExecutor, WithSpanExecutor}; From 027f2448196455b8d7487e18021f9b811a653aed Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 7/8] feat(rt/tokio): tweak informative `TokioExecute` note Signed-off-by: katelyn martin --- src/rt/tokio.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/rt/tokio.rs b/src/rt/tokio.rs index d42d9df3..dee88e05 100644 --- a/src/rt/tokio.rs +++ b/src/rt/tokio.rs @@ -71,7 +71,13 @@ mod with_tokio_io; /// Future executor that utilises `tokio` threads. /// /// Spawned futures do not inherit the current tracing span, even when the -/// `tracing` feature is enabled. +/// `tracing` feature is enabled. To propagate spans, wrap this executor in +/// one of the components from [`rt::tracing`](crate::rt::tracing), such as +/// [`CurrentSpanExecutor`](crate::rt::CurrentSpanExecutor) (available with +/// the `tracing` feature). +/// +/// See the module-level documentation of [`rt::tracing`](crate::rt::tracing) +/// for more information about propagating [`tracing`] spans to spawned tasks. /// /// The temporary `rt-tracing-exec-force` feature restores propagation of the /// current span for libraries that do not allow customizing their executor. From 4c39c84e8a562cf24235cc1a25c7fb73314369a5 Mon Sep 17 00:00:00 2001 From: katelyn martin Date: Sat, 19 Sep 2026 00:00:00 +0000 Subject: [PATCH 8/8] feat(examples): add `client_tracing.rs` example this is an example similar to `client.rs` that makes use of the tracing functionality in `rt::tracing`. this example uses the current span executor, and runs the client in an info-level span to demonstrate the functionality. the `fmt` feature is added to our development dependency upon `tracing_subscriber`. when run against the server example, the logs look like the following: ``` ; cargo run --example client_tracing --features 'client client-legacy http1 tokio tracing' -- http://127.0.0.1:8000 2026-09-21T18:12:10.840038Z INFO client_tracing: tracing subscriber initialized 2026-09-21T18:12:10.840072Z INFO client_tracing: parsed URL url=http://127.0.0.1:8000/ 2026-09-21T18:12:10.840137Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::pool: checkout waiting for idle connection: ("http", 127.0.0.1:8000) 2026-09-21T18:12:10.840181Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::connect::http: Http::connect; scheme=Some("http"), host=Some("127.0.0.1"), port=Some(Port(8000)) 2026-09-21T18:12:10.840199Z DEBUG sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::connect::http: connecting to 127.0.0.1:8000 2026-09-21T18:12:10.840335Z DEBUG sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::connect::http: connected to 127.0.0.1:8000 2026-09-21T18:12:10.840369Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::client: http1 handshake complete, spawning background dispatcher task 2026-09-21T18:12:10.840386Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::client: waiting for connection to be ready 2026-09-21T18:12:10.840422Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::client: connection is ready 2026-09-21T18:12:10.840432Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::pool: checkout dropped for ("http", 127.0.0.1:8000) 2026-09-21T18:12:10.840666Z TRACE sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::pool: put; add idle connection for ("http", 127.0.0.1:8000) 2026-09-21T18:12:10.840689Z DEBUG sending request{url=http://127.0.0.1:8000/}: hyper_util::client::legacy::pool: pooling idle connection for ("http", 127.0.0.1:8000) 2026-09-21T18:12:10.840709Z INFO client_tracing: received response resp.version=HTTP/1.1 resp.status=200 OK resp.body=Hello, world! ``` this demonstrates the informative context (the destination url in this case) that is attached to background tasks spawned by the client. Signed-off-by: katelyn martin --- Cargo.toml | 2 +- examples/client_tracing.rs | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 examples/client_tracing.rs diff --git a/Cargo.toml b/Cargo.toml index 9024e5c5..a0d3bc3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,7 +46,7 @@ tokio-test = "0.4" tower-test = "0.4" pretty_env_logger = "0.5" tracing-core = "0.1.29" -tracing-subscriber = { version = "0.3", default-features = false, features = ["registry", "std"] } +tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt", "registry", "std"] } [target.'cfg(any(target_os = "linux", target_os = "macos"))'.dev-dependencies] pnet_datalink = "0.35.0" diff --git a/examples/client_tracing.rs b/examples/client_tracing.rs new file mode 100644 index 00000000..23f41110 --- /dev/null +++ b/examples/client_tracing.rs @@ -0,0 +1,62 @@ +use std::env; + +use http_body_util::{BodyExt, Empty}; +use hyper::Request; +use hyper_util::client::legacy::{Client, connect::HttpConnector}; +use tracing::Instrument; + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), Box> { + let _tracing = tracing_subscriber::fmt() + .with_max_level(tracing::Level::TRACE) + .init(); + tracing::info!("tracing subscriber initialized"); + + let url = match env::args().nth(1) { + Some(url) => url, + None => { + tracing::error!("Usage: client "); + return Ok(()); + } + }; + + // HTTPS requires picking a TLS implementation, so give a better + // warning if the user tries to request an 'https' URL. + let url = url + .parse::() + .inspect_err(|err| tracing::error!(%err, "failed to parse url"))?; + if url.scheme_str() != Some("http") { + tracing::error!("This example only works with 'http' URLs."); + return Ok(()); + } + tracing::info!(%url, "parsed URL"); + + let executor = { + // Propagate spans to spawned tasks. + use hyper_util::rt::{CurrentSpanExecutor, TokioExecutor}; + let tokio = TokioExecutor::new(); + CurrentSpanExecutor::new(tokio) + }; + + let client = Client::builder(executor).build(HttpConnector::new()); + + let req = Request::builder() + .uri(url.clone()) + .body(Empty::::new())?; + + let span = tracing::info_span!("sending request", %url); + let resp = client.request(req).instrument(span).await?; + + let (resp, body) = resp.into_parts(); + let body = body.collect().await.unwrap().to_bytes().to_vec(); + let body = String::from_utf8(body).unwrap(); + + tracing::info!( + ?resp.version, + %resp.status, + resp.body = %body, + "received response", + ); + + Ok(()) +}