Skip to main content

error_forge/
logging.rs

1use crate::error::ForgeError;
2use crate::macros::ErrorLevel;
3use std::sync::OnceLock;
4
5/// Trait for error logging adapters
6///
7/// Implement this trait to create a custom logger for error-forge
8/// that integrates with your logging system
9pub trait ErrorLogger: Send + Sync + 'static {
10    /// Log an error with the given level
11    fn log_error(&self, error: &dyn ForgeError, level: ErrorLevel);
12
13    /// Log a message with the given level
14    fn log_message(&self, message: &str, level: ErrorLevel);
15
16    /// Called when a panic occurs (if panic hook is registered)
17    fn log_panic(&self, info: &std::panic::PanicHookInfo);
18}
19
20// The global error logger
21static ERROR_LOGGER: OnceLock<Box<dyn ErrorLogger>> = OnceLock::new();
22
23/// Register a logger for errors
24///
25/// Only one logger can be registered at a time.
26/// If a logger is already registered, this will return an error.
27pub fn register_logger(logger: impl ErrorLogger) -> Result<(), &'static str> {
28    let boxed = Box::new(logger);
29    match ERROR_LOGGER.set(boxed) {
30        Ok(_) => Ok(()),
31        Err(_) => Err("Error logger already registered"),
32    }
33}
34
35/// Get the current logger, if one is registered
36pub fn logger() -> Option<&'static dyn ErrorLogger> {
37    ERROR_LOGGER.get().map(|boxed| boxed.as_ref())
38}
39
40/// Log an error with the appropriate level
41pub fn log_error(error: &dyn ForgeError) {
42    if let Some(logger) = logger() {
43        let level = if error.is_fatal() {
44            ErrorLevel::Critical
45        } else if !error.is_retryable() {
46            ErrorLevel::Error
47        } else {
48            ErrorLevel::Warning
49        };
50
51        logger.log_error(error, level);
52    }
53}
54
55/// Standard logging implementation for common logging crates
56#[cfg(feature = "log")]
57pub mod log_impl {
58    use super::*;
59    use log::{debug, error, info, warn};
60
61    /// A logger that uses the `log` crate
62    pub struct LogAdapter;
63
64    impl ErrorLogger for LogAdapter {
65        fn log_error(&self, error: &dyn ForgeError, level: ErrorLevel) {
66            let kind = error.kind();
67            let message = error.dev_message();
68            match level {
69                ErrorLevel::Critical => {
70                    error!(target: "error-forge", "[CRITICAL] [{kind}] {message}")
71                }
72                ErrorLevel::Error => error!(target: "error-forge", "[ERROR] [{kind}] {message}"),
73                ErrorLevel::Warning => warn!(target: "error-forge", "[WARNING] [{kind}] {message}"),
74                ErrorLevel::Info => info!(target: "error-forge", "[INFO] [{kind}] {message}"),
75                ErrorLevel::Debug => debug!(target: "error-forge", "[DEBUG] [{kind}] {message}"),
76            }
77        }
78
79        fn log_message(&self, message: &str, level: ErrorLevel) {
80            match level {
81                ErrorLevel::Critical | ErrorLevel::Error => {
82                    error!(target: "error-forge", "{message}")
83                }
84                ErrorLevel::Warning => warn!(target: "error-forge", "{message}"),
85                ErrorLevel::Info => info!(target: "error-forge", "{message}"),
86                ErrorLevel::Debug => debug!(target: "error-forge", "{message}"),
87            }
88        }
89
90        fn log_panic(&self, info: &std::panic::PanicHookInfo) {
91            error!(target: "error-forge", "PANIC: {info}");
92        }
93    }
94
95    /// Initialize logging with the log crate adapter
96    pub fn init() -> Result<(), &'static str> {
97        register_logger(LogAdapter)
98    }
99}
100
101#[cfg(feature = "tracing")]
102pub mod tracing_impl {
103    use super::*;
104    use tracing::{debug, error, info, warn};
105
106    /// A logger that uses the `tracing` crate
107    pub struct TracingAdapter;
108
109    impl ErrorLogger for TracingAdapter {
110        fn log_error(&self, error: &dyn ForgeError, level: ErrorLevel) {
111            match level {
112                ErrorLevel::Critical => {
113                    error!(target: "error-forge", kind = %error.kind(), message = %error.dev_message(), "Critical error")
114                }
115                ErrorLevel::Error => {
116                    error!(target: "error-forge", kind = %error.kind(), message = %error.dev_message(), "Error")
117                }
118                ErrorLevel::Warning => {
119                    warn!(target: "error-forge", kind = %error.kind(), message = %error.dev_message(), "Warning")
120                }
121                ErrorLevel::Info => {
122                    info!(target: "error-forge", kind = %error.kind(), message = %error.dev_message(), "Info")
123                }
124                ErrorLevel::Debug => {
125                    debug!(target: "error-forge", kind = %error.kind(), message = %error.dev_message(), "Debug")
126                }
127            }
128        }
129
130        fn log_message(&self, message: &str, level: ErrorLevel) {
131            match level {
132                ErrorLevel::Critical | ErrorLevel::Error => {
133                    error!(target: "error-forge", "{message}")
134                }
135                ErrorLevel::Warning => warn!(target: "error-forge", "{message}"),
136                ErrorLevel::Info => info!(target: "error-forge", "{message}"),
137                ErrorLevel::Debug => debug!(target: "error-forge", "{message}"),
138            }
139        }
140
141        fn log_panic(&self, info: &std::panic::PanicHookInfo) {
142            error!(target: "error-forge", panic = %info, "Panic occurred");
143        }
144    }
145
146    /// Initialize logging with the tracing adapter
147    pub fn init() -> Result<(), &'static str> {
148        register_logger(TracingAdapter)
149    }
150}
151
152/// Build your own error logger - example implementation
153pub mod custom {
154    use super::*;
155
156    // Type aliases for complex types
157    /// Function type for error logging
158    type ErrorFn = Box<dyn Fn(&dyn ForgeError, ErrorLevel) + Send + Sync + 'static>;
159    /// Function type for message logging
160    type MessageFn = Box<dyn Fn(&str, ErrorLevel) + Send + Sync + 'static>;
161    /// Function type for panic logging
162    type PanicFn = Box<dyn Fn(&std::panic::PanicHookInfo) + Send + Sync + 'static>;
163
164    /// Builder for creating a custom error logger
165    #[derive(Default)]
166    pub struct ErrorLoggerBuilder {
167        error_fn: Option<ErrorFn>,
168        message_fn: Option<MessageFn>,
169        panic_fn: Option<PanicFn>,
170    }
171
172    impl ErrorLoggerBuilder {
173        /// Create a new error logger builder
174        pub fn new() -> Self {
175            Self::default()
176        }
177
178        /// Set the function to use for logging errors
179        pub fn with_error_fn<F>(mut self, f: F) -> Self
180        where
181            F: Fn(&dyn ForgeError, ErrorLevel) + Send + Sync + 'static,
182        {
183            self.error_fn = Some(Box::new(f));
184            self
185        }
186
187        /// Set the function to use for logging messages
188        pub fn with_message_fn<F>(mut self, f: F) -> Self
189        where
190            F: Fn(&str, ErrorLevel) + Send + Sync + 'static,
191        {
192            self.message_fn = Some(Box::new(f));
193            self
194        }
195
196        /// Set the function to use for logging panics
197        pub fn with_panic_fn<F>(mut self, f: F) -> Self
198        where
199            F: Fn(&std::panic::PanicHookInfo) + Send + Sync + 'static,
200        {
201            self.panic_fn = Some(Box::new(f));
202            self
203        }
204
205        /// Build the error logger
206        pub fn build(self) -> CustomErrorLogger {
207            CustomErrorLogger {
208                error_fn: self.error_fn,
209                message_fn: self.message_fn,
210                panic_fn: self.panic_fn,
211            }
212        }
213    }
214
215    /// A custom error logger that uses user-provided functions
216    pub struct CustomErrorLogger {
217        error_fn: Option<ErrorFn>,
218        message_fn: Option<MessageFn>,
219        panic_fn: Option<PanicFn>,
220    }
221
222    impl ErrorLogger for CustomErrorLogger {
223        fn log_error(&self, error: &dyn ForgeError, level: ErrorLevel) {
224            if let Some(error_fn) = &self.error_fn {
225                error_fn(error, level);
226            }
227        }
228
229        fn log_message(&self, message: &str, level: ErrorLevel) {
230            if let Some(message_fn) = &self.message_fn {
231                message_fn(message, level);
232            }
233        }
234
235        fn log_panic(&self, info: &std::panic::PanicHookInfo) {
236            if let Some(panic_fn) = &self.panic_fn {
237                panic_fn(info);
238            }
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::AppError;
247    use std::sync::Arc;
248    use std::sync::Mutex;
249
250    #[test]
251    fn test_custom_logger() {
252        // A simple test logger that captures logs in a Vec
253        struct TestLogger {
254            logs: Arc<Mutex<Vec<String>>>,
255        }
256
257        impl ErrorLogger for TestLogger {
258            fn log_error(&self, error: &dyn ForgeError, level: ErrorLevel) {
259                let kind = error.kind();
260                let message = error.dev_message();
261                let log = format!("{level:?}: [{kind}] {message}");
262                self.logs.lock().unwrap().push(log);
263            }
264
265            fn log_message(&self, message: &str, level: ErrorLevel) {
266                let log = format!("{level:?}: {message}");
267                self.logs.lock().unwrap().push(log);
268            }
269
270            fn log_panic(&self, info: &std::panic::PanicHookInfo) {
271                let log = format!("PANIC: {info}");
272                self.logs.lock().unwrap().push(log);
273            }
274        }
275
276        // Create and register the logger
277        let logs = Arc::new(Mutex::new(Vec::new()));
278        let logger = TestLogger {
279            logs: Arc::clone(&logs),
280        };
281
282        // We need to make sure we have a fresh state for this test
283        // In a real app, you'd only register once at startup
284        let _ = register_logger(logger);
285
286        // Log an error
287        let error = AppError::config("Test error");
288        log_error(&error);
289
290        // Check that the log was captured
291        let captured_logs = logs.lock().unwrap();
292        assert!(!captured_logs.is_empty());
293        assert!(captured_logs[0].contains("[Config]"));
294        assert!(captured_logs[0].contains("Test error"));
295    }
296}