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}