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