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}