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 fn with_span_context(self) -> SpanContextFuture<Self> {
54 SpanContextFuture { inner: self, span: Span::current() }
55 }
56
57 /// Captures a specific span's metadata as error context on failure.
58 ///
59 /// Unlike `with_span_context()`, this method uses the provided span
60 /// instead of the current span.
61 fn with_span(self, span: Span) -> SpanContextFuture<Self> {
62 SpanContextFuture { inner: self, span }
63 }
64}
65
66impl<F, T, E> FutureSpanExt<T, E> for F where F: Future<Output = Result<T, E>> {}
67
68pin_project! {
69 /// Future wrapper that captures span context on error.
70 ///
71 /// Created by [`FutureSpanExt::with_span_context`] or [`FutureSpanExt::with_span`].
72 #[must_use = "futures do nothing unless polled"]
73 pub struct SpanContextFuture<F> {
74 #[pin]
75 inner: F,
76 span: Span,
77 }
78}
79
80impl<F, T, E> Future for SpanContextFuture<F>
81where
82 F: Future<Output = Result<T, E>>,
83{
84 type Output = BoxedComposableResult<T, E>;
85
86 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
87 let this = self.project();
88
89 match this.inner.poll(cx) {
90 Poll::Ready(Ok(value)) => Poll::Ready(Ok(value)),
91 Poll::Ready(Err(error)) => {
92 let context = span_to_context(this.span);
93 Poll::Ready(Err(Box::new(ComposableError::new(error).with_context(context))))
94 },
95 Poll::Pending => Poll::Pending,
96 }
97 }
98}
99
100/// Converts a tracing span to an error context.
101///
102/// Extracts the span's metadata (name, target, level, fields) and creates
103/// a structured error context. This provides rich observability when errors
104/// occur within instrumented code.
105///
106/// # Fields Captured
107///
108/// - **Tag**: The span name (e.g., `fetch_user`)
109/// - **Metadata `target`**: The module path where the span was created
110/// - **Metadata `level`**: The span's log level (TRACE, DEBUG, INFO, WARN, ERROR)
111/// - **Metadata `fields`**: Names of fields defined on the span (values require subscriber)
112fn span_to_context(span: &Span) -> ErrorContext {
113 let Some(meta) = span.metadata() else {
114 return ErrorContext::new("in unknown span");
115 };
116
117 let mut builder = ErrorContext::builder().tag(meta.name());
118
119 // Add target module path
120 builder = builder.metadata("target", meta.target());
121
122 // Add log level
123 builder = builder.metadata("level", meta.level().as_str());
124
125 // Add field names (values not accessible without subscriber integration)
126 let field_names: alloc::vec::Vec<&str> = meta.fields().iter().map(|f| f.name()).collect();
127 if !field_names.is_empty() {
128 builder = builder.metadata("fields", field_names.join(", "));
129 }
130
131 builder.build()
132}
133
134/// Extension trait for `Result` types to add span context to errors.
135pub trait ResultSpanExt<T, E> {
136 /// Adds the current span's context to an error.
137 ///
138 /// # Example
139 ///
140 /// ```rust,ignore
141 /// use error_rail::async_ext::ResultSpanExt;
142 ///
143 /// fn process() -> BoxedComposableResult<Data, ProcessError> {
144 /// do_work().with_current_span()
145 /// }
146 /// ```
147 fn with_current_span(self) -> BoxedComposableResult<T, E>;
148
149 /// Adds a specific span's context to an error.
150 fn with_span(self, span: &Span) -> BoxedComposableResult<T, E>;
151}
152
153impl<T, E> ResultSpanExt<T, E> for Result<T, E> {
154 fn with_current_span(self) -> BoxedComposableResult<T, E> {
155 self.with_span(&Span::current())
156 }
157
158 fn with_span(self, span: &Span) -> BoxedComposableResult<T, E> {
159 match self {
160 Ok(v) => Ok(v),
161 Err(e) => {
162 let context = span_to_context(span);
163 Err(Box::new(ComposableError::new(e).with_context(context)))
164 },
165 }
166 }
167}
168
169/// Instruments an error with the current span and all its parents.
170///
171/// This function captures the entire span hierarchy, providing a complete
172/// picture of the execution context when the error occurred.
173///
174/// # Example
175///
176/// ```rust,ignore
177/// use error_rail::async_ext::instrument_error;
178///
179/// let error = ApiError::NotFound;
180/// let instrumented = instrument_error(error);
181/// // Error now contains context from all active spans
182/// ```
183pub fn instrument_error<E>(error: E) -> ComposableError<E> {
184 let span = Span::current();
185 ComposableError::new(error).with_context(span_to_context(&span))
186}
187
188// Required for alloc::format!
189extern crate alloc;