versalogrs/
lib.rs

1use notify_rust::Notification;
2use chrono::Local;
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    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}
23
24static COLORS: &[(&str, &str)] = &[
25    ("INFO", "\x1b[32m"),
26    ("ERROR", "\x1b[31m"),
27    ("WARNING", "\x1b[33m"),
28    ("DEBUG", "\x1b[36m"),
29    ("CRITICAL", "\x1b[35m"),
30];
31
32static SYMBOLS: &[(&str, &str)] = &[
33    ("INFO", "[+]"),
34    ("ERROR", "[-]"),
35    ("WARNING", "[!]"),
36    ("DEBUG", "[D]"),
37    ("CRITICAL", "[C]"),
38];
39
40static RESET: &str = "\x1b[0m";
41
42static VALID_MODES: &[&str] = &["simple", "simple2", "detailed", "file"];
43static VALID_SAVE_LEVELS: &[&str] = &["INFO", "ERROR", "WARNING", "DEBUG", "CRITICAL"];
44
45pub fn NewVersaLog(mode: &str, show_file: bool, show_tag: bool, tag: &str, enable_all: bool, notice: bool, all_save: bool, save_levels: Vec<String>) -> VersaLog {
46    let mode = mode.to_lowercase();
47    let tag = tag.to_string();
48    
49    if !VALID_MODES.contains(&mode.as_str()) {
50        panic!("Invalid mode '{}' specified. Valid modes are: simple, simple2, detailed, file", mode);
51    }
52
53    let mut showFile = show_file;
54    let mut showTag = show_tag;
55    let mut notice_enabled = notice;
56    let mut allsave = all_save;
57    let mut savelevels = save_levels;
58
59    if enable_all {
60        showFile = true;
61        showTag = true;
62        notice_enabled = true;
63        allsave = true;
64    }
65
66    if mode == "file" {
67        showFile = true;
68    }
69
70    if allsave {
71        if savelevels.is_empty() {
72            savelevels = VALID_SAVE_LEVELS.iter().map(|s| s.to_string()).collect();
73        } else {
74            for level in &savelevels {
75                if !VALID_SAVE_LEVELS.contains(&level.as_str()) {
76                    panic!("Invalid saveLevels specified. Valid levels are: {:?}", VALID_SAVE_LEVELS);
77                }
78            }
79        }
80    }
81
82    let tx = if allsave {
83        let (tx, rx) = channel::<(String, String)>();
84        thread::spawn(move || {
85            while let Ok((log_text, _level)) = rx.recv() {
86                let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
87                let log_dir = cwd.join("log");
88                if !log_dir.exists() {
89                    let _ = fs::create_dir_all(&log_dir);
90                }
91                let today = Local::now().format("%Y-%m-%d").to_string();
92                let log_file = log_dir.join(format!("{}.log", today));
93                let log_entry = format!("{}\n", log_text);
94                let _ = fs::OpenOptions::new()
95                    .create(true)
96                    .append(true)
97                    .write(true)
98                    .open(&log_file)
99                    .and_then(|mut file| file.write_all(log_entry.as_bytes()));
100            }
101        });
102        Some(tx)
103    } else { None };
104
105    VersaLog {
106        mode,
107        tag,
108        showFile,
109        showTag,
110        notice: notice_enabled,
111        enableall: enable_all,
112        allsave,
113        savelevels,
114        silent: false,
115        tx,
116    }
117}
118
119pub fn NewVersaLogSimple(mode: &str, tag: &str) -> VersaLog {
120    NewVersaLog(mode, false, false, tag, false, false, false, Vec::new())
121}
122
123pub fn NewVersaLogSimple2(mode: &str, tag: &str, enable_all: bool) -> VersaLog {
124    NewVersaLog(mode, false, false, tag, enable_all, false, false, Vec::new())
125}
126
127impl VersaLog {
128    pub fn log(&self, msg: String, level: String, tags: &[&str]) {
129        let level = level.to_uppercase();
130        
131        let color = COLORS.iter()
132            .find(|(l, _)| *l == level)
133            .map(|(_, c)| *c)
134            .unwrap_or("");
135        let symbol = SYMBOLS.iter()
136            .find(|(l, _)| *l == level)
137            .map(|(_, s)| *s)
138            .unwrap_or("");
139        
140        let caller = if self.showFile || self.mode == "file" {
141            self.get_caller()
142        } else {
143            String::new()
144        };
145        
146        let final_tag = if !tags.is_empty() && !tags[0].is_empty() {
147            tags[0].to_string()
148        } else if self.showTag && !self.tag.is_empty() {
149            self.tag.clone()
150        } else {
151            String::new()
152        };
153        
154        let (output, plain) = match self.mode.as_str() {
155            "simple" => {
156                if self.showFile {
157                    if !final_tag.is_empty() {
158                        let output = format!("[{}][{}]{}{}{} {}", caller, final_tag, color, symbol, RESET, msg);
159                        let plain = format!("[{}][{}]{} {}", caller, final_tag, symbol, msg);
160                        (output, plain)
161                    } else {
162                        let output = format!("[{}]{}{}{} {}", caller, color, symbol, RESET, msg);
163                        let plain = format!("[{}]{} {}", caller, symbol, msg);
164                        (output, plain)
165                    }
166                } else {
167                    if !final_tag.is_empty() {
168                        let output = format!("[{}]{}{}{} {}", final_tag, color, symbol, RESET, msg);
169                        let plain = format!("[{}]{} {}", final_tag, symbol, msg);
170                        (output, plain)
171                    } else {
172                        let output = format!("{}{}{} {}", color, symbol, RESET, msg);
173                        let plain = format!("{} {}", symbol, msg);
174                        (output, plain)
175                    }
176                }
177            },
178            "simple2" => {
179                let timestamp = self.get_time();
180                if self.showFile {
181                    if !final_tag.is_empty() {
182                        let output = format!("[{}] [{}][{}]{}{}{} {}", timestamp, caller, final_tag, color, symbol, RESET, msg);
183                        let plain = format!("[{}] [{}][{}]{} {}", timestamp, caller, final_tag, symbol, msg);
184                        (output, plain)
185                    } else {
186                        let output = format!("[{}] [{}]{}{}{} {}", timestamp, caller, color, symbol, RESET, msg);
187                        let plain = format!("[{}] [{}]{} {}", timestamp, caller, symbol, msg);
188                        (output, plain)
189                    }
190                } else {
191                    let output = format!("[{}] {}{}{} {}", timestamp, color, symbol, RESET, msg);
192                    let plain = format!("[{}] {} {}", timestamp, symbol, msg);
193                    (output, plain)
194                }
195            },
196            "file" => {
197                let output = format!("[{}]{}{}[{}]{}", caller, color, level, RESET, msg);
198                let plain = format!("[{}][{}] {}", caller, level, msg);
199                (output, plain)
200            },
201            _ => {
202                let timestamp = self.get_time();
203                let mut output = format!("[{}]{}{}[{}]", timestamp, color, level, RESET);
204                let mut plain = format!("[{}][{}]", timestamp, level);
205                
206                if !final_tag.is_empty() {
207                    output.push_str(&format!("[{}]", final_tag));
208                    plain.push_str(&format!("[{}]", final_tag));
209                }
210                
211                if self.showFile {
212                    output.push_str(&format!("[{}]", caller));
213                    plain.push_str(&format!("[{}]", caller));
214                }
215                
216                output.push_str(&format!(" : {}", msg));
217                plain.push_str(&format!(" : {}", msg));
218                
219                (output, plain)
220            }
221        };
222        
223        if !self.silent {
224            println!("{}", output);
225        }
226        self.save_log(plain, level.clone());
227        
228        if self.notice && (level == "ERROR" || level == "CRITICAL") {
229            let _ = Notification::new()
230                .summary(&format!("{} Log notice", level))
231                .body(&msg)
232                .show();
233        }
234    }
235
236    pub fn set_silent(&mut self, silent: bool) {
237        self.silent = silent;
238    }
239
240    pub fn install_panic_hook(self: std::sync::Arc<Self>) {
241        let logger = self.clone();
242        panic::set_hook(Box::new(move |info| {
243            let payload = info.payload();
244            let msg = if let Some(s) = payload.downcast_ref::<&str>() {
245                (*s).to_string()
246            } else if let Some(s) = payload.downcast_ref::<String>() {
247                s.clone()
248            } else {
249                "unknown panic".to_string()
250            };
251
252            let mut details = String::new();
253            if let Some(loc) = info.location() {
254                details.push_str(&format!("at {}:{}:{}\n", loc.file(), loc.line(), loc.column()));
255            }
256            let bt = Backtrace::new();
257            details.push_str(&format!("{:?}", bt));
258
259            logger.Critical_no_tag(&format!("Unhandled panic: {}\n{}", msg, details));
260        }));
261    }
262    
263    pub fn Info(&self, msg: &str, tags: &[&str]) {
264        self.log(msg.to_string(), "INFO".to_string(), tags);
265    }
266    
267    pub fn Error(&self, msg: &str, tags: &[&str]) {
268        self.log(msg.to_string(), "ERROR".to_string(), tags);
269    }
270    
271    pub fn Warning(&self, msg: &str, tags: &[&str]) {
272        self.log(msg.to_string(), "WARNING".to_string(), tags);
273    }
274    
275    pub fn Debug(&self, msg: &str, tags: &[&str]) {
276        self.log(msg.to_string(), "DEBUG".to_string(), tags);
277    }
278    
279    pub fn Critical(&self, msg: &str, tags: &[&str]) {
280        self.log(msg.to_string(), "CRITICAL".to_string(), tags);
281    }
282
283    pub fn info(&self, msg: &str, tags: &[&str]) {
284        self.Info(msg, tags);
285    }
286    
287    pub fn error(&self, msg: &str, tags: &[&str]) {
288        self.Error(msg, tags);
289    }
290    
291    pub fn warning(&self, msg: &str, tags: &[&str]) {
292        self.Warning(msg, tags);
293    }
294    
295    pub fn debug(&self, msg: &str, tags: &[&str]) {
296        self.Debug(msg, tags);
297    }
298    
299    pub fn critical(&self, msg: &str, tags: &[&str]) {
300        self.Critical(msg, tags);
301    }
302    
303    pub fn Info_no_tag(&self, msg: &str) {
304        self.log(msg.to_string(), "INFO".to_string(), &[]);
305    }
306    
307    pub fn Error_no_tag(&self, msg: &str) {
308        self.log(msg.to_string(), "ERROR".to_string(), &[]);
309    }
310    
311    pub fn Warning_no_tag(&self, msg: &str) {
312        self.log(msg.to_string(), "WARNING".to_string(), &[]);
313    }
314    
315    pub fn Debug_no_tag(&self, msg: &str) {
316        self.log(msg.to_string(), "DEBUG".to_string(), &[]);
317    }
318    
319    pub fn Critical_no_tag(&self, msg: &str) {
320        self.log(msg.to_string(), "CRITICAL".to_string(), &[]);
321    }
322
323    fn get_time(&self) -> String {
324        Local::now().format("%Y-%m-%d %H:%M:%S").to_string()
325    }
326    
327    fn get_caller(&self) -> String {
328        let bt = Backtrace::new();
329        if let Some(frame) = bt.frames().get(3) {
330            if let Some(symbol) = frame.symbols().first() {
331                if let Some(file) = symbol.filename() {
332                    if let Some(file_name) = file.file_name() {
333                        if let Some(line) = symbol.lineno() {
334                            return format!("{}:{}", file_name.to_string_lossy(), line);
335                        }
336                    }
337                }
338            }
339        }
340        "unknown:0".to_string()
341    }
342    
343    fn save_log(&self, log_text: String, level: String) {
344        if !self.allsave || !self.savelevels.contains(&level) {
345            return;
346        }
347
348        if let Some(tx) = &self.tx {
349            let _ = tx.send((log_text, level));
350            return;
351        }
352
353        let cwd = env::current_dir().unwrap_or_else(|_| env::current_dir().unwrap());
354        let log_dir = cwd.join("log");
355        if !log_dir.exists() {
356            let _ = fs::create_dir_all(&log_dir);
357        }
358        let today = Local::now().format("%Y-%m-%d").to_string();
359        let log_file = log_dir.join(format!("{}.log", today));
360        let log_entry = format!("{}\n", log_text);
361        let _ = fs::OpenOptions::new()
362            .create(true)
363            .append(true)
364            .write(true)
365            .open(&log_file)
366            .and_then(|mut file| file.write_all(log_entry.as_bytes()));
367    }
368}