Skip to main content

common/
logging.rs

1use std::{fmt, fs, io, thread};
2use std::fs::{File, OpenOptions};
3use std::path::Path;
4use std::fmt::Formatter;
5use std::io::Write;
6use std::sync::{mpsc, Arc, Mutex, OnceLock, RwLock};
7use std::sync::mpsc::Sender;
8use chrono::{DateTime, Local};
9use serde::{Deserialize, Serialize};
10
11/// Defines the severity or category of a log message.
12#[derive (Debug,Clone,Copy, Serialize, Deserialize)]
13pub enum LogLevel {
14    /// A background success event or system confirmation.
15    SuccessEvent,
16    /// General informational message.
17    Info,
18    /// Warning about a non-fatal issue or unexpected behavior.
19    Warning,
20    /// Critical or fatal system error.
21    Error,
22    /// An error triggered by invalid user input or actions.
23    UserError,
24    /// A success message resulting directly from a user action.
25    UserSuccess,
26}
27
28impl fmt::Display for LogLevel {
29    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30        let text = match self {
31            LogLevel::SuccessEvent =>   "Success",
32            LogLevel::Info =>           "INFO",
33            LogLevel::Warning =>        "WARN",
34            LogLevel::Error =>          "ERROR",
35            LogLevel::UserError =>      "USER ERROR",
36            LogLevel::UserSuccess =>    "USER success"
37        };
38
39        write!(f, "{text}")
40    }
41}
42
43/// Internal representation of a single log event.
44#[derive (Debug,Clone)]
45pub struct LogMessage {
46    /// The severity level of the message.
47    level: LogLevel,
48    /// The actual log text.
49    text: String,
50    /// The local time when the message was dispatched.
51    timestamp: DateTime<Local>,
52    /// Flag indicating whether this message is only relevant in debug mode.
53    is_debug: bool,
54    
55}
56
57/// Trait for destinations that can process and output log messages.
58///
59/// Sinks must be thread-safe (`Send + Sync`) to be utilized by the background logger.
60pub trait LogSink: Send + Sync {
61    /// Processes a single incoming log message.
62    ///
63    /// # Arguments
64    ///
65    /// * `msg` - Reference to the structured [`LogMessage`]
66    fn receive(&self, msg: &LogMessage);
67}
68
69/// The central logging dispatcher that routes messages to all registered sinks via a background thread.
70pub struct Logger {
71    /// Thread-safe collection of all registered log sinks.
72    sinks: Arc<RwLock<Vec<Box<dyn LogSink>>>>,
73    /// Sender channel used to dispatch messages to the background logging thread.
74    log_tx: Sender<LogMessage>,
75}
76
77impl Logger {
78    /// Retrieves or initializes the global singleton instance of the [`Logger`].
79    pub fn global() -> &'static Logger {
80        static LOGGER: OnceLock<Logger> = OnceLock::new();
81        LOGGER.get_or_init(|| {
82            let (tx, rx) = mpsc::channel::<LogMessage>();
83            let sinks = Arc::new(RwLock::new(Vec::<Box<dyn LogSink>>::new()));
84
85            let thread_sinks = sinks.clone();
86
87            thread::spawn(move || {
88                while let Ok(msg) = rx.recv() {
89                    if let Ok(locked_sinks) = thread_sinks.read() {
90                        for sink in locked_sinks.iter() {
91                            sink.receive(&msg);
92                        }
93                    }
94                }
95            });
96
97            Logger {
98                sinks,
99                log_tx: tx,
100            }
101        })
102    }
103
104    /// Registers a new log sink to receive future messages.
105    ///
106    /// # Arguments
107    ///
108    /// * `sink` - A boxed instance implementing the [`LogSink`] trait
109    pub fn add_sink(&self, sink: Box<dyn LogSink>) {
110        self.sinks.write().unwrap().push(sink);
111    }
112
113    /// Creates and sends a new log message to the background processing thread.
114    ///
115    /// # Arguments
116    ///
117    /// * `level`    - The severity level of the message
118    /// * `text`     - The formatted log text
119    /// * `is_debug` - Whether this message should only be visible in debug builds
120    pub fn dispatch(&self, level: LogLevel, text: String, is_debug: bool) {
121        let msg = LogMessage {
122            level,
123            text,
124            timestamp: Local::now(),
125            is_debug,
126        };
127
128        let _ = self.log_tx.send(msg);
129    }
130}
131
132/// Dispatches a formatted log message to the global logger.
133///
134/// This macro is the primary interface for logging throughout the application. It behaves
135/// exactly like standard Rust formatting macros (e.g., `println!` or `format!`), allowing
136/// you to easily interpolate variables into your log messages. Messages sent via `r_log!`
137/// are always processed and distributed to all registered sinks (like terminal and files),
138/// regardless of whether the application is compiled in debug or release mode.
139///
140/// # Arguments
141///
142/// * `$level` - The severity of the log, provided as a [`LogLevel`] variant (e.g., `LogLevel::Info`, `LogLevel::Warning`).
143/// * `$arg`   - A format string and an arbitrary number of formatting arguments, following standard `std::fmt` syntax.
144///
145/// # Examples
146///
147/// ```rust
148/// // Basic usage with a simple string
149/// r_log!(LogLevel::Info, "System initialized successfully");
150///
151/// // Formatting with variables
152/// let port = 8080;
153/// r_log!(LogLevel::SuccessEvent, "Server listening on port {}", port);
154///
155/// // Logging complex errors
156/// r_log!(LogLevel::Error, "Failed to load configuration file at {}: {}", path, err);
157/// ```
158#[macro_export]
159macro_rules! r_log {
160    ($level:expr, $($arg:tt)*) => {
161        $crate::logging::Logger::global().dispatch($level, format!($($arg)*), false)
162    }
163}
164
165/// Dispatches a formatted debug log message to the global logger.
166///
167/// This macro functions identically to [`r_log!`], but messages logged with this macro
168/// are strictly flagged as debug messages. The visibility of these messages depends
169/// on the configured [`LogSink`]s.
170///
171/// Specifically, the default [`TerminalSink`] will ignore and hide these messages when
172/// the application is compiled in release mode (i.e., `not(debug_assertions)`).
173/// However, the default [`FileSink`] will always record them, regardless of the build
174/// profile, ensuring debug trails are kept in the logs without cluttering the user's terminal.
175///
176/// # Arguments
177///
178/// * `$level` - The severity of the log, provided as a [`LogLevel`] variant.
179/// * `$arg`   - A format string and its arguments, following standard `std::fmt` syntax.
180#[macro_export]
181macro_rules! r_debug_log {
182    ($level:expr, $($arg:tt)*) => {
183        $crate::logging::Logger::global().dispatch($level, format!($($arg)*), true)
184    };
185}
186
187/// A log sink that formats and prints messages to standard output, supporting terminal colors and interactive prompts.
188pub struct TerminalSink {
189    /// An optional command-line prompt string to restore after printing a log line.
190    pub cli_prompt: Option<String>,
191}
192
193impl LogSink for TerminalSink {
194    fn receive(&self, message: &LogMessage) {
195        if message.is_debug && !cfg!(all(debug_assertions, not(test))) {
196            return;
197        }
198        
199        let color = match message.level {
200            LogLevel::SuccessEvent =>   "\x1B[42m\x1B[30m",
201            LogLevel::Info =>           "\x1B[44m\x1B[30m",
202            LogLevel::Warning =>        "\x1B[43m\x1B[30m",
203            LogLevel::Error =>          "\x1B[41m\x1B[30m",
204            LogLevel::UserError =>      "\x1B[35m",
205            LogLevel::UserSuccess =>    "\x1B[32m",
206        };
207
208        let width = crossterm::terminal::size()
209            .map(|(width, _height)| (width as usize).saturating_sub(2).min(500) )
210            .unwrap_or(80);
211        let time = message.timestamp.format("%H:%M:%S");
212        let raw_line = format!("({}) [{:-^12}] {}", time, message.level.to_string(), message.text);
213
214        let is_system_message = color.contains("\x1B[4");
215        let log_line = match is_system_message {
216            true =>     format!("{} {:<width$} \x1B[0m", color, raw_line),
217            false =>    format!("{} {} \x1B[0m", color, raw_line)
218        };
219
220
221        let stdout = io::stdout();
222        let mut handle = stdout.lock();
223
224        if let Some(prompt) = &self.cli_prompt {
225            write!(handle,"\r\x1b[2K").unwrap();
226            writeln!(handle, "{}", log_line).unwrap();
227            write!(handle, "{}", prompt).unwrap();
228            handle.flush().unwrap();
229        } else {
230            writeln!(handle, "{}", log_line).unwrap();
231            handle.flush().unwrap();
232        }
233
234    }
235}
236
237/// A log sink that writes messages sequentially to a specified file.
238pub struct FileSink {
239    /// Thread-safe handle to the open log file.
240    file: Mutex<File>,
241}
242
243impl FileSink {
244    /// Initializes a new [`FileSink`]. If a file already exists at the given path, it is archived and renamed.
245    ///
246    /// # Arguments
247    ///
248    /// * `path` - The file path where logs should be written
249    pub fn new(path: &str) -> FileSink {
250        let path_object = Path::new(path);
251
252        if path_object.exists() {
253            if let Ok(metadata) = path_object.metadata() {
254                let (time_str, prefix) = match fs::metadata(&path).and_then(|m| m.created().or_else(|_| metadata.modified())) {
255                    Ok(file_time) => {
256                        let datetime: DateTime<Local> = file_time.into();
257                        (datetime.format("%Y-%m-%d_%H-%M-%S").to_string(), "")
258                    }
259                    Err(_) => {
260                        let now = Local::now().format("%Y-%m-%d_%H-%M-%S").to_string();
261                        (now, "old_file_backuped_")
262                    }
263                };
264
265                let file_stem = path_object.file_stem().and_then(|s| s.to_str()).unwrap_or("backup");
266                let ext = path_object.extension().and_then(|s| s.to_str()).unwrap_or("log");
267
268                let archive_name = format!("{}{}_{}.{}", prefix, time_str, file_stem, ext);
269                let archive_path = path_object.with_file_name(archive_name);
270
271                if let Err(e) = fs::rename(&path_object, &archive_path) {
272                    println!("Warning: Couldnt archive old Log-File: {}", e);
273                }
274            }
275        }
276
277        let file = OpenOptions::new()
278            .create(true)
279            .write(true)
280            .truncate(true)
281            .open(path)
282            .expect("Failed to open log file");
283
284        Self {
285            file: Mutex::new(file)
286        }
287    }
288}
289
290impl LogSink for FileSink {
291    fn receive(&self, message: &LogMessage) {
292        let time = message.timestamp.format("%H:%M:%S");
293
294        let log_line = format!("({}) [{:-^12}] {}\n", time, message.level.to_string(), message.text);
295
296        if let Ok(mut file) = self.file.lock() {
297            let _ = file.write_all(log_line.as_bytes());
298
299            let _ = file.flush();
300        }
301    }
302}