versalogrs/
lib.rs

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