versalogrs/
lib.rs

1 use backtrace::Backtrace;
2use chrono::{Local, NaiveDate};
3use notify_rust::Notification;
4use std::env;
5use std::fs;
6use std::io::Write;
7use std::panic;
8use std::sync::mpsc::{Sender, channel};
9use std::thread;
10
11pub struct VersaLog {
12    enum_mode: String,
13    tag: String,
14    showFile: bool,
15    showTag: bool,
16    notice: bool,
17    enableall: bool,
18    allsave: bool,
19    savelevels: Vec<String>,
20    silent: bool,
21    tx: Option<Sender<(String, String)>>,
22    last_cleanup_date: Option<NaiveDate>,
23    catch_exceptions: bool,
24}
25
26static COLORS: &[(&str, &str)] = &[
27    ("INFO", "\x1b[32m"),
28    ("ERROR", "\x1b[31m"),
29    ("WARNING", "\x1b[33m"),
30    ("DEBUG", "\x1b[36m"),
31    ("CRITICAL", "\x1b[35m"),
32];
33
34static SYMBOLS: &[(&str, &str)] = &[
35    ("INFO", "[+]"),
36    ("ERROR", "[-]"),
37    ("WARNING", "[!]"),
38    ("DEBUG", "[D]"),
39    ("CRITICAL", "[C]"),
40];
41
42static RESET: &str = "\x1b[0m";
43
44static VALID_MODES: &[&str] = &["simple", "simple2", "detailed", "file"];
45static VALID_SAVE_LEVELS: &[&str] = &["INFO", "ERROR", "WARNING", "DEBUG", "CRITICAL"];
46
47pub fn NewVersaLog(
48    enum_mode: &str,
49    show_file: bool,
50    show_tag: bool,
51    tag: &str,
52    enable_all: bool,
53    notice: bool,
54    all_save: bool,
55    save_levels: Vec<String>,
56    catch_exceptions: bool,
57) -> VersaLog {
58    let _mode = enum_mode.to_lowercase();
59    let tag = tag.to_string();
60
61    if !VALID_MODES.contains(&enum_mode) {
62        panic!(
63            "Invalid mode '{}' specified. Valid modes are: simple, simple2, detailed, file",
64            enum_mode
65        );
66    }
67
68    let mut showFile = show_file;
69    let mut showTag = show_tag;
70    let mut notice_enabled = notice;
71    let mut allsave = all_save;
72    let mut savelevels = save_levels;
73
74    if enable_all {
75        showFile = true;
76        showTag = true;
77        notice_enabled = true;
78        allsave = true;
79    }
80
81    if enum_mode == "file" {
82        showFile = true;
83    }
84
85    if allsave {
86        if savelevels.is_empty() {
87            savelevels = VALID_SAVE_LEVELS.iter().map(|s| s.to_string()).collect();
88        } else {
89            for level in &savelevels {
90                if !VALID_SAVE_LEVELS.contains(&level.as_str()) {
91                    panic!(
92                        "Invalid saveLevels specified. Valid levels are: {:?}",
93                        VALID_SAVE_LEVELS
94                    );
95                }
96            }
97        }
98    }
99
100    let tx = if allsave {
101        let (tx, rx) = channel::<(String, String)>();
102        thread::spawn(move || {
103            while let Ok((log_text, _level)) = rx.recv() {
104                let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
105                let log_dir = cwd.join("log");
106                if !log_dir.exists() {
107                    let _ = fs::create_dir_all(&log_dir);
108                }
109                let today = Local::now().format("%Y-%m-%d").to_string();
110                let log_file = log_dir.join(format!("{}.log", today));
111                let log_entry = format!("{}\n", log_text);
112                let _ = fs::OpenOptions::new()
113                    .create(true)
114                    .append(true)
115                    .write(true)
116                    .open(&log_file)
117                    .and_then(|mut file| file.write_all(log_entry.as_bytes()));
118            }
119        });
120        Some(tx)
121    } else {
122        None
123    };
124
125    VersaLog {
126        enum_mode: enum_mode.to_string(),
127        tag,
128        showFile,
129        showTag,
130        notice: notice_enabled,
131        enableall: enable_all,
132        allsave,
133        savelevels,
134        silent: false,
135        tx,
136        last_cleanup_date: None,
137        catch_exceptions,
138    }
139}
140
141pub fn NewVersaLogSimple(enum_mode: &str, tag: &str) -> VersaLog {
142    NewVersaLog(
143        enum_mode,
144        false,
145        false,
146        tag,
147        false,
148        false,
149        false,
150        Vec::new(),
151        false,
152    )
153}
154
155pub fn NewVersaLogSimple2(enum_mode: &str, tag: &str, enable_all: bool) -> VersaLog {
156    NewVersaLog(
157        enum_mode,
158        false,
159        false,
160        tag,
161        enable_all,
162        false,
163        false,
164        Vec::new(),
165        false,
166    )
167}
168
169impl VersaLog {
170    pub fn log(&self, msg: String, level: String, tags: &[&str]) {
171        let level = level.to_uppercase();
172
173        let color = COLORS
174            .iter()
175            .find(|(l, _)| *l == level)
176            .map(|(_, c)| *c)
177            .unwrap_or("");
178        let symbol = SYMBOLS
179            .iter()
180            .find(|(l, _)| *l == level)
181            .map(|(_, s)| *s)
182            .unwrap_or("");
183
184        let caller = if self.showFile || self.enum_mode == "file" {
185            self.get_caller()
186        } else {
187            String::new()
188        };
189
190        let final_tag = if !tags.is_empty() && !tags[0].is_empty() {
191            tags[0].to_string()
192        } else if self.showTag && !self.tag.is_empty() {
193            self.tag.clone()
194        } else {
195            String::new()
196        };
197
198        let (output, plain) = match self.enum_mode.as_str() {
199            "simple" => {
200                if self.showFile {
201                    if !final_tag.is_empty() {
202                        let output = format!(
203                            "[{}][{}]{}{}{} {}",
204                            caller, final_tag, color, symbol, RESET, msg
205                        );
206                        let plain = format!("[{}][{}]{} {}", caller, final_tag, symbol, msg);
207                        (output, plain)
208                    } else {
209                        let output = format!("[{}]{}{}{} {}", caller, color, symbol, RESET, msg);
210                        let plain = format!("[{}]{} {}", caller, symbol, msg);
211                        (output, plain)
212                    }
213                } else {
214                    if !final_tag.is_empty() {
215                        let output = format!("[{}]{}{}{} {}", final_tag, color, symbol, RESET, msg);
216                        let plain = format!("[{}]{} {}", final_tag, symbol, msg);
217                        (output, plain)
218                    } else {
219                        let output = format!("{}{}{} {}", color, symbol, RESET, msg);
220                        let plain = format!("{} {}", symbol, msg);
221                        (output, plain)
222                    }
223                }
224            }
225            "simple2" => {
226                let timestamp = self.get_time();
227                if self.showFile {
228                    if !final_tag.is_empty() {
229                        let output = format!(
230                            "[{}] [{}][{}]{}{}{} {}",
231                            timestamp, caller, final_tag, color, symbol, RESET, msg
232                        );
233                        let plain = format!(
234                            "[{}] [{}][{}]{} {}",
235                            timestamp, caller, final_tag, symbol, msg
236                        );
237                        (output, plain)
238                    } else {
239                        let output = format!(
240                            "[{}] [{}]{}{}{} {}",
241                            timestamp, caller, color, symbol, RESET, msg
242                        );
243                        let plain = format!("[{}] [{}]{} {}", timestamp, caller, symbol, msg);
244                        (output, plain)
245                    }
246                } else {
247                    let output = format!("[{}] {}{}{} {}", timestamp, color, symbol, RESET, msg);
248                    let plain = format!("[{}] {} {}", timestamp, symbol, msg);
249                    (output, plain)
250                }
251            }
252            "file" => {
253                 let output = format!("[{}] {}{} [{}] {}", caller, color, level, RESET, msg);
254                 let plain = format!("[{}][{}] {}", caller, level, msg);
255                 (output, plain)
256            }
257            _ => {
258                let timestamp = self.get_time();
259                let mut parts = vec![format!("[{}]", timestamp)];
260                
261                parts.push(format!("{}[{}]{}", color, level, RESET));
262                
263                if !final_tag.is_empty() {
264                    parts.push(format!("[{}]", final_tag));
265                }
266                
267                if self.showFile {
268                    parts.push(format!("[{}]", caller));
269                }
270                
271                let output = format!("{} {}", parts.join(""), msg);
272                let plain = format!("{} {}", parts.join(""), msg);
273                
274                (output, plain)
275            }
276        };
277
278        if !self.silent {
279            println!("{}", output);
280        }
281        self.save_log(plain, level.clone());
282
283        if self.notice && (level == "ERROR" || level == "CRITICAL") {
284            let _ = Notification::new()
285                .summary(&format!("{} Log notice", level))
286                .body(&msg)
287                .show();
288        }
289    }
290
291    pub fn set_silent(&mut self, silent: bool) {
292        self.silent = silent;
293    }
294
295    pub fn install_panic_hook(self: std::sync::Arc<Self>) {
296        let logger = self.clone();
297        panic::set_hook(Box::new(move |info| {
298            let payload = info.payload();
299            let msg = if let Some(s) = payload.downcast_ref::<&str>() {
300                (*s).to_string()
301            } else if let Some(s) = payload.downcast_ref::<String>() {
302                s.clone()
303            } else {
304                "unknown panic".to_string()
305            };
306
307            let mut details = String::new();
308            if let Some(loc) = info.location() {
309                details.push_str(&format!(
310                    "at {}:{}:{}\n",
311                    loc.file(),
312                    loc.line(),
313                    loc.column()
314                ));
315            }
316            let bt = Backtrace::new();
317            details.push_str(&format!("{:?}", bt));
318
319            logger.Critical_no_tag(&format!("Unhandled panic: {}\n{}", msg, details));
320        }));
321    }
322
323    pub fn handle_exception(&self, exc_type: &str, exc_value: &str, exc_traceback: &str) {
324        let tb_str = format!(
325            "Exception Type: {}\nException Value: {}\nTraceback:\n{}",
326            exc_type, exc_value, exc_traceback
327        );
328        self.Critical_no_tag(&format!("Unhandled exception:\n{}", tb_str));
329    }
330
331    pub fn Info(&self, msg: &str, tags: &[&str]) {
332        self.log(msg.to_string(), "INFO".to_string(), tags);
333    }
334
335    pub fn Error(&self, msg: &str, tags: &[&str]) {
336        self.log(msg.to_string(), "ERROR".to_string(), tags);
337    }
338
339    pub fn Warning(&self, msg: &str, tags: &[&str]) {
340        self.log(msg.to_string(), "WARNING".to_string(), tags);
341    }
342
343    pub fn Debug(&self, msg: &str, tags: &[&str]) {
344        self.log(msg.to_string(), "DEBUG".to_string(), tags);
345    }
346
347    pub fn Critical(&self, msg: &str, tags: &[&str]) {
348        self.log(msg.to_string(), "CRITICAL".to_string(), tags);
349    }
350
351    pub fn info(&self, msg: &str, tags: &[&str]) {
352        self.Info(msg, tags);
353    }
354
355    pub fn error(&self, msg: &str, tags: &[&str]) {
356        self.Error(msg, tags);
357    }
358
359    pub fn warning(&self, msg: &str, tags: &[&str]) {
360        self.Warning(msg, tags);
361    }
362
363    pub fn debug(&self, msg: &str, tags: &[&str]) {
364        self.Debug(msg, tags);
365    }
366
367    pub fn critical(&self, msg: &str, tags: &[&str]) {
368        self.Critical(msg, tags);
369    }
370
371    pub fn Info_no_tag(&self, msg: &str) {
372        self.log(msg.to_string(), "INFO".to_string(), &[]);
373    }
374
375    pub fn Error_no_tag(&self, msg: &str) {
376        self.log(msg.to_string(), "ERROR".to_string(), &[]);
377    }
378
379    pub fn Warning_no_tag(&self, msg: &str) {
380        self.log(msg.to_string(), "WARNING".to_string(), &[]);
381    }
382
383    pub fn Debug_no_tag(&self, msg: &str) {
384        self.log(msg.to_string(), "DEBUG".to_string(), &[]);
385    }
386
387    pub fn Critical_no_tag(&self, msg: &str) {
388        self.log(msg.to_string(), "CRITICAL".to_string(), &[]);
389    }
390
391    fn get_time(&self) -> String {
392        Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
393    }
394
395    fn get_caller(&self) -> String {
396        let bt = Backtrace::new();
397        if let Some(frame) = bt.frames().get(3) {
398            if let Some(symbol) = frame.symbols().first() {
399                if let Some(file) = symbol.filename() {
400                    if let Some(file_name) = file.file_name() {
401                        if let Some(line) = symbol.lineno() {
402                            return format!("{}:{}", file_name.to_string_lossy(), line);
403                        }
404                    }
405                }
406            }
407        }
408        "unknown:0".to_string()
409    }
410
411    fn cleanup_old_logs(&self, days: i64) {
412        let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
413        let log_dir = cwd.join("log");
414
415        if !log_dir.exists() {
416            return;
417        }
418
419        let now = Local::now().naive_local().date();
420
421        if let Ok(entries) = fs::read_dir(&log_dir) {
422            for entry in entries {
423                if let Ok(entry) = entry {
424                    let path = entry.path();
425                    if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("log") {
426                        if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
427                            if let Ok(file_date) = NaiveDate::parse_from_str(
428                                &file_name.replace(".log", ""),
429                                "%Y-%m-%d",
430                            ) {
431                                if (now - file_date).num_days() >= days {
432                                    if let Err(e) = fs::remove_file(&path) {
433                                        if !self.silent {
434                                            println!(
435                                                "[LOG CLEANUP WARNING] {} cannot be removed: {}",
436                                                path.display(),
437                                                e
438                                            );
439                                        }
440                                    } else if !self.silent {
441                                        println!("[LOG CLEANUP] removed: {}", path.display());
442                                    }
443                                }
444                            }
445                        }
446                    }
447                }
448            }
449        }
450    }
451
452    fn save_log(&self, log_text: String, level: String) {
453        if !self.allsave || !self.savelevels.contains(&level) {
454            return;
455        }
456
457        if let Some(tx) = &self.tx {
458            let _ = tx.send((log_text, level));
459            return;
460        }
461
462        self.save_log_sync(log_text, level);
463    }
464
465    fn save_log_sync(&self, log_text: String, level: String) {
466        if !self.allsave || !self.savelevels.contains(&level) {
467            return;
468        }
469
470        let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
471        let log_dir = cwd.join("log");
472        if !log_dir.exists() {
473            let _ = fs::create_dir_all(&log_dir);
474        }
475        let today = Local::now().format("%Y-%m-%d").to_string();
476        let log_file = log_dir.join(format!("{}.log", today));
477        let log_entry = format!("{}\n", log_text);
478        let _ = fs::OpenOptions::new()
479            .create(true)
480            .append(true)
481            .write(true)
482            .open(&log_file)
483            .and_then(|mut file| file.write_all(log_entry.as_bytes()));
484
485        let today_date = Local::now().naive_local().date();
486        if self.last_cleanup_date != Some(today_date) {
487            self.cleanup_old_logs(7);
488        }
489    }
490}