Skip to main content

charon_error/report/
error_report.rs

1//! The core [`ErrorReport`] type that collects a chain of error frames.
2//!
3//! An `ErrorReport` accumulates [`ErrorFrame`](crate::ErrorFrame)s from
4//! the original trigger up to the top-level context. It supports
5//! formatting, attachment, and conversion from standard errors and `anyhow`.
6
7use crate::{
8    ERGlobalSettings, ErrorAttachment, ErrorFmt, ErrorFmtLoD, ErrorFmtSettings, ErrorFormatObj,
9    ErrorFrame, ErrorSensitivityLabel, IndentationStyle, ResultER, ResultExt, StringError, map,
10};
11use colored::*;
12use std::error::Error;
13use std::fmt::{Debug, Display};
14use tracing::instrument;
15
16/// The core error type that collects a chain of [`ErrorFrame`]s.
17///
18/// An `ErrorReport` holds an ordered list of error frames, from the most specific
19/// (the original trigger) to the most general (the top-level context). Each frame
20/// captures the error, source location, timestamp, backtrace, tracing span, and
21/// optional attachments.
22///
23/// # Creating an ErrorReport
24///
25/// ```rust
26/// use charon_error::{ErrorReport, StringError};
27///
28/// let report = ErrorReport::from_error(StringError::new("disk full"));
29/// ```
30///
31/// # Chaining errors
32///
33/// ```rust
34/// use charon_error::{ErrorReport, StringError};
35///
36/// let report = ErrorReport::from_error(StringError::new("io error"))
37///     .push_error(StringError::new("failed to save file"));
38/// ```
39///
40/// # Using with `Result`
41///
42/// Use [`ResultER<T>`](crate::ResultER) and the [`ResultExt`](crate::ResultExt)
43/// trait for ergonomic error handling:
44///
45/// ```rust,no_run
46/// use charon_error::prelude::*;
47///
48/// fn read_file() -> ResultER<String> {
49///     std::fs::read_to_string("data.txt")
50///         .change_context(StringError::new("failed to read data file"))
51/// }
52/// ```
53#[derive(Debug)]
54#[must_use = "error reports must be used or returned"]
55pub struct ErrorReport {
56    /// All error frames listed from most specific error (trigger)
57    /// to most general error (effected).
58    pub frames: Vec<ErrorFrame>,
59    /// Unique error id to identify this error report.
60    unique_id: String,
61}
62
63impl ErrorFmt for ErrorReport {
64    #[instrument]
65    fn create_format_obj(&self, settings: ErrorFmtSettings) -> ResultER<ErrorFormatObj> {
66        let object = ErrorFormatObj::Object(map! {
67            "last_error" => ErrorFormatObj::ColorString(self.get_last_error_title().bright_red().bold()),
68            "unique_id" => ErrorFormatObj::String(self.unique_id.clone()),
69            "frames" => ErrorFormatObj::Array(self.frames.iter().map(|f| f.create_format_obj(settings)).collect::<ResultER<Vec<_>>>()?),
70        });
71        Ok(match settings.level_of_detail {
72            ErrorFmtLoD::Compact => object.filter_object_fields(vec!["last_error", "frames"])?,
73            ErrorFmtLoD::Medium | ErrorFmtLoD::SubmitReport => {
74                object.filter_object_fields(vec!["last_error", "frames", "unique_id"])?
75            }
76            ErrorFmtLoD::Full | ErrorFmtLoD::Debug => object,
77        })
78    }
79}
80
81impl ErrorReport {
82    /// Create an `ErrorReport` from any error type.
83    ///
84    /// This is the primary constructor. The error is wrapped in an [`ErrorFrame`]
85    /// that captures the source location, backtrace, and current tracing span.
86    #[track_caller]
87    #[must_use = "the newly created report must be used"]
88    pub fn from_error<E: Error + Send + Sync + 'static>(error: E) -> Self {
89        Self {
90            frames: vec![ErrorFrame::new(error)],
91            unique_id: Self::create_new_unique_id(),
92        }
93    }
94
95    /// Create an `ErrorReport` from a boxed dynamic error.
96    #[track_caller]
97    #[must_use = "the newly created report must be used"]
98    pub fn from_dyn_error(error: Box<dyn Error + Send + Sync + 'static>) -> Self {
99        Self {
100            frames: vec![ErrorFrame::from_dyn_error(error)],
101            unique_id: Self::create_new_unique_id(),
102        }
103    }
104
105    /// Create an `ErrorReport` from an `anyhow::Error` reference.
106    ///
107    /// Walks the error chain and creates a frame for each cause.
108    #[track_caller]
109    #[must_use = "the newly created report must be used"]
110    pub fn from_anyhow_error_ref(error: &anyhow::Error) -> Self {
111        // Can not use closure here because of `#[track_caller]`
112        let errors: Vec<_> = error.chain().collect();
113        let mut frames: Vec<ErrorFrame> = vec![];
114        for cause in errors {
115            frames.push(ErrorFrame::new(StringError::from_error(cause)));
116        }
117        if frames.is_empty() {
118            std::panic::panic_any(StringError::new(
119                "Calling `ErrorReport::from_anyhow_error_ref()` on anyhow::Error with no causes. \
120                This should not be possible, please report.",
121            ));
122        }
123        Self {
124            frames,
125            unique_id: "".to_owned(),
126        }
127    }
128
129    /// Add a new error to the chain, providing higher-level context.
130    ///
131    /// Returns `self` for chaining.
132    #[track_caller]
133    #[must_use = "the modified report with the new error must be used"]
134    pub fn push_error<R: Error + Send + Sync + 'static>(mut self, new_error: R) -> Self {
135        self.frames.push(ErrorFrame::new(new_error));
136        self
137    }
138
139    /// Attach labeled data to the most recent error frame.
140    ///
141    /// Attachments are key-value pairs wrapped in an [`ErrorSensitivityLabel`]
142    /// to control visibility in reports.
143    ///
144    /// # Panics
145    ///
146    /// Panics if the report has no frames.
147    /// This should not be possible, if it happens this is considered a bug inside `charon-error` itself.
148    #[track_caller]
149    #[must_use = "the modified report with the attachment must be used"]
150    #[instrument(skip(key_name, attachment))]
151    pub fn attach<S: Into<String>>(
152        mut self,
153        key_name: S,
154        attachment: ErrorSensitivityLabel<ErrorAttachment>,
155    ) -> Self {
156        if self.frames.is_empty() {
157            tracing::error!(
158                "Calling `ErrorReport::attach()` on ErrorReport with no frames. {:?}",
159                self
160            );
161            std::panic::panic_any(self.push_error(StringError::new(
162                "Calling `ErrorReport::attach()` on ErrorReport with no frames.",
163            )));
164        }
165        let last_frame = self.frames.last_mut().unwrap_error();
166        last_frame
167            .attachments
168            .insert(key_name.into(), Box::new(attachment));
169        self
170    }
171
172    /// Get a reference to the last (most general) error in the chain.
173    ///
174    /// # Panics
175    ///
176    /// Panics if the report has no frames.
177    /// This should not be possible, if it happens this is considered a bug inside `charon-error` itself.
178    // Size of `&(dyn Error)` is not known, so have to keep it in a Box.
179    #[allow(clippy::borrowed_box)]
180    #[must_use]
181    #[instrument]
182    pub fn get_last_error(&self) -> &Box<dyn Error + Send + Sync + 'static> {
183        if self.frames.is_empty() {
184            tracing::error!(
185                "Calling `ErrorReport::get_last_error()` on ErrorReport with no frames. {:?}",
186                self
187            );
188            // Use normal `panic!` because this function is triggered inside panic hook.
189            panic!("Calling `ErrorReport::get_last_error()` on ErrorReport with no frames.");
190        }
191        &self.frames.last().unwrap_error().error
192    }
193
194    /// Get the display string of the last error in the chain.
195    #[must_use]
196    pub fn get_last_error_title(&self) -> String {
197        self.get_last_error().to_string()
198    }
199
200    /// Create a new Error Report ID hash.
201    fn create_new_unique_id() -> String {
202        let amount_chars: usize = 20;
203        use rand::RngExt;
204        let hash: String = rand::rng()
205            .sample_iter(rand::distr::Alphanumeric)
206            .take(amount_chars)
207            .map(char::from)
208            .collect();
209        hash
210    }
211
212    /// Get the unique identifier for this error report.
213    #[must_use]
214    pub fn get_unique_id(&self) -> String {
215        self.unique_id.clone()
216    }
217
218    /// Shorthand to attach a public string to the most recent frame.
219    #[must_use = "the modified report with the attachment must be used"]
220    pub fn attach_public_string<S: Into<String>, A: Display>(
221        self,
222        key_name: S,
223        attachment: A,
224    ) -> Self {
225        self.attach(
226            key_name,
227            ErrorSensitivityLabel::Public(ErrorAttachment::String(attachment.to_string())),
228        )
229    }
230}
231
232impl Display for ErrorReport {
233    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
234        write!(
235            f,
236            "{}",
237            self.stringify(ErrorFmtSettings {
238                level_of_detail: ErrorFmtLoD::Medium,
239                frame_limit: None,
240                enable_color: true,
241                link_format: ERGlobalSettings::get_or_default_settings()
242                    .unwrap_error()
243                    .link_format,
244                indentation_style: IndentationStyle::FourSpaces,
245                ..Default::default()
246            })
247            .unwrap_error()
248        )
249    }
250}
251
252impl<E: Error + Send + Sync + 'static> From<E> for ErrorReport {
253    #[track_caller]
254    fn from(error: E) -> Self {
255        Self::from_error(error)
256    }
257}