rootcause_backtrace/lib.rs
1#![deny(
2 missing_docs,
3 unsafe_code,
4 rustdoc::invalid_rust_codeblocks,
5 rustdoc::broken_intra_doc_links,
6 missing_copy_implementations,
7 unused_doc_comments
8)]
9
10//! Stack backtrace attachment collector for rootcause error reports.
11//!
12//! This crate provides functionality to automatically capture and attach stack
13//! backtraces to error reports. This is useful for debugging to see the call
14//! stack that led to an error.
15//!
16//! # Quick Start
17//!
18//! ## Using Hooks (Automatic for All Errors)
19//!
20//! Register a backtrace collector as a hook to automatically capture backtraces
21//! for all errors:
22//!
23//! ```rust
24//! use rootcause::hooks::Hooks;
25//! use rootcause_backtrace::BacktraceCollector;
26//!
27//! // Capture backtraces for all errors
28//! Hooks::new()
29//! .report_creation_hook(BacktraceCollector::new_from_env())
30//! .install()
31//! .expect("failed to install hooks");
32//!
33//! // Now all errors automatically get backtraces!
34//! fn example() -> rootcause::Report {
35//! rootcause::report!("something went wrong")
36//! }
37//! println!("{}", example().context("additional context"));
38//! ```
39//!
40//! This will print a backtrace similar to the following:
41//! ```text
42//! ● additional context
43//! ├ src/main.rs:12
44//! ├ Backtrace
45//! │ │ main - /build/src/main.rs:12
46//! │ │ note: 39 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
47//! │ ╰─
48//! │
49//! ● something went wrong
50//! ├ src/main.rs:10
51//! ╰ Backtrace
52//! │ example - /build/src/main.rs:10
53//! │ main - /build/src/main.rs:12
54//! │ note: 40 frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.
55//! ╰─
56//! ```
57//!
58//! ## Manual Attachment (Per-Error)
59//!
60//! Attach backtraces to specific errors using the extension trait:
61//!
62//! ```rust
63//! use std::io;
64//!
65//! use rootcause::{Report, report};
66//! use rootcause_backtrace::BacktraceExt;
67//!
68//! fn operation() -> Result<(), Report> {
69//! Err(report!("operation failed"))
70//! }
71//!
72//! // Attach backtrace to the error in the Result
73//! let result = operation().attach_backtrace();
74//! ```
75//!
76//! # Environment Variables
77//!
78//! - `RUST_BACKTRACE=full` - Disables filtering and shows full paths
79//! - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
80//! - `leafs` - Only capture backtraces for leaf errors (errors without
81//! children)
82//! - `full_paths` - Show full file paths in backtraces
83//!
84//! # Path privacy
85//!
86//! By default, backtrace paths are shortened paths for improved readability,
87//! but this may still expose private file system structure when a path is not
88//! recognized as belonging to a known prefix (e.g., RUST_SRC).
89//!
90//! If exposing private file system paths is a concern, then we recommend using
91//! the `--remap-path-prefix` option of `rustc` to remap source paths to
92//! generic placeholders.
93//!
94//! A good default way to handle this is to set the following environment
95//! variables when building your application for release:
96//!
97//! ```sh
98//! export RUSTFLAGS="--remap-path-prefix=$HOME=/home/user --remap-path-prefix=$PWD=/build"
99//! ```
100//!
101//! # Debugging symbols in release builds
102//!
103//! To ensure that backtraces contain useful symbol and source location
104//! information in release builds, make sure to enable debug symbols in your
105//! `Cargo.toml`:
106//!
107//! ```toml
108//! [profile.release]
109//! strip = false
110//! # You can also set this to "line-tables-only" for smaller binaries
111//! debug = true
112//! ```
113//!
114//! # Filtering
115//!
116//! Control which frames appear in backtraces:
117//!
118//! ```rust
119//! use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
120//!
121//! let collector = BacktraceCollector {
122//! filter: BacktraceFilter {
123//! skipped_initial_crates: &["rootcause", "rootcause-backtrace"], // Skip frames from rootcause at start
124//! skipped_middle_crates: &["tokio"], // Skip tokio frames in middle
125//! skipped_final_crates: &["std"], // Skip std frames at end
126//! max_entry_count: 15, // Limit to 15 frames
127//! show_full_path: false, // Show shortened paths
128//! },
129//! capture_backtrace_for_reports_with_children: false, // Only leaf errors
130//! };
131//! ```
132
133use std::{borrow::Cow, fmt, panic::Location, sync::OnceLock};
134
135use backtrace::BytesOrWideString;
136use rootcause::{
137 Report, ReportMut,
138 handlers::{
139 AttachmentFormattingPlacement, AttachmentFormattingStyle, AttachmentHandler,
140 FormattingFunction,
141 },
142 hooks::report_creation::ReportCreationHook,
143 markers::{self, Dynamic, ObjectMarkerFor},
144 report_attachment::ReportAttachment,
145};
146
147/// Stack backtrace information.
148///
149/// Contains a collection of stack frames representing the call stack
150/// at the point where a report was created.
151///
152/// # Examples
153///
154/// Capture a backtrace manually:
155///
156/// ```rust
157/// use rootcause_backtrace::{Backtrace, BacktraceFilter};
158///
159/// let backtrace = Backtrace::capture(&BacktraceFilter::DEFAULT);
160/// if let Some(bt) = backtrace {
161/// println!("Captured {} frames", bt.entries.len());
162/// }
163/// ```
164#[derive(Debug)]
165pub struct Backtrace {
166 /// The entries in the backtrace, ordered from most recent to oldest.
167 pub entries: Vec<BacktraceEntry>,
168 /// Total number of frames that were omitted due to filtering.
169 pub total_omitted_frames: usize,
170}
171
172/// A single entry in a stack backtrace.
173#[derive(Debug)]
174pub enum BacktraceEntry {
175 /// A normal stack frame.
176 Frame(Frame),
177 /// A group of omitted frames from a specific crate.
178 OmittedFrames {
179 /// Number of omitted frames.
180 count: usize,
181 /// The name of the crate whose frames were omitted.
182 skipped_crate: &'static str,
183 },
184}
185
186/// A single stack frame in a backtrace.
187///
188/// Represents one function call in the call stack, including symbol information
189/// and source location if available.
190#[derive(Debug)]
191pub struct Frame {
192 /// The demangled symbol name for this frame.
193 pub sym_demangled: String,
194 /// File path information for this frame, if available.
195 pub frame_path: Option<FramePath>,
196 /// Line number in the source file, if available.
197 pub lineno: Option<u32>,
198}
199
200/// File path information for a stack frame.
201///
202/// Contains the raw path and processed components for better display
203/// formatting.
204#[derive(Debug)]
205pub struct FramePath {
206 /// The raw file path from the debug information.
207 pub raw_path: String,
208 /// The crate name if detected from the path.
209 pub crate_name: Option<Cow<'static, str>>,
210 /// Common path prefix information for shortening display.
211 pub split_path: Option<FramePrefix>,
212}
213
214/// A common prefix for a frame path.
215///
216/// This struct represents a decomposed file path where a known prefix
217/// has been identified and separated from the rest of the path.
218#[derive(Debug)]
219pub struct FramePrefix {
220 /// The kind of prefix used to identify this prefix.
221 ///
222 /// Examples: `"RUST_SRC"` for Rust standard library paths,
223 /// `"CARGO"` for Cargo registry crate paths,
224 /// `"ROOTCAUSE"` for rootcause library paths.
225 pub prefix_kind: &'static str,
226 /// The full prefix path that was removed from the original path.
227 ///
228 /// Example: `"/home/user/.cargo/registry/src/index.crates.
229 /// io-1949cf8c6b5b557f"`
230 pub prefix: String,
231 /// The remaining path after the prefix was removed.
232 ///
233 /// Example: `"indexmap-2.12.1/src/map/core/entry.rs"`
234 pub suffix: String,
235}
236
237/// Handler for formatting [`Backtrace`] attachments.
238#[derive(Copy, Clone)]
239pub struct BacktraceHandler<const SHOW_FULL_PATH: bool>;
240
241fn get_function_name(s: &str) -> &str {
242 let mut word_start = 0usize;
243 let mut word_end = 0usize;
244 let mut angle_nesting_level = 0u64;
245 let mut curly_nesting_level = 0u64;
246 let mut potential_function_arrow = false;
247 let mut inside_word = false;
248
249 for (i, c) in s.char_indices() {
250 if curly_nesting_level == 0 && angle_nesting_level == 0 {
251 if !inside_word && unicode_ident::is_xid_start(c) {
252 word_start = i;
253 inside_word = true;
254 } else if inside_word && !unicode_ident::is_xid_continue(c) {
255 word_end = i;
256 inside_word = false;
257 }
258 }
259
260 let was_potential_function_arrow = potential_function_arrow;
261 potential_function_arrow = c == '-';
262
263 if c == '<' {
264 angle_nesting_level = angle_nesting_level.saturating_add(1);
265 } else if c == '>' && !was_potential_function_arrow {
266 angle_nesting_level = angle_nesting_level.saturating_sub(1);
267 } else if c == '{' {
268 curly_nesting_level = curly_nesting_level.saturating_add(1);
269 if !inside_word && curly_nesting_level == 1 && angle_nesting_level == 0 {
270 word_start = i;
271 inside_word = true;
272 }
273 } else if c == '}' {
274 curly_nesting_level = curly_nesting_level.saturating_sub(1);
275 if inside_word && curly_nesting_level == 0 {
276 word_end = i + 1;
277 inside_word = false;
278 }
279 }
280 }
281
282 if word_start < word_end {
283 &s[word_start..word_end]
284 } else {
285 // We started at word start but never found an end; return rest of string
286 &s[word_start..]
287 }
288}
289
290impl<const SHOW_FULL_PATH: bool> AttachmentHandler<Backtrace> for BacktraceHandler<SHOW_FULL_PATH> {
291 fn display(value: &Backtrace, f: &mut fmt::Formatter<'_>) -> fmt::Result {
292 const MAX_UNWRAPPED_SYM_LENGTH: usize = 25;
293 let mut max_seen_length = 0;
294 for entry in &value.entries {
295 if let BacktraceEntry::Frame(frame) = entry {
296 let sym = get_function_name(&frame.sym_demangled);
297 if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH && sym.len() > max_seen_length {
298 max_seen_length = sym.len();
299 }
300 }
301 }
302
303 for entry in &value.entries {
304 match entry {
305 BacktraceEntry::OmittedFrames {
306 count,
307 skipped_crate,
308 } => {
309 writeln!(
310 f,
311 "... omitted {count} frame(s) from crate '{skipped_crate}' ..."
312 )?;
313 continue;
314 }
315 BacktraceEntry::Frame(frame) => {
316 let sym = get_function_name(&frame.sym_demangled);
317
318 if sym.len() <= MAX_UNWRAPPED_SYM_LENGTH {
319 write!(f, "{:<max_seen_length$} - ", sym)?;
320 } else {
321 write!(f, "{sym}\n - ")?;
322 }
323
324 if let Some(path) = &frame.frame_path {
325 if SHOW_FULL_PATH {
326 write!(f, "{}", path.raw_path)?;
327 } else if let Some(split_path) = &path.split_path {
328 write!(f, "[..]/{}", split_path.suffix)?;
329 } else {
330 write!(f, "{}", path.raw_path)?;
331 }
332
333 if let Some(lineno) = frame.lineno {
334 write!(f, ":{lineno}")?;
335 }
336 }
337 writeln!(f)?;
338 }
339 }
340 }
341
342 if value.total_omitted_frames > 0 {
343 writeln!(
344 f,
345 "note: {} frame(s) omitted. For a complete backtrace, set RUST_BACKTRACE=full.",
346 value.total_omitted_frames
347 )?;
348 }
349
350 Ok(())
351 }
352
353 fn debug(value: &Backtrace, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
354 std::fmt::Debug::fmt(value, formatter)
355 }
356
357 fn preferred_formatting_style(
358 backtrace: &Backtrace,
359 _report_formatting_function: FormattingFunction,
360 ) -> AttachmentFormattingStyle {
361 AttachmentFormattingStyle {
362 placement: if backtrace.entries.is_empty() {
363 AttachmentFormattingPlacement::Hidden
364 } else {
365 AttachmentFormattingPlacement::InlineWithHeader {
366 header: "Backtrace",
367 }
368 },
369 priority: 10,
370 ..Default::default()
371 }
372 }
373}
374
375/// Attachment collector for capturing stack backtraces.
376///
377/// When registered as a report creation hook, this collector automatically
378/// captures the current stack backtrace and attaches it as a [`Backtrace`]
379/// attachment.
380///
381/// # Examples
382///
383/// Basic usage with default settings:
384///
385/// ```rust
386/// use rootcause::hooks::Hooks;
387/// use rootcause_backtrace::BacktraceCollector;
388///
389/// Hooks::new()
390/// .report_creation_hook(BacktraceCollector::new_from_env())
391/// .install()
392/// .expect("failed to install hooks");
393/// ```
394///
395/// Custom configuration:
396///
397/// ```rust
398/// use rootcause::hooks::Hooks;
399/// use rootcause_backtrace::{BacktraceCollector, BacktraceFilter};
400///
401/// let collector = BacktraceCollector {
402/// filter: BacktraceFilter {
403/// skipped_initial_crates: &[],
404/// skipped_middle_crates: &[],
405/// skipped_final_crates: &[],
406/// max_entry_count: 30,
407/// show_full_path: true,
408/// },
409/// capture_backtrace_for_reports_with_children: true,
410/// };
411///
412/// Hooks::new()
413/// .report_creation_hook(collector)
414/// .install()
415/// .expect("failed to install hooks");
416/// ```
417#[derive(Copy, Clone)]
418pub struct BacktraceCollector {
419 /// Configuration for filtering and formatting backtrace frames.
420 pub filter: BacktraceFilter,
421
422 /// If set to true, a backtrace is captured for every report creation,
423 /// including reports that have child reports (i.e., reports created with
424 /// existing children). If set to false, a backtrace is captured only
425 /// for reports created without any children. Reports created without
426 /// children always receive a backtrace regardless of this setting.
427 pub capture_backtrace_for_reports_with_children: bool,
428}
429
430/// Configuration for filtering frames from certain crates in a backtrace.
431///
432/// # Examples
433///
434/// Use default filtering:
435///
436/// ```rust
437/// use rootcause_backtrace::BacktraceFilter;
438///
439/// let filter = BacktraceFilter::DEFAULT;
440/// ```
441///
442/// Custom filtering to focus on application code:
443///
444/// ```rust
445/// use rootcause_backtrace::BacktraceFilter;
446///
447/// let filter = BacktraceFilter {
448/// // Hide rootcause crate frames at the start
449/// skipped_initial_crates: &["rootcause", "rootcause-backtrace"],
450/// // Hide framework frames in the middle
451/// skipped_middle_crates: &["tokio", "hyper", "tower"],
452/// // Hide runtime frames at the end
453/// skipped_final_crates: &["std", "tokio"],
454/// // Show only the most relevant 10 frames
455/// max_entry_count: 10,
456/// // Show shortened paths
457/// show_full_path: false,
458/// };
459/// ```
460#[derive(Copy, Clone, Debug)]
461pub struct BacktraceFilter {
462 /// Set of crate names whose frames should be hidden when they appear
463 /// at the beginning of a backtrace.
464 pub skipped_initial_crates: &'static [&'static str],
465 /// Set of crate names whose frames should be hidden when they appear
466 /// in the middle of a backtrace.
467 pub skipped_middle_crates: &'static [&'static str],
468 /// Set of crate names whose frames should be hidden when they appear
469 /// at the end of a backtrace.
470 pub skipped_final_crates: &'static [&'static str],
471 /// Maximum number of entries to include in the backtrace.
472 pub max_entry_count: usize,
473 /// Whether to show full file paths in the backtrace frames.
474 pub show_full_path: bool,
475}
476
477impl BacktraceFilter {
478 /// Default backtrace filter settings.
479 pub const DEFAULT: Self = Self {
480 skipped_initial_crates: &[
481 "backtrace",
482 "rootcause",
483 "rootcause-backtrace",
484 "core",
485 "std",
486 "alloc",
487 ],
488 skipped_middle_crates: &["std", "core", "alloc", "tokio"],
489 skipped_final_crates: &["std", "core", "alloc", "tokio"],
490 max_entry_count: 20,
491 show_full_path: false,
492 };
493}
494
495impl Default for BacktraceFilter {
496 fn default() -> Self {
497 Self::DEFAULT
498 }
499}
500
501#[derive(Debug)]
502struct RootcauseEnvOptions {
503 rust_backtrace_full: bool,
504 backtrace_leafs_only: bool,
505 show_full_path: bool,
506}
507
508impl RootcauseEnvOptions {
509 fn get() -> &'static Self {
510 static ROOTCAUSE_FLAGS: OnceLock<RootcauseEnvOptions> = OnceLock::new();
511
512 ROOTCAUSE_FLAGS.get_or_init(|| {
513 let rust_backtrace_full =
514 std::env::var_os("RUST_BACKTRACE").is_some_and(|var| var == "full");
515 let mut show_full_path = rust_backtrace_full;
516 let mut backtrace_leafs_only = false;
517 if let Some(var) = std::env::var_os("ROOTCAUSE_BACKTRACE") {
518 for v in var.to_string_lossy().split(',') {
519 if v.eq_ignore_ascii_case("leafs") {
520 backtrace_leafs_only = true;
521 } else if v.eq_ignore_ascii_case("full_paths") {
522 show_full_path = true;
523 }
524 }
525 }
526 RootcauseEnvOptions {
527 rust_backtrace_full,
528 backtrace_leafs_only,
529 show_full_path,
530 }
531 })
532 }
533}
534
535impl BacktraceCollector {
536 /// Creates a new [`BacktraceCollector`] with default settings.
537 ///
538 /// Configuration is controlled by environment variables. By default,
539 /// filtering is applied and backtraces are only captured for reports
540 /// without children.
541 ///
542 /// # Environment Variables
543 ///
544 /// - `RUST_BACKTRACE=full` - Disables all filtering and shows all frames
545 /// - `ROOTCAUSE_BACKTRACE` - Comma-separated options:
546 /// - `leafs` - Only capture backtraces for leaf errors (errors without
547 /// children)
548 /// - `full_paths` - Show full file paths instead of shortened paths
549 ///
550 /// The `RUST_BACKTRACE=full` setting implies `full_paths` unless explicitly
551 /// overridden by `ROOTCAUSE_BACKTRACE`.
552 ///
553 /// # Examples
554 ///
555 /// ```rust
556 /// use rootcause::hooks::Hooks;
557 /// use rootcause_backtrace::BacktraceCollector;
558 ///
559 /// // Respects RUST_BACKTRACE and ROOTCAUSE_BACKTRACE environment variables
560 /// Hooks::new()
561 /// .report_creation_hook(BacktraceCollector::new_from_env())
562 /// .install()
563 /// .expect("failed to install hooks");
564 /// ```
565 pub fn new_from_env() -> Self {
566 let env_options = RootcauseEnvOptions::get();
567 let capture_backtrace_for_reports_with_children = !env_options.backtrace_leafs_only;
568
569 Self {
570 filter: if env_options.rust_backtrace_full {
571 BacktraceFilter {
572 skipped_initial_crates: &[],
573 skipped_middle_crates: &[],
574 skipped_final_crates: &[],
575 max_entry_count: usize::MAX,
576 show_full_path: env_options.show_full_path,
577 }
578 } else {
579 BacktraceFilter {
580 show_full_path: env_options.show_full_path,
581 ..BacktraceFilter::DEFAULT
582 }
583 },
584 capture_backtrace_for_reports_with_children,
585 }
586 }
587}
588
589impl ReportCreationHook for BacktraceCollector {
590 fn on_local_creation(&self, mut report: ReportMut<'_, Dynamic, markers::Local>) {
591 let do_capture =
592 self.capture_backtrace_for_reports_with_children || report.children().is_empty();
593 if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
594 let attachment = if self.filter.show_full_path {
595 ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
596 } else {
597 ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
598 };
599 report.attachments_mut().push(attachment.into_dynamic());
600 }
601 }
602
603 fn on_sendsync_creation(&self, mut report: ReportMut<'_, Dynamic, markers::SendSync>) {
604 let do_capture =
605 self.capture_backtrace_for_reports_with_children || report.children().is_empty();
606 if do_capture && let Some(backtrace) = Backtrace::capture(&self.filter) {
607 let attachment = if self.filter.show_full_path {
608 ReportAttachment::new_custom::<BacktraceHandler<true>>(backtrace)
609 } else {
610 ReportAttachment::new_custom::<BacktraceHandler<false>>(backtrace)
611 };
612 report.attachments_mut().push(attachment.into_dynamic());
613 }
614 }
615}
616
617const fn get_rootcause_backtrace_matcher(
618 location: &'static Location<'static>,
619) -> Option<(&'static str, usize)> {
620 let file = location.file();
621
622 let Some(prefix_len) = file.len().checked_sub("/src/lib.rs".len()) else {
623 return None;
624 };
625
626 let (prefix, suffix) = file.split_at(prefix_len);
627 // Assert the suffix is /src/lib.rs (or \src\lib.rs on Windows)
628 // This is a compile-time check that the caller location is valid
629 if std::path::MAIN_SEPARATOR == '/' {
630 assert!(suffix.eq_ignore_ascii_case("/src/lib.rs"));
631 } else {
632 assert!(suffix.eq_ignore_ascii_case(r#"/src\lib.rs"#));
633 }
634
635 let (matcher_prefix, _) = file.split_at(prefix_len + 4);
636
637 let mut splitter_prefix = prefix;
638 while !splitter_prefix.is_empty() {
639 let (new_prefix, last_char) = splitter_prefix.split_at(splitter_prefix.len() - 1);
640 splitter_prefix = new_prefix;
641 if last_char.eq_ignore_ascii_case(std::path::MAIN_SEPARATOR_STR) {
642 break;
643 }
644 }
645
646 Some((matcher_prefix, splitter_prefix.len()))
647}
648
649const ROOTCAUSE_BACKTRACE_MATCHER: Option<(&str, usize)> =
650 get_rootcause_backtrace_matcher(Location::caller());
651const ROOTCAUSE_MATCHER: Option<(&str, usize)> =
652 get_rootcause_backtrace_matcher(rootcause::__private::ROOTCAUSE_LOCATION);
653
654impl Backtrace {
655 /// Captures the current stack backtrace, applying optional filtering.
656 pub fn capture(filter: &BacktraceFilter) -> Option<Self> {
657 let mut initial_filtering = !filter.skipped_initial_crates.is_empty();
658 let mut entries: Vec<BacktraceEntry> = Vec::new();
659 let mut total_omitted_frames = 0;
660
661 let mut delayed_omitted_frame: Option<Frame> = None;
662 let mut currently_omitted_crate_name: Option<&'static str> = None;
663 let mut currently_omitted_frames = 0;
664
665 backtrace::trace(|frame| {
666 backtrace::resolve_frame(frame, |symbol| {
667 // Don't consider frames without symbol names or filenames.
668 let (Some(sym), Some(filename_raw)) = (symbol.name(), symbol.filename_raw()) else {
669 return;
670 };
671
672 if entries.len() >= filter.max_entry_count {
673 total_omitted_frames += 1;
674 return;
675 }
676
677 let frame_path = FramePath::new(filename_raw);
678
679 if initial_filtering {
680 if let Some(cur_crate_name) = &frame_path.crate_name
681 && filter.skipped_initial_crates.contains(&&**cur_crate_name)
682 {
683 total_omitted_frames += 1;
684 return;
685 } else {
686 initial_filtering = false;
687 }
688 }
689
690 if let Some(cur_crate_name) = &frame_path.crate_name
691 && let Some(currently_omitted_crate_name) = ¤tly_omitted_crate_name
692 && cur_crate_name == currently_omitted_crate_name
693 {
694 delayed_omitted_frame = None;
695 currently_omitted_frames += 1;
696 total_omitted_frames += 1;
697 return;
698 }
699
700 if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
701 if let Some(delayed_frame) = delayed_omitted_frame.take() {
702 entries.push(BacktraceEntry::Frame(delayed_frame));
703 } else {
704 entries.push(BacktraceEntry::OmittedFrames {
705 count: currently_omitted_frames,
706 skipped_crate: currently_omitted_crate_name,
707 });
708 }
709 currently_omitted_frames = 0;
710 }
711
712 if let Some(cur_crate_name) = &frame_path.crate_name
713 && let Some(skipped_crate) = filter
714 .skipped_middle_crates
715 .iter()
716 .find(|&crate_name| crate_name == cur_crate_name)
717 {
718 currently_omitted_crate_name = Some(skipped_crate);
719 currently_omitted_frames = 1;
720 total_omitted_frames += 1;
721 delayed_omitted_frame = Some(Frame {
722 sym_demangled: format!("{sym:#}"),
723 frame_path: Some(frame_path),
724 lineno: symbol.lineno(),
725 });
726 return;
727 }
728
729 entries.push(BacktraceEntry::Frame(Frame {
730 sym_demangled: format!("{sym:#}"),
731 frame_path: Some(frame_path),
732 lineno: symbol.lineno(),
733 }));
734 });
735
736 true
737 });
738
739 if let Some(currently_omitted_crate_name) = currently_omitted_crate_name.take() {
740 if let Some(delayed_frame) = delayed_omitted_frame.take() {
741 entries.push(BacktraceEntry::Frame(delayed_frame));
742 } else {
743 entries.push(BacktraceEntry::OmittedFrames {
744 count: currently_omitted_frames,
745 skipped_crate: currently_omitted_crate_name,
746 });
747 }
748 }
749
750 while let Some(last) = entries.last() {
751 match last {
752 BacktraceEntry::Frame(frame) => {
753 let mut skip = false;
754 if let Some(frame_path) = &frame.frame_path
755 && let Some(crate_name) = &frame_path.crate_name
756 && filter.skipped_final_crates.contains(&&**crate_name)
757 {
758 skip = true;
759 } else if frame.sym_demangled == "__libc_start_call_main"
760 || frame.sym_demangled == "__libc_start_main_impl"
761 {
762 skip = true;
763 } else if let Some(frame_path) = &frame.frame_path
764 && frame.sym_demangled == "_start"
765 && frame_path.raw_path.contains("zig/libc/glibc")
766 {
767 skip = true;
768 }
769
770 if skip {
771 total_omitted_frames += 1;
772 entries.pop();
773 } else {
774 break;
775 }
776 }
777 BacktraceEntry::OmittedFrames {
778 skipped_crate,
779 count,
780 } => {
781 if filter.skipped_final_crates.contains(skipped_crate) {
782 total_omitted_frames += count;
783 entries.pop();
784 } else {
785 break;
786 }
787 }
788 }
789 }
790
791 if entries.is_empty() && total_omitted_frames == 0 {
792 None
793 } else {
794 Some(Self {
795 entries,
796 total_omitted_frames,
797 })
798 }
799 }
800}
801
802impl FramePath {
803 fn new(path: BytesOrWideString<'_>) -> Self {
804 static REGEXES: OnceLock<[regex::Regex; 2]> = OnceLock::new();
805 let [std_regex, registry_regex] = REGEXES.get_or_init(|| {
806 [
807 // Matches Rust standard library paths:
808 // - /lib/rustlib/src/rust/library/{std|core|alloc}/src/...
809 // - /rustc/{40-char-hash}/library/{std|core|alloc}/src/...
810 regex::Regex::new(
811 r"(?:/lib/rustlib/src/rust|^/rustc/[0-9a-f]{40})/library/(std|core|alloc)/src/.*$",
812 )
813 .expect("built-in regex pattern for std library paths should be valid"),
814 // Matches Cargo registry paths:
815 // - /.cargo/registry/src/{index}-{16-char-hash}/{crate}-{version}/src/...
816 regex::Regex::new(
817 r"/\.cargo/registry/src/[^/]+-[0-9a-f]{16}/([^./]+)-[0-9]+\.[^/]*/src/.*$",
818 )
819 .expect("built-in regex pattern for cargo registry paths should be valid"),
820 ]
821 });
822
823 let path_str = path.to_string();
824
825 if let Some(captures) = std_regex.captures(&path_str) {
826 let raw_path = path.to_str_lossy().into_owned();
827 let crate_capture = captures
828 .get(1)
829 .expect("regex capture group 1 should exist for std library paths");
830 let split = crate_capture.start();
831 let (prefix, suffix) = (&path_str[..split - 1], &path_str[split..]);
832
833 Self {
834 raw_path,
835 split_path: Some(FramePrefix {
836 prefix_kind: "RUST_SRC",
837 prefix: prefix.to_string(),
838 suffix: suffix.to_string(),
839 }),
840 crate_name: Some(crate_capture.as_str().to_string().into()),
841 }
842 } else if let Some(captures) = registry_regex.captures(&path_str) {
843 let raw_path = path.to_str_lossy().into_owned();
844 let crate_capture = captures
845 .get(1)
846 .expect("regex capture group 1 should exist for cargo registry paths");
847 let split = crate_capture.start();
848 let (prefix, suffix) = (&path_str[..split - 1], &path_str[split..]);
849
850 Self {
851 raw_path,
852 crate_name: Some(crate_capture.as_str().to_string().into()),
853 split_path: Some(FramePrefix {
854 prefix_kind: "CARGO",
855 prefix: prefix.to_string(),
856 suffix: suffix.to_string(),
857 }),
858 }
859 } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
860 ROOTCAUSE_MATCHER
861 && path_str.starts_with(rootcause_matcher_prefix)
862 {
863 let raw_path = path.to_str_lossy().into_owned();
864 let (prefix, suffix) = (
865 &path_str[..rootcause_splitter_prefix_len],
866 &path_str[rootcause_splitter_prefix_len + 1..],
867 );
868 Self {
869 raw_path,
870 split_path: Some(FramePrefix {
871 prefix_kind: "ROOTCAUSE",
872 prefix: prefix.to_string(),
873 suffix: suffix.to_string(),
874 }),
875 crate_name: Some(Cow::Borrowed("rootcause")),
876 }
877 } else if let Some((rootcause_matcher_prefix, rootcause_splitter_prefix_len)) =
878 ROOTCAUSE_BACKTRACE_MATCHER
879 && path_str.starts_with(rootcause_matcher_prefix)
880 {
881 let raw_path = path.to_str_lossy().into_owned();
882 let (prefix, suffix) = (
883 &path_str[..rootcause_splitter_prefix_len],
884 &path_str[rootcause_splitter_prefix_len + 1..],
885 );
886 Self {
887 raw_path,
888 split_path: Some(FramePrefix {
889 prefix_kind: "ROOTCAUSE",
890 prefix: prefix.to_string(),
891 suffix: suffix.to_string(),
892 }),
893 crate_name: Some(Cow::Borrowed("rootcause-backtrace")),
894 }
895 } else {
896 let raw_path = path.to_str_lossy().into_owned();
897 Self {
898 raw_path,
899 crate_name: None,
900 split_path: None,
901 }
902 }
903 }
904}
905
906/// Extension trait for attaching backtraces to reports.
907///
908/// This trait provides methods to easily attach a captured backtrace to a
909/// report or to the error contained within a `Result`.
910///
911/// # Examples
912///
913/// Attach backtrace to a report:
914///
915/// ```rust
916/// use std::io;
917///
918/// use rootcause::report;
919/// use rootcause_backtrace::BacktraceExt;
920///
921/// let report = report!(io::Error::other("An error occurred")).attach_backtrace();
922/// ```
923///
924/// Attach backtrace to a `Result`:
925///
926/// ```rust
927/// use std::io;
928///
929/// use rootcause::{Report, report};
930/// use rootcause_backtrace::BacktraceExt;
931///
932/// fn might_fail() -> Result<(), Report> {
933/// Err(report!(io::Error::other("operation failed")).into_dynamic())
934/// }
935///
936/// let result = might_fail().attach_backtrace();
937/// ```
938///
939/// Use a custom filter:
940///
941/// ```rust
942/// use std::io;
943///
944/// use rootcause::report;
945/// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
946///
947/// let filter = BacktraceFilter {
948/// skipped_initial_crates: &[],
949/// skipped_middle_crates: &[],
950/// skipped_final_crates: &[],
951/// max_entry_count: 50,
952/// show_full_path: true,
953/// };
954///
955/// let report = report!(io::Error::other("detailed error")).attach_backtrace_with_filter(&filter);
956/// ```
957pub trait BacktraceExt: Sized {
958 /// Attaches a captured backtrace to the report using the default filter.
959 ///
960 /// # Examples
961 ///
962 /// ```rust
963 /// use std::io;
964 ///
965 /// use rootcause::report;
966 /// use rootcause_backtrace::BacktraceExt;
967 ///
968 /// let report = report!(io::Error::other("error")).attach_backtrace();
969 /// ```
970 fn attach_backtrace(self) -> Self {
971 self.attach_backtrace_with_filter(&BacktraceFilter::DEFAULT)
972 }
973
974 /// Attaches a captured backtrace to the report using the specified filter.
975 ///
976 /// # Examples
977 ///
978 /// ```rust
979 /// use std::io;
980 ///
981 /// use rootcause::report;
982 /// use rootcause_backtrace::{BacktraceExt, BacktraceFilter};
983 ///
984 /// let filter = BacktraceFilter {
985 /// max_entry_count: 10,
986 /// ..BacktraceFilter::DEFAULT
987 /// };
988 ///
989 /// let report = report!(io::Error::other("error")).attach_backtrace_with_filter(&filter);
990 /// ```
991 fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self;
992}
993
994impl<C: ?Sized, T> BacktraceExt for Report<C, markers::Mutable, T>
995where
996 Backtrace: ObjectMarkerFor<T>,
997{
998 fn attach_backtrace_with_filter(mut self, filter: &BacktraceFilter) -> Self {
999 if let Some(backtrace) = Backtrace::capture(&filter) {
1000 if filter.show_full_path {
1001 self = self.attach_custom::<BacktraceHandler<true>, _>(backtrace);
1002 } else {
1003 self = self.attach_custom::<BacktraceHandler<false>, _>(backtrace);
1004 }
1005 }
1006 self
1007 }
1008}
1009
1010impl<C: ?Sized, V, T> BacktraceExt for Result<V, Report<C, markers::Mutable, T>>
1011where
1012 Backtrace: ObjectMarkerFor<T>,
1013{
1014 fn attach_backtrace_with_filter(self, filter: &BacktraceFilter) -> Self {
1015 match self {
1016 Ok(v) => Ok(v),
1017 Err(report) => Err(report.attach_backtrace_with_filter(filter)),
1018 }
1019 }
1020}