Skip to main content

ducktrace_logger/
lib.rs

1pub fn add(left: u64, right: u64) -> u64 {
2    left + right
3}
4
5#[cfg(test)]
6mod tests {
7    use super::*;
8
9    #[test]
10    fn it_works() {
11        let result = add(2, 2);
12        assert_eq!(result, 4);
13    }
14}
15
16use std::{
17    env,
18    path::Path,
19    fs::{OpenOptions, File},
20    io::Write,
21    sync::{OnceLock, Mutex},
22    time::Instant,
23};
24use chrono::Local;
25use colored::*;
26
27static LOGGER: OnceLock<Mutex<DuckTraceLogger>> = OnceLock::new();
28
29struct DuckTraceLogger {
30    level: LogLevel,
31    log_file: Option<File>,
32    log_path: Option<String>,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd)]
36enum LogLevel {
37    Debug,
38    Info,
39    Warning,
40    Error,
41    Critical,
42}
43
44impl DuckTraceLogger {
45    fn new(file_override: Option<&str>, level_override: Option<&str>) -> Self {
46        let level = match level_override {
47            Some(l) => Self::level_from_str(l),
48            None => match env::var("DT_LOG_LEVEL")
49                .unwrap_or_else(|_| "INFO".to_string())
50                .to_uppercase()
51                .as_str()
52            {
53                "DEBUG" => LogLevel::Debug,
54                "WARNING" => LogLevel::Warning,
55                "ERROR" => LogLevel::Error,
56                "CRITICAL" => LogLevel::Critical,
57                _ => LogLevel::Info,
58            },
59        };
60
61        let (log_file, log_path) = if let Some(path_str) = file_override {
62            let path = Path::new(path_str);
63            let dir = path
64                .parent()
65                .map(|p| p.to_string_lossy().into_owned())
66                .unwrap_or_else(|| {
67                    env::var("DT_LOG_PATH").unwrap_or_else(|_| {
68                        let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
69                        format!("{}/.config/duckTrace", home)
70                    })
71                });
72            let file = path
73                .file_name()
74                .map(|f| f.to_string_lossy().into_owned())
75                .unwrap_or_else(|| "unknown-script.log".to_string());
76
77            Self::open_log_file(&dir, &file)
78        } else {
79            let dir = env::var("DT_LOG_PATH").unwrap_or_else(|_| {
80                let home = env::var("HOME").unwrap_or_else(|_| ".".to_string());
81                format!("{}/.config/duckTrace", home)
82            });
83            let file = env::var("DT_LOG_FILE").unwrap_or_else(|_| "unknown-script.log".to_string());
84
85            Self::open_log_file(&dir, &file)
86        };
87
88        let mut logger = Self {
89            level,
90            log_file,
91            log_path,
92        };
93        logger.log_config();
94        logger
95    }
96
97    fn open_log_file(dir: &str, filename: &str) -> (Option<File>, Option<String>) {
98        if std::fs::create_dir_all(dir).is_err() {
99            return (None, None);
100        }
101
102        let full_path = Path::new(dir).join(filename);
103        let path_str = full_path.to_string_lossy().into_owned();
104
105        match OpenOptions::new()
106            .create(true)
107            .append(true)
108            .open(&full_path)
109        {
110            Ok(file) => (Some(file), Some(path_str)),
111            Err(_) => (None, None),
112        }
113    }
114
115    fn level_from_str(s: &str) -> LogLevel {
116        match s.to_uppercase().as_str() {
117            "DEBUG" => LogLevel::Debug,
118            "INFO" => LogLevel::Info,
119            "WARNING" => LogLevel::Warning,
120            "ERROR" => LogLevel::Error,
121            "CRITICAL" => LogLevel::Critical,
122            _ => LogLevel::Info,
123        }
124    }
125
126    fn log_config(&mut self) {
127        let log_path_display = self
128            .log_path
129            .as_ref()
130            .map(|p| p.as_str())
131            .unwrap_or("Logging to file disabled");
132        let level_str = match self.level {
133            LogLevel::Debug => "DEBUG",
134            LogLevel::Info => "INFO",
135            LogLevel::Warning => "WARNING",
136            LogLevel::Error => "ERROR",
137            LogLevel::Critical => "CRITICAL",
138        };
139        self.log(
140            LogLevel::Debug,
141            &format!(
142                "Logger initialized: level={}, file={}",
143                level_str, log_path_display
144            ),
145        );
146    }
147
148    fn should_log(&self, msg_level: LogLevel) -> bool {
149        msg_level >= self.level
150    }
151
152    fn get_symbol(&self, level: LogLevel) -> &'static str {
153        match level {
154            LogLevel::Debug => "⁉️",
155            LogLevel::Info => "✅",
156            LogLevel::Warning => "⚠️",
157            LogLevel::Error => "❌",
158            LogLevel::Critical => "🚨",
159        }
160    }
161
162    fn format_message(&self, level: LogLevel, message: &str) -> String {
163        let timestamp = Local::now().format("%H:%M:%S");
164        let symbol = self.get_symbol(level);
165        let level_str = match level {
166            LogLevel::Debug => "DEBUG",
167            LogLevel::Info => "INFO",
168            LogLevel::Warning => "WARNING",
169            LogLevel::Error => "ERROR",
170            LogLevel::Critical => "CRITICAL",
171        };
172
173        format!(
174            "[🦆📜] [{}] {}{}{} ⮞ {}",
175            timestamp, symbol, level_str, symbol, message
176        )
177    }
178
179    fn colorize_console(&self, level: LogLevel, formatted_msg: &str) -> String {
180        match level {
181            LogLevel::Debug => formatted_msg.blue().bold().to_string(),
182            LogLevel::Info => formatted_msg.green().bold().to_string(),
183            LogLevel::Warning => formatted_msg.yellow().bold().to_string(),
184            LogLevel::Error => formatted_msg.red().bold().blink().to_string(),
185            LogLevel::Critical => formatted_msg.red().bold().blink().to_string(),
186        }
187    }
188
189    fn add_duck_say(&self, level: LogLevel, message: &str) -> String {
190        if matches!(level, LogLevel::Error | LogLevel::Critical) {
191            format!(
192                "\n\x1b[3m\x1b[38;2;0;150;150m🦆 duck say \x1b[1m\x1b[38;2;255;255;0m⮞\x1b[0m\x1b[3m\x1b[38;2;0;150;150m fuck ❌ {}\x1b[0m",
193                message
194            )
195        } else {
196            String::new()
197        }
198    }
199
200    pub fn log(&mut self, level: LogLevel, message: &str) {
201        if !self.should_log(level) {
202            return;
203        }
204
205        let formatted = self.format_message(level, message);
206        let console_output = self.colorize_console(level, &formatted);
207
208        eprintln!("{}", console_output);
209
210        if matches!(level, LogLevel::Error | LogLevel::Critical) {
211            let duck_say = self.add_duck_say(level, message);
212            eprintln!("{}", duck_say);
213        }
214
215        if let Some(file) = &mut self.log_file {
216            let timestamp = Local::now().format("%H:%M:%S");
217            let level_str = match level {
218                LogLevel::Debug => "DEBUG",
219                LogLevel::Info => "INFO",
220                LogLevel::Warning => "WARNING",
221                LogLevel::Error => "ERROR",
222                LogLevel::Critical => "CRITICAL",
223            };
224
225            let file_msg = format!("[{}] {} - {}\n", timestamp, level_str, message);
226            let _ = writeln!(file, "{}", file_msg);
227        }
228    }
229}
230
231fn with_logger<F>(level: LogLevel, msg: &str, f: F)
232where
233    F: FnOnce(&mut DuckTraceLogger, LogLevel, &str),
234{
235    let logger = LOGGER.get_or_init(|| Mutex::new(DuckTraceLogger::new(None, None)));
236    let mut guard = logger.lock().unwrap();
237    f(&mut guard, level, msg);
238}
239
240pub fn dt_debug(msg: &str) {
241    with_logger(LogLevel::Debug, msg, |logger, lvl, m| logger.log(lvl, m));
242}
243
244pub fn dt_info(msg: &str) {
245    with_logger(LogLevel::Info, msg, |logger, lvl, m| logger.log(lvl, m));
246}
247
248pub fn dt_warning(msg: &str) {
249    with_logger(LogLevel::Warning, msg, |logger, lvl, m| logger.log(lvl, m));
250}
251
252pub fn dt_error(msg: &str) {
253    with_logger(LogLevel::Error, msg, |logger, lvl, m| logger.log(lvl, m));
254}
255
256pub fn dt_critical(msg: &str) {
257    with_logger(LogLevel::Critical, msg, |logger, lvl, m| logger.log(lvl, m));
258}
259
260pub fn dt_setup(file_override: Option<&str>, level_override: Option<&str>) {
261    let _ = LOGGER.get_or_init(|| Mutex::new(DuckTraceLogger::new(file_override, level_override)));
262}
263
264pub struct DtTimer {
265    operation_name: String,
266    start_time: Instant,
267}
268
269impl DtTimer {
270    pub fn new(operation_name: &str) -> Self {
271        dt_debug(&format!("Starting {}...", operation_name));
272        Self {
273            operation_name: operation_name.to_string(),
274            start_time: Instant::now(),
275        }
276    }
277
278    pub fn lap(&self, lap_name: &str) {
279        let elapsed = self.start_time.elapsed().as_secs_f64();
280        dt_debug(&format!("{} - {}: {:.3}s", self.operation_name, lap_name, elapsed));
281    }
282
283    pub fn complete(self) {
284        let elapsed = self.start_time.elapsed().as_secs_f64();
285        dt_debug(&format!("Completed {} in {:.3}s", self.operation_name, elapsed));
286    }
287}
288
289pub fn dt_timer(name: &str) -> DtTimer {
290    DtTimer::new(name)
291}
292
293pub fn dt_duck_say(message: &str) {
294    let teal = Color::TrueColor { r: 0, g: 150, b: 150 };
295    let yellow = Color::TrueColor { r: 255, g: 255, b: 0 };
296
297    eprintln!(
298        "{}{} {}",
299        "🦆 duck say ".italic().color(teal),
300        "⮞".bold().color(yellow),
301        message.italic().color(teal)
302    );
303}
304
305#[macro_export]
306macro_rules! duck_log {
307    (debug: $($arg:tt)*) => {
308        $crate::dt_debug(&format!($($arg)*));
309    };
310    (info: $($arg:tt)*) => {
311        $crate::dt_info(&format!($($arg)*));
312    };
313    (warning: $($arg:tt)*) => {
314        $crate::dt_warning(&format!($($arg)*));
315    };
316    (error: $($arg:tt)*) => {
317        $crate::dt_error(&format!($($arg)*));
318    };
319    (critical: $($arg:tt)*) => {
320        $crate::dt_critical(&format!($($arg)*));
321    };
322}
323
324#[macro_export]
325macro_rules! dt_debug {
326    ($($arg:tt)*) => {
327        $crate::dt_debug(&format!($($arg)*));
328    };
329}
330
331#[macro_export]
332macro_rules! dt_info {
333    ($($arg:tt)*) => {
334        $crate::dt_info(&format!($($arg)*));
335    };
336}
337
338#[macro_export]
339macro_rules! dt_warning {
340    ($($arg:tt)*) => {
341        $crate::dt_warning(&format!($($arg)*));
342    };
343}
344
345#[macro_export]
346macro_rules! dt_error {
347    ($($arg:tt)*) => {
348        $crate::dt_error(&format!($($arg)*));
349    };
350}
351
352#[macro_export]
353macro_rules! dt_critical {
354    ($($arg:tt)*) => {
355        $crate::dt_critical(&format!($($arg)*));
356    };
357}
358
359#[macro_export]
360macro_rules! duck_say {
361    ($($arg:tt)*) => {
362        $crate::dt_say(&format!($($arg)*));
363    };
364}