error_rail/async_ext/
tracing_ext.rs

1//! Tracing integration for error-rail.
2//!
3//! This module provides utilities for integrating error-rail with the
4//! `tracing` ecosystem, automatically capturing span information as
5//! error context.
6//!
7//! # Feature Flag
8//!
9//! Requires the `tracing` feature:
10//!
11//! ```toml
12//! [dependencies]
13//! error-rail = { version = "0.8", features = ["tracing"] }
14//! ```
15
16use core::future::Future;
17use core::pin::Pin;
18use core::task::{Context, Poll};
19
20use pin_project_lite::pin_project;
21use tracing::Span;
22
23use crate::types::{BoxedComposableResult, ComposableError};
24use crate::ErrorContext;
25
26/// Extension trait for futures that adds tracing span context to errors.
27///
28/// This trait provides methods to automatically capture the current tracing
29/// span's metadata and attach it as error context when errors occur.
30///
31/// # Example
32///
33/// ```rust,ignore
34/// use error_rail::async_ext::FutureSpanExt;
35/// use tracing::info_span;
36///
37/// async fn fetch_user(id: u64) -> Result<User, ApiError> {
38///     let span = info_span!("fetch_user", user_id = id);
39///     
40///     async {
41///         database.get_user(id).await
42///     }
43///     .with_span_context()
44///     .instrument(span)
45///     .await
46/// }
47/// ```
48pub trait FutureSpanExt<T, E>: Future<Output = Result<T, E>> + Sized {
49    /// Captures the current span's metadata as error context on failure.
50    ///
51    /// When the future resolves to an error, the current span's name and
52    /// metadata are attached as context to the error.
53    #[inline]
54    fn with_span_context(self) -> SpanContextFuture<Self> {
55        SpanContextFuture { inner: self, span: Span::current() }
56    }
57
58    /// Captures a specific span's metadata as error context on failure.
59    ///
60    /// Unlike `with_span_context()`, this method uses the provided span
61    /// instead of the current span.
62    #[inline]
63    fn with_span(self, span: Span) -> SpanContextFuture<Self> {
64        SpanContextFuture { inner: self, span }
65    }
66}
67
68impl<F, T, E> FutureSpanExt<T, E> for F where F: Future<Output = Result<T, E>> {}
69
70pin_project! {
71    /// Future wrapper that captures span context on error.
72    ///
73    /// Created by [`FutureSpanExt::with_span_context`] or [`FutureSpanExt::with_span`].
74    #[must_use = "futures do nothing unless polled"]
75    pub struct SpanContextFuture<F> {
76        #[pin]
77        inner: F,
78        span: Span,
79    }
80}
81
82impl<F, T, E> Future for SpanContextFuture<F>
83where
84    F: Future<Output = Result<T, E>>,
85{
86    type Output = BoxedComposableResult<T, E>;
87
88    #[inline]
89    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
90        let this = self.project();
91
92        match this.inner.poll(cx) {
93            Poll::Ready(Ok(value)) => Poll::Ready(Ok(value)),
94            Poll::Ready(Err(error)) => Poll::Ready(Err(Box::new(
95                ComposableError::new(error).with_context(span_to_context(this.span)),
96            ))),
97            Poll::Pending => Poll::Pending,
98        }
99    }
100}
101
102/// Converts a tracing span to an error context.
103///
104/// Extracts the span's metadata (name, target, level, fields) and creates
105/// a structured error context. This provides rich observability when errors
106/// occur within instrumented code.
107///
108/// # Fields Captured
109///
110/// - **Tag**: The span name (e.g., `fetch_user`)
111/// - **Metadata `target`**: The module path where the span was created
112/// - **Metadata `level`**: The span's log level (TRACE, DEBUG, INFO, WARN, ERROR)
113/// - **Metadata `fields`**: Names of fields defined on the span (values require subscriber)
114#[inline]
115fn span_to_context(span: &Span) -> ErrorContext {
116    let Some(meta) = span.metadata() else {
117        return ErrorContext::new("in unknown span");
118    };
119
120    let mut builder = ErrorContext::builder()
121        .tag(meta.name())
122        .metadata("target", meta.target())
123        .metadata("level", meta.level().as_str());
124
125    let mut fields_iter = meta.fields().iter();
126    if let Some(first) = fields_iter.next() {
127        use crate::types::alloc_type::String;
128        use core::fmt::Write;
129
130        let mut fields_str = String::from(first.name());
131        for field in fields_iter {
132            fields_str.push_str(", ");
133            let _ = write!(fields_str, "{}", field.name());
134        }
135        builder = builder.metadata("fields", fields_str);
136    }
137
138    builder.build()
139}
140
141/// Extension trait for `Result` types to add span context to errors.
142pub trait ResultSpanExt<T, E> {
143    /// Adds the current span's context to an error.
144    ///
145    /// # Example
146    ///
147    /// ```rust,ignore
148    /// use error_rail::async_ext::ResultSpanExt;
149    ///
150    /// fn process() -> BoxedComposableResult<Data, ProcessError> {
151    ///     do_work().with_current_span()
152    /// }
153    /// ```
154    fn with_current_span(self) -> BoxedComposableResult<T, E>;
155
156    /// Adds a specific span's context to an error.
157    fn with_span(self, span: &Span) -> BoxedComposableResult<T, E>;
158}
159
160impl<T, E> ResultSpanExt<T, E> for Result<T, E> {
161    #[inline]
162    fn with_current_span(self) -> BoxedComposableResult<T, E> {
163        self.with_span(&Span::current())
164    }
165
166    #[inline]
167    fn with_span(self, span: &Span) -> BoxedComposableResult<T, E> {
168        match self {
169            Ok(v) => Ok(v),
170            Err(e) => Err(Box::new(ComposableError::new(e).with_context(span_to_context(span)))),
171        }
172    }
173}
174
175/// Instruments an error with the current span and all its parents.
176///
177/// This function captures the entire span hierarchy, providing a complete
178/// picture of the execution context when the error occurred.
179///
180/// # Example
181///
182/// ```rust,ignore
183/// use error_rail::async_ext::instrument_error;
184///
185/// let error = ApiError::NotFound;
186/// let instrumented = instrument_error(error);
187/// // Error now contains context from all active spans
188/// ```
189#[inline]
190pub fn instrument_error<E>(error: E) -> ComposableError<E> {
191    ComposableError::new(error).with_context(span_to_context(&Span::current()))
192}