Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.12.0] - Unreleased

### Added

- Added support for formatting the error sources for a context [#94](https://github.com/rootcause-rs/rootcause/pull/94).
- This adds new fields to the `ContextFormattingStyle` and the `DefaultReportFormatter`.

## [0.11.0] - 2025-12-12

Expand Down Expand Up @@ -151,7 +156,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial release

[Unreleased]: https://github.com/rootcause-rs/rootcause/compare/v0.11.0...HEAD
[0.12.0]: https://github.com/rootcause-rs/rootcause/compare/v0.11.0...HEAD
[0.11.0]: https://github.com/rootcause-rs/rootcause/compare/v0.10.0...v0.11.0
[0.10.0]: https://github.com/rootcause-rs/rootcause/compare/v0.9.1...v0.10.0
[0.9.1]: https://github.com/rootcause-rs/rootcause/compare/v0.9.0...v0.9.1
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rootcause"
version = "0.11.0"
version = "0.12.0"
edition = "2024"
license = "MIT/Apache-2.0"
categories = ["rust-patterns", "no-std"]
Expand Down Expand Up @@ -35,7 +35,7 @@ error-stack05 = { package = "error-stack", version = "0.5.0", default-features =
eyre = { version = "0.6.12", default-features = false, optional = true }

# Internal dependencies
rootcause-internals = { path = "rootcause-internals", version = "=0.11.0" }
rootcause-internals = { path = "rootcause-internals", version = "=0.12.0" }

[dev-dependencies]
derive_more = { version = "2.1.0", default-features = false, features = [
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ Once you're comfortable with the basics, rootcause offers powerful features for
- [`retry_with_collection.rs`](examples/retry_with_collection.rs) - Collecting multiple retry attempts
- [`batch_processing.rs`](examples/batch_processing.rs) - Gathering errors from parallel operations
- [`inspecting_errors.rs`](examples/inspecting_errors.rs) - Programmatic tree traversal and data extraction for analytics
- [`following_error_sources.rs`](examples/following_error_sources.rs) - Displaying full error source chains for debugging third-party library errors
- [`custom_handler.rs`](examples/custom_handler.rs) - Customizing error formatting and data collection
- [`formatting_hooks.rs`](examples/formatting_hooks.rs) - Advanced formatting customization

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Demonstrations of rootcause features and patterns.
## Hooks & Formatting

- [`formatting_hooks.rs`](formatting_hooks.rs) - Global formatting overrides: placement, priority, custom context display
- [`following_error_sources.rs`](following_error_sources.rs) - Display full error source chains: useful for debugging third-party library errors with deep cause chains
- [`report_creation_hook.rs`](report_creation_hook.rs) - Automatic attachment on creation: simple collectors vs conditional logic
- [`conditional_formatting.rs`](conditional_formatting.rs) - Conditional formatting based on runtime context (environment, feature flags, etc.)

Expand Down
148 changes: 148 additions & 0 deletions examples/following_error_sources.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! Demonstrates how to display error source chains in reports.
//!
//! By default, rootcause only displays the immediate error context. This
//! example shows how to enable source chain traversal to display the full error
//! chain, providing better diagnostic information.

use rootcause::{
ReportRef,
hooks::{Hooks, context_formatter::ContextFormatterHook},
markers::{Dynamic, Local, Uncloneable},
prelude::*,
};
use rootcause_internals::handlers::{ContextFormattingStyle, ContextHandler, FormattingFunction};

// A simple error type that can chain to other errors
#[derive(Debug)]
struct ChainedError {
message: String,
source: Option<Box<dyn std::error::Error + Send + Sync>>,
}

impl ChainedError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
source: None,
}
}

fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
}

impl std::fmt::Display for ChainedError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.message)
}
}

impl std::error::Error for ChainedError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if let Some(source) = &self.source {
Some(&**source)
} else {
None
}
}
}

fn main() {
// Create an error with a source chain
let error = ChainedError::new("request failed").with_source(
ChainedError::new("connection error")
.with_source(ChainedError::new("TLS handshake failed")),
);

println!("=== Method 1: Using a ContextHandler ===\n");
println!("Use this approach when the error type itself should control formatting.");
println!("Best for:");
println!(" • Library authors defining how their error types are displayed");
println!(" • Different behavior for different error types");
println!(" • When the decision is inherent to what the error represents\n");

// Define a custom handler that enables source chain following.
// This associates the behavior directly with the ChainedError type.
struct ErrorWithSourcesHandler;
impl ContextHandler<ChainedError> for ErrorWithSourcesHandler {
fn source(value: &ChainedError) -> Option<&(dyn std::error::Error + 'static)> {
std::error::Error::source(value)
}

fn display(
value: &ChainedError,
formatter: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
std::fmt::Display::fmt(value, formatter)
}

fn debug(
value: &ChainedError,
formatter: &mut std::fmt::Formatter<'_>,
) -> std::fmt::Result {
std::fmt::Debug::fmt(value, formatter)
}

fn preferred_formatting_style(
_value: &ChainedError,
formatting_function: FormattingFunction,
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
// Enable source chain traversal
follow_source: true,
// Note: Set follow_source_depth to Some(n) to limit chain depth
// No depth limit (show all)
follow_source_depth: None,
}
}
}

let report = Report::new_sendsync_custom::<ErrorWithSourcesHandler>(error)
.context("Failed to fetch data");
println!("{report}");

println!("\n=== Method 2: Using a ContextFormatterHook ===\n");
println!("Use this approach for application-wide configuration of a specific type.");
println!("Best for:");
println!(" • Configuring third-party error types you don't control");
println!(" • Environment-based behavior (dev vs production)");
println!(" • Changing formatting without modifying where errors are created");
println!(" • Centralizing configuration instead of specifying at each creation site\n");

// Install a global hook that enables source chain following for ChainedError.
// Unlike the handler approach, this applies to ALL ChainedError instances
// in your application, even when created with Report::new() instead of
// Report::new_sendsync_custom().
struct ChainedErrorFormatter;
impl ContextFormatterHook<ChainedError> for ChainedErrorFormatter {
fn preferred_context_formatting_style(
&self,
_report: ReportRef<'_, Dynamic, Uncloneable, Local>,
_report_formatting_function: FormattingFunction,
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: FormattingFunction::Display,
follow_source: true,
follow_source_depth: None,
}
}
}

Hooks::new()
.context_formatter::<ChainedError, _>(ChainedErrorFormatter)
.install()
.ok();

// Create a new error (same structure as before)
let error2 = ChainedError::new("request failed").with_source(
ChainedError::new("connection error")
.with_source(ChainedError::new("TLS handshake failed")),
);

// No custom handler needed - the hook applies globally
let report2 = report!(error2).context("Failed to fetch data");
println!("{report2}");
}
12 changes: 3 additions & 9 deletions rootcause-backtrace/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
[package]
name = "rootcause-backtrace"
version = "0.11.0"
version = "0.12.0"
edition = "2024"
license = "MIT/Apache-2.0"
categories = ["rust-patterns"]
keywords = [
"error",
"error-handling",
"ergonomic",
"library",
"backtrace",
]
keywords = ["error", "error-handling", "ergonomic", "library", "backtrace"]
description = "Backtraces support for the rootcause error reporting library"
repository = "https://github.com/rootcause-rs/rootcause"
documentation = "https://docs.rs/rootcause-backtrace"
Expand All @@ -25,4 +19,4 @@ regex = { version = "1.12.2", default-features = false }
unicode-ident = "1.0.22"

# Internal dependencies
rootcause = { path = "../", version = "=0.11.0" }
rootcause = { path = "../", version = "=0.12.0" }
2 changes: 1 addition & 1 deletion rootcause-internals/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "rootcause-internals"
version = "0.11.0"
version = "0.12.0"
edition = "2024"
license = "MIT/Apache-2.0"
description = "Internals for the rootcause crate"
Expand Down
8 changes: 8 additions & 0 deletions rootcause-internals/src/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ pub trait ContextHandler<C>: 'static {
/// // Use the same formatting as the report
/// ContextFormattingStyle {
/// function: report_formatting_function,
/// follow_source: false,
/// follow_source_depth: None,
/// }
/// }
/// }
Expand Down Expand Up @@ -433,12 +435,18 @@ pub trait AttachmentHandler<A>: 'static {
/// // Explicitly request debug formatting
/// let debug_style = ContextFormattingStyle {
/// function: FormattingFunction::Debug,
/// follow_source: false,
/// follow_source_depth: None,
/// };
/// ```
#[derive(Copy, Clone, Debug, Default)]
pub struct ContextFormattingStyle {
/// The preferred formatting function to use
pub function: FormattingFunction,
/// Whether to follow the source chain when formatting
pub follow_source: bool,
/// The maximum depth to follow the source chain when formatting
pub follow_source_depth: Option<usize>,
}

/// Formatting preferences for an attachment when displayed in a report.
Expand Down
2 changes: 2 additions & 0 deletions src/compat/anyhow1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ impl ContextHandler<anyhow::Error> for AnyhowHandler {
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
follow_source: false,
follow_source_depth: None,
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/compat/boxed_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ impl ContextHandler<Box<dyn Error + Send + Sync>> for BoxedErrorHandler {
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
follow_source: false,
follow_source_depth: None,
}
}
}
Expand Down Expand Up @@ -197,6 +199,8 @@ impl ContextHandler<Box<dyn Error>> for BoxedErrorHandler {
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
follow_source: false,
follow_source_depth: None,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/compat/error_stack05.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ where
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
follow_source: false,
follow_source_depth: None,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/compat/error_stack06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ where
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
follow_source: false,
follow_source_depth: None,
}
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/compat/eyre06.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ impl ContextHandler<eyre::Report> for EyreHandler {
) -> ContextFormattingStyle {
ContextFormattingStyle {
function: formatting_function,
follow_source: false,
follow_source_depth: None,
}
}
}
Expand Down
Loading