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 output = format!("[{}] {}{} [{}] {}", timestamp, color, level, final_tag, RESET);
260                let mut plain = format!("[{}][{}]", timestamp, level);
261
262                if !final_tag.is_empty() {
263                    output.push_str(&format!("[{}]", final_tag));
264                    plain.push_str(&format!("[{}]", final_tag));
265                }
266
267                if self.showFile {
268                    output.push_str(&format!("[{}]", caller));
269                    plain.push_str(&format!("[{}]", caller));
270                }
271
272                output.push_str(&format!(" : {}", msg));
273                plain.push_str(&format!(" : {}", msg));
274
275                (output, plain)
276            }
277        };
278
279        if !self.silent {
280            println!("{}", output);
281        }
282        self.save_log(plain, level.clone());
283
284        if self.notice && (level == "ERROR" || level == "CRITICAL") {
285            let _ = Notification::new()
286                .summary(&format!("{} Log notice", level))
287                .body(&msg)
288                .show();
289        }
290    }
291
292    pub fn set_silent(&mut self, silent: bool) {
293        self.silent = silent;
294    }
295
296    pub fn install_panic_hook(self: std::sync::Arc<Self>) {
297        let logger = self.clone();
298        panic::set_hook(Box::new(move |info| {
299            let payload = info.payload();
300            let msg = if let Some(s) = payload.downcast_ref::<&str>() {
301                (*s).to_string()
302            } else if let Some(s) = payload.downcast_ref::<String>() {
303                s.clone()
304            } else {
305                "unknown panic".to_string()
306            };
307
308            let mut details = String::new();
309            if let Some(loc) = info.location() {
310                details.push_str(&format!(
311                    "at {}:{}:{}\n",
312                    loc.file(),
313                    loc.line(),
314                    loc.column()
315                ));
316            }
317            let bt = Backtrace::new();
318            details.push_str(&format!("{:?}", bt));
319
320            logger.Critical_no_tag(&format!("Unhandled panic: {}\n{}", msg, details));
321        }));
322    }
323
324    pub fn handle_exception(&self, exc_type: &str, exc_value: &str, exc_traceback: &str) {
325        let tb_str = format!(
326            "Exception Type: {}\nException Value: {}\nTraceback:\n{}",
327            exc_type, exc_value, exc_traceback
328        );
329        self.Critical_no_tag(&format!("Unhandled exception:\n{}", tb_str));
330    }
331
332    pub fn Info(&self, msg: &str, tags: &[&str]) {
333        self.log(msg.to_string(), "INFO".to_string(), tags);
334    }
335
336    pub fn Error(&self, msg: &str, tags: &[&str]) {
337        self.log(msg.to_string(), "ERROR".to_string(), tags);
338    }
339
340    pub fn Warning(&self, msg: &str, tags: &[&str]) {
341        self.log(msg.to_string(), "WARNING".to_string(), tags);
342    }
343
344    pub fn Debug(&self, msg: &str, tags: &[&str]) {
345        self.log(msg.to_string(), "DEBUG".to_string(), tags);
346    }
347
348    pub fn Critical(&self, msg: &str, tags: &[&str]) {
349        self.log(msg.to_string(), "CRITICAL".to_string(), tags);
350    }
351
352    pub fn info(&self, msg: &str, tags: &[&str]) {
353        self.Info(msg, tags);
354    }
355
356    pub fn error(&self, msg: &str, tags: &[&str]) {
357        self.Error(msg, tags);
358    }
359
360    pub fn warning(&self, msg: &str, tags: &[&str]) {
361        self.Warning(msg, tags);
362    }
363
364    pub fn debug(&self, msg: &str, tags: &[&str]) {
365        self.Debug(msg, tags);
366    }
367
368    pub fn critical(&self, msg: &str, tags: &[&str]) {
369        self.Critical(msg, tags);
370    }
371
372    pub fn Info_no_tag(&self, msg: &str) {
373        self.log(msg.to_string(), "INFO".to_string(), &[]);
374    }
375
376    pub fn Error_no_tag(&self, msg: &str) {
377        self.log(msg.to_string(), "ERROR".to_string(), &[]);
378    }
379
380    pub fn Warning_no_tag(&self, msg: &str) {
381        self.log(msg.to_string(), "WARNING".to_string(), &[]);
382    }
383
384    pub fn Debug_no_tag(&self, msg: &str) {
385        self.log(msg.to_string(), "DEBUG".to_string(), &[]);
386    }
387
388    pub fn Critical_no_tag(&self, msg: &str) {
389        self.log(msg.to_string(), "CRITICAL".to_string(), &[]);
390    }
391
392    fn get_time(&self) -> String {
393        Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
394    }
395
396    fn get_caller(&self) -> String {
397        let bt = Backtrace::new();
398        if let Some(frame) = bt.frames().get(3) {
399            if let Some(symbol) = frame.symbols().first() {
400                if let Some(file) = symbol.filename() {
401                    if let Some(file_name) = file.file_name() {
402                        if let Some(line) = symbol.lineno() {
403                            return format!("{}:{}", file_name.to_string_lossy(), line);
404                        }
405                    }
406                }
407            }
408        }
409        "unknown:0".to_string()
410    }
411
412    fn cleanup_old_logs(&self, days: i64) {
413        let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
414        let log_dir = cwd.join("log");
415
416        if !log_dir.exists() {
417            return;
418        }
419
420        let now = Local::now().naive_local().date();
421
422        if let Ok(entries) = fs::read_dir(&log_dir) {
423            for entry in entries {
424                if let Ok(entry) = entry {
425                    let path = entry.path();
426                    if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("log") {
427                        if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
428                            if let Ok(file_date) = NaiveDate::parse_from_str(
429                                &file_name.replace(".log", ""),
430                                "%Y-%m-%d",
431                            ) {
432                                if (now - file_date).num_days() >= days {
433                                    if let Err(e) = fs::remove_file(&path) {
434                                        if !self.silent {
435                                            println!(
436                                                "[LOG CLEANUP WARNING] {} cannot be removed: {}",
437                                                path.display(),
438                                                e
439                                            );
440                                        }
441                                    } else if !self.silent {
442                                        println!("[LOG CLEANUP] removed: {}", path.display());
443                                    }
444                                }
445                            }
446                        }
447                    }
448                }
449            }
450        }
451    }
452
453    fn save_log(&self, log_text: String, level: String) {
454        if !self.allsave || !self.savelevels.contains(&level) {
455            return;
456        }
457
458        if let Some(tx) = &self.tx {
459            let _ = tx.send((log_text, level));
460            return;
461        }
462
463        self.save_log_sync(log_text, level);
464    }
465
466    fn save_log_sync(&self, log_text: String, level: String) {
467        if !self.allsave || !self.savelevels.contains(&level) {
468            return;
469        }
470
471        let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
472        let log_dir = cwd.join("log");
473        if !log_dir.exists() {
474            let _ = fs::create_dir_all(&log_dir);
475        }
476        let today = Local::now().format("%Y-%m-%d").to_string();
477        let log_file = log_dir.join(format!("{}.log", today));
478        let log_entry = format!("{}\n", log_text);
479        let _ = fs::OpenOptions::new()
480            .create(true)
481            .append(true)
482            .write(true)
483            .open(&log_file)
484            .and_then(|mut file| file.write_all(log_entry.as_bytes()));
485
486        let today_date = Local::now().naive_local().date();
487        if self.last_cleanup_date != Some(today_date) {
488            self.cleanup_old_logs(7);
489        }
490    }
491}