Skip to content
Draft
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
7 changes: 7 additions & 0 deletions Cargo.lock

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

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ documentation = "https://docs.rs/rootcause"
rust-version = "1.89"

[workspace]
members = ["rootcause-internals", "rootcause-backtrace", "rootcause-tracing"]
members = [
"rootcause-internals",
"rootcause-backtrace",
"rootcause-tracing",
"rootcause-preformat",
]

[features]
default = []
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ rootcause is designed to be lightweight and extensible. The core library provide

- **[`rootcause-backtrace`](https://docs.rs/rootcause-backtrace)** - Automatic stack trace capture for debugging. Install hooks to attach backtraces to all errors, or use the extension trait to add them selectively.
- **[`rootcause-tracing`](https://docs.rs/rootcause-tracing)** - Tracing span capture for error reports. Automatically capture and display the active tracing spans when errors occur, providing operation context especially useful in async code.
- **[`rootcause-preformat`](https://docs.rs/rootcause-preformat)** - Methods for preformatting a reports and attachments. This is mostly useful when you need to go from a `Local` report to a `SendSync` report.

## Next Steps

Expand Down Expand Up @@ -302,6 +303,7 @@ The rootcause ecosystem consists of multiple crates:

- **`rootcause-backtrace`** - Optional backtrace capture support. Provides hooks for automatic stack trace collection.
- **`rootcause-tracing`** - Optional tracing span capture. Provides hooks to attach active tracing spans to error reports.
- **`rootcause-preformat`** - Provides extension traits for preformatting a report in order to go from `Local` objects to a `SendSync` ones.

The split between `rootcause` and `rootcause-internals` provides a clean API boundary: internals define how data is stored, while the main crate ensures that storage is accessed safely through Rust's type system. This makes it easy to understand the underlying representation while keeping the safe API ergonomic. Extensions integrate via the hook system without requiring changes to core.

Expand Down
5 changes: 2 additions & 3 deletions examples/conditional_formatting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ use std::env;

use rootcause::{
hooks::{Hooks, attachment_formatter::AttachmentFormatterHook},
markers::Dynamic,
prelude::*,
report_attachment::ReportAttachmentRef,
};
Expand Down Expand Up @@ -46,7 +45,7 @@ struct CredentialsFormatter;
impl AttachmentFormatterHook<ApiCredentials> for CredentialsFormatter {
fn preferred_formatting_style(
&self,
_attachment: ReportAttachmentRef<'_, Dynamic>,
_attachment: ReportAttachmentRef<'_, ApiCredentials>,
report_formatting_function: rootcause::handlers::FormattingFunction,
) -> rootcause::handlers::AttachmentFormattingStyle {
use rootcause::handlers::{
Expand Down Expand Up @@ -101,7 +100,7 @@ struct DebugSnapshotFormatter;
impl AttachmentFormatterHook<DebugSnapshot> for DebugSnapshotFormatter {
fn preferred_formatting_style(
&self,
_attachment: ReportAttachmentRef<'_, Dynamic>,
_attachment: ReportAttachmentRef<'_, DebugSnapshot>,
report_formatting_function: rootcause::handlers::FormattingFunction,
) -> rootcause::handlers::AttachmentFormattingStyle {
use rootcause::handlers::{
Expand Down
19 changes: 10 additions & 9 deletions examples/context_methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
//! - `context()`: Wraps report as child under new context
//! - `context_to()`: Uses `ReportConversion` trait implementation
//! - `context_transform()`: Changes context type in-place
//! - `context_transform_nested()`: Preformats and wraps as child
//! - Cloning the context and using `context()` to preserve multiple locations
//!
//! The focus is on understanding **what each method does to the report
//! structure** and **what information is preserved or lost**.

use rootcause::{ReportConversion, markers, preformatted::PreformattedContext, prelude::*};
use rootcause::{ReportConversion, markers, prelude::*};

#[derive(Debug)]
enum AppError {
Expand All @@ -30,12 +30,12 @@ impl std::fmt::Display for AppError {
impl<T> ReportConversion<std::num::ParseIntError, markers::Mutable, T> for AppError
where
AppError: markers::ObjectMarkerFor<T>,
rootcause::preformatted::PreformattedContext: markers::ObjectMarkerFor<T>,
{
fn convert_report(
report: Report<std::num::ParseIntError, markers::Mutable, T>,
) -> Report<Self, markers::Mutable, T> {
report.context_transform_nested(AppError::Parse)
let current_context = report.current_context().clone();
report.context(AppError::Parse(current_context))
}
}

Expand All @@ -61,16 +61,17 @@ fn main() {
println!("{report2}\n");
assert_eq!(report2.iter_sub_reports().count(), 0);

// context_transform_nested() - Creates new parent node, child preformatted
// (type lost)
// Clones the context to preserve multiple locations, while preserving the original
// context type
println!("Using context_transform_nested():");
let report3: Report<AppError> =
parse_error("not_a_number").context_transform_nested(AppError::Parse);
let report3 = parse_error("not_a_number");
let report3_context = report3.current_context().clone();
let report3 = report3.context(AppError::Parse(report3_context));
println!("{report3}\n");
assert_eq!(report3.iter_sub_reports().count(), 1);
assert_eq!(
report3.children().get(0).unwrap().current_context_type_id(),
std::any::TypeId::of::<PreformattedContext>()
std::any::TypeId::of::<std::num::ParseIntError>()
);

// context_to() - Uses ReportConversion impl (context_transform_nested in this
Expand Down
11 changes: 7 additions & 4 deletions examples/derive_more_interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use derive_more::{Display, Error, From};
use rootcause::{ReportConversion, markers, prelude::*};

// Shared error types used across patterns
#[derive(Error, Debug, Display)]
#[derive(Clone, Error, Debug, Display)]
#[expect(dead_code, reason = "example code")]
enum DatabaseError {
#[display("Connection timeout after {seconds}s")]
Expand Down Expand Up @@ -96,10 +96,13 @@ fn process_early_report(user_id: u32) -> Result<String, Report<AppError2>> {
Ok(data)
}

// If we want to capture multiple locations, we can use context_transform_nested
// instead
// If we want to capture multiple locations, we can clone the context. Alternatively we could use
// the extensions in rootcause-preformat.
fn process_early_report_multiple_locations(user_id: u32) -> Result<String, Report<AppError2>> {
let data = query_report(user_id).context_transform_nested(AppError2::Database)?;
let data = query_report(user_id).map_err(|report| {
let current_context = report.current_context().clone();
report.context(AppError2::Database(current_context))
})?;
Ok(data)
}

Expand Down
4 changes: 2 additions & 2 deletions examples/following_error_sources.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
use rootcause::{
ReportRef,
hooks::{Hooks, context_formatter::ContextFormatterHook},
markers::{Dynamic, Local, Uncloneable},
markers::{Local, Uncloneable},
prelude::*,
};
use rootcause_internals::handlers::{ContextFormattingStyle, FormattingFunction};
Expand All @@ -35,7 +35,7 @@ struct ReqwestErrorFormatter;
impl ContextFormatterHook<reqwest::Error> for ReqwestErrorFormatter {
fn preferred_context_formatting_style(
&self,
_report: ReportRef<'_, Dynamic, Uncloneable, Local>,
_report: ReportRef<'_, reqwest::Error, Uncloneable, Local>,
report_formatting_function: FormattingFunction,
) -> ContextFormattingStyle {
ContextFormattingStyle {
Expand Down
6 changes: 3 additions & 3 deletions examples/formatting_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use rootcause::{
Hooks, attachment_formatter::AttachmentFormatterHook,
context_formatter::ContextFormatterHook,
},
markers::{Dynamic, Local, Uncloneable},
markers::{Local, Uncloneable},
prelude::*,
report_attachment::ReportAttachmentRef,
};
Expand Down Expand Up @@ -54,7 +54,7 @@ struct DatabaseQueryFormatter;
impl AttachmentFormatterHook<DatabaseQuery> for DatabaseQueryFormatter {
fn preferred_formatting_style(
&self,
_attachment: ReportAttachmentRef<'_, Dynamic>,
_attachment: ReportAttachmentRef<'_, DatabaseQuery>,
formatting_function: FormattingFunction,
) -> AttachmentFormattingStyle {
match formatting_function {
Expand Down Expand Up @@ -100,7 +100,7 @@ struct ActionRequiredFormatter;
impl AttachmentFormatterHook<ActionRequired> for ActionRequiredFormatter {
fn preferred_formatting_style(
&self,
_attachment: ReportAttachmentRef<'_, Dynamic>,
_attachment: ReportAttachmentRef<'_, ActionRequired>,
_report_formatting_function: FormattingFunction,
) -> AttachmentFormattingStyle {
AttachmentFormattingStyle {
Expand Down
11 changes: 7 additions & 4 deletions examples/thiserror_interop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use rootcause::{ReportConversion, markers, prelude::*};
use thiserror::Error;

// Shared error types used across patterns
#[derive(Error, Debug)]
#[derive(Clone, Error, Debug)]
#[expect(dead_code, reason = "example code")]
enum DatabaseError {
#[error("Connection timeout after {seconds}s")]
Expand Down Expand Up @@ -96,10 +96,13 @@ fn process_early_report(user_id: u32) -> Result<String, Report<AppError2>> {
Ok(data)
}

// If we want to capture multiple locations, we can use context_transform_nested
// instead
// If we want to capture multiple locations, we can clone the context. Alternatively we could use
// the extensions in rootcause-preformat.
fn process_early_report_multiple_locations(user_id: u32) -> Result<String, Report<AppError2>> {
let data = query_report(user_id).context_transform_nested(AppError2::Database)?;
let data = query_report(user_id).map_err(|report| {
let current_context = report.current_context().clone();
report.context(AppError2::Database(current_context))
})?;
Ok(data)
}

Expand Down
16 changes: 16 additions & 0 deletions rootcause-preformat/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "rootcause-preformat"
version = "0.12.1"
edition = "2024"
license = "MIT/Apache-2.0"
categories = ["rust-patterns", "development-tools::debugging"]
keywords = ["error", "error-handling", "tracing", "spans", "observability"]
description = "Preformatting support for the rootcause error reporting library"
repository = "https://github.com/rootcause-rs/rootcause"
documentation = "https://docs.rs/rootcause-preformat"
rust-version = "1.89"

[dependencies]

# Internal dependencies
rootcause = { path = "../", version = "=0.12.1" }
Loading
Loading