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}