snafu_virtstack/lib.rs
1//! # SNAFU Virtual Stack Trace
2//!
3//! A lightweight, efficient error handling library for Rust that implements virtual stack traces
4//! based on [GreptimeDB's error handling approach](https://greptime.com/blogs/2024-05-07-error-rust).
5//! This library combines the power of [SNAFU](https://github.com/shepmaster/snafu) error handling
6//! with virtual stack traces to provide meaningful error context without the overhead of system backtraces.
7//!
8//! ## Motivation
9//!
10//! Traditional error handling in Rust often faces a dilemma:
11//! - **Option 1:** Use system backtraces - long hard to read stack traces only referencing functions and lines
12//! - **Option 2:** Simple error propagation - lacks context about where errors originated
13//!
14//! Virtual stack traces provide a third way: capturing meaningful context at each error propagation point with minimal overhead.
15//!
16//! ## Features
17//!
18//! - 🚀 **Lightweight**: Only ~100KB binary overhead vs several MB for system backtraces
19//! - 📍 **Precise Location Tracking**: Automatically captures file, line, and column information
20//! - 🔗 **Error Chain Walking**: Traverses the entire error source chain
21//! - 🎯 **Zero-Cost Abstraction**: Context generation can be postponed until needed
22//! - 🛠️ **Seamless Integration**: Works perfectly with SNAFU error handling
23//! - 📝 **Developer-Friendly**: Automatic Debug implementation with formatted stack traces
24//!
25//! ## Basic Usage
26//!
27//! Simply add the `#[stack_trace_debug]` attribute to your SNAFU error enum:
28//!
29//! ```rust
30//! use snafu::prelude::*;
31//! use snafu_virtstack::stack_trace_debug;
32//!
33//! #[derive(Snafu)]
34//! #[stack_trace_debug] // Add this attribute
35//! enum MyError {
36//! #[snafu(display("Failed to read file: {filename}"))]
37//! FileRead { filename: String, source: std::io::Error },
38//!
39//! #[snafu(display("Invalid data format"))]
40//! InvalidFormat { source: serde_json::Error },
41//! }
42//!
43//! fn process_file(filename: &str) -> Result<String, MyError> {
44//! let content = std::fs::read_to_string(filename)
45//! .context(FileReadSnafu { filename })?;
46//!
47//! let data: serde_json::Value = serde_json::from_str(&content)
48//! .context(InvalidFormatSnafu)?;
49//!
50//! Ok(data.to_string())
51//! }
52//! ```
53//!
54//! ## Generated Debug Output
55//!
56//! When an error occurs, the generated [`Debug`] implementation will display:
57//!
58//! ```text
59//! Error: Failed to read file: config.json
60//! Virtual Stack Trace:
61//! 0: Failed to read file: config.json at src/main.rs:15:23
62//! 1: No such file or directory (os error 2) at src/main.rs:16:10
63//! ```
64//!
65//! ## Advanced Usage
66//!
67//! You can also access the virtual stack programmatically:
68//!
69//! ```rust
70//! use snafu_virtstack::VirtualStackTrace;
71//! # use snafu::prelude::*;
72//! # use snafu_virtstack::stack_trace_debug;
73//! # #[derive(Snafu)]
74//! # #[stack_trace_debug]
75//! # enum MyError {
76//! # #[snafu(display("Something went wrong"))]
77//! # SomethingWrong,
78//! # }
79//!
80//! let error = MyError::SomethingWrong;
81//! let stack = error.virtual_stack();
82//!
83//! for (i, frame) in stack.iter().enumerate() {
84//! println!("Frame {}: {} at {}:{}",
85//! i,
86//! frame.message,
87//! frame.location.file(),
88//! frame.location.line()
89//! );
90//! }
91//! ```
92//!
93//! ## Requirements
94//!
95//! - Must be applied to `enum` types only
96//! - The enum should derive [`Snafu`] for full functionality
97//! - Works best with error enums that have source fields for error chaining
98//!
99//! ## Performance Benefits
100//!
101//! The virtual stack trace approach provides several key advantages:
102//!
103//! ### 1. Performance Efficiency
104//! Unlike system backtraces that capture the entire call stack (expensive operation),
105//! virtual stack traces only record error propagation points. This results in:
106//! - Lower CPU usage during error handling
107//! - Reduced memory footprint
108//! - Smaller binary sizes (100KB vs several MB)
109//!
110//! ### 2. Meaningful Context
111//! Virtual stack traces capture:
112//! - The exact location where each error was propagated
113//! - Custom error messages at each level
114//! - The complete error chain from root cause to final error
115//!
116//! ### 3. Production-Ready
117//! - Safe to use in production environments
118//! - No performance penalties in the happy path
119//! - Can be enabled/disabled at runtime if needed
120//!
121//! ## How It Works
122//!
123//! 1. **Proc Macro Magic**: The [`stack_trace_debug`] attribute automatically implements:
124//! - [`VirtualStackTrace`] trait for stack frame collection
125//! - Custom [`Debug`] implementation for formatted output
126//!
127//! 2. **Location Tracking**: Uses Rust's `#[track_caller]` to capture precise locations
128//! where errors are propagated
129//!
130//! 3. **Error Chain Walking**: Automatically traverses the `source()` chain to build
131//! complete error context
132//!
133//! 4. **Zero-Cost Until Needed**: Stack frames are only generated when the error is
134//! actually inspected
135
136// Re-export the proc macro so users only need to depend on this crate
137pub use snafu_virtstack_macro::stack_trace_debug;
138
139/// Core trait for virtual stack trace functionality.
140///
141/// This trait is automatically implemented by the [`stack_trace_debug`] proc macro attribute.
142/// It provides access to the virtual stack trace showing the error propagation path.
143///
144/// # Example
145///
146/// ```rust
147/// use snafu::prelude::*;
148/// use snafu_virtstack::{stack_trace_debug, VirtualStackTrace};
149///
150/// #[derive(Snafu)]
151/// #[stack_trace_debug]
152/// enum MyError {
153/// #[snafu(display("Something went wrong"))]
154/// SomethingWrong,
155/// }
156///
157/// let error = MyError::SomethingWrong;
158/// let stack = error.virtual_stack();
159/// for frame in stack {
160/// println!("{}", frame);
161/// }
162/// ```
163pub trait VirtualStackTrace {
164 /// Returns a virtual stack trace showing error propagation path.
165 ///
166 /// Each [`StackFrame`] in the returned vector represents one step in the error
167 /// propagation chain, from the outermost error context down to the root cause.
168 fn virtual_stack(&self) -> Vec<StackFrame>;
169}
170
171/// Represents a single frame in the virtual stack trace.
172///
173/// Each frame captures the location where an error was propagated and the
174/// associated error message. This provides precise context about the error
175/// propagation path without the overhead of system backtraces.
176#[derive(Debug, Clone)]
177pub struct StackFrame {
178 /// Location where the error occurred or was propagated
179 pub location: &'static std::panic::Location<'static>,
180 /// Error message for this frame
181 pub message: String,
182}
183
184impl StackFrame {
185 /// Creates a new stack frame with the given location and message.
186 ///
187 /// # Arguments
188 ///
189 /// * `location` - The location where the error occurred, typically from `std::panic::Location::caller()`
190 /// * `message` - A descriptive message for this error frame
191 ///
192 /// # Example
193 ///
194 /// ```rust
195 /// use snafu_virtstack::StackFrame;
196 /// use std::panic::Location;
197 ///
198 /// #[track_caller]
199 /// fn create_frame() -> StackFrame {
200 /// StackFrame::new(
201 /// Location::caller(),
202 /// "Something went wrong".to_string()
203 /// )
204 /// }
205 /// ```
206 pub fn new(location: &'static std::panic::Location<'static>, message: String) -> Self {
207 Self { location, message }
208 }
209}
210
211impl std::fmt::Display for StackFrame {
212 /// Formats the stack frame showing the message and location information.
213 ///
214 /// The format is: `{message} at {file}:{line}:{column}`
215 ///
216 /// # Example Output
217 ///
218 /// ```text
219 /// Failed to read configuration file at src/config.rs:42:15
220 /// ```
221 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
222 write!(
223 f,
224 "{} at {}:{}:{}",
225 self.message,
226 self.location.file(),
227 self.location.line(),
228 self.location.column()
229 )
230 }
231}