Skip to main content

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