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