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