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