hackerlog/
lib.rs

1mod format;
2mod levels;
3mod macros;
4mod timing;
5
6use chrono::Local;
7use std::{
8    fmt,
9    io::{self, Write},
10    sync::{
11        atomic::{AtomicBool, AtomicU8, Ordering},
12        Mutex, OnceLock,
13    },
14};
15use std::{process, thread};
16use termion::color;
17
18#[cfg(feature = "structured")]
19mod structured;
20
21pub use format::{FormatPlaceholder, FormatTemplate};
22pub use levels::LogLevel;
23pub use timing::TimedOperation;
24
25#[cfg(feature = "structured")]
26pub use structured::structured::LogEvent;
27
28// Global logger configuration
29pub struct Logger {
30    verbose: AtomicBool,
31    min_level: AtomicU8,
32    writer: Mutex<Box<dyn Write + Send>>,
33    context: Mutex<Vec<(String, String)>>,
34    format: Mutex<FormatTemplate>,
35}
36
37impl Default for Logger {
38    fn default() -> Self {
39        Self {
40            verbose: AtomicBool::new(false),
41            min_level: AtomicU8::new(LogLevel::INFO as u8),
42            writer: Mutex::new(Box::new(io::stdout())),
43            context: Mutex::new(Vec::new()),
44            format: Mutex::new(FormatTemplate::parse("{symbol} {context}{message}")),
45        }
46    }
47}
48
49// Global logger instance
50static LOGGER: OnceLock<Logger> = OnceLock::new();
51
52pub fn logger() -> &'static Logger {
53    LOGGER.get_or_init(Logger::default)
54}
55
56impl Logger {
57    pub fn set_format(&self, template: &str) -> &Self {
58        *self.format.lock().unwrap() = FormatTemplate::parse(template);
59        self
60    }
61
62    // Default formats for different styles
63    pub fn use_simple_format(&self) -> &Self {
64        self.set_format("{symbol} {message}")
65    }
66
67    pub fn use_detailed_format(&self) -> &Self {
68        self.set_format("[{level}] {datetime} {message}")
69    }
70
71    pub fn use_debug_format(&self) -> &Self {
72        self.set_format("{datetime} [{level}] <{file}:{line}> {message}")
73    }
74
75    pub fn verbose(&self, enabled: bool) -> &Self {
76        self.verbose.store(enabled, Ordering::Relaxed);
77        self
78    }
79
80    pub fn min_level(&self, level: LogLevel) -> &Self {
81        self.min_level.store(level as u8, Ordering::Relaxed);
82        self
83    }
84
85    pub fn set_writer(&self, writer: Box<dyn Write + Send>) -> io::Result<()> {
86        *self.writer.lock().unwrap() = writer;
87        Ok(())
88    }
89
90    pub fn add_context<K, V>(&self, key: K, value: V) -> ContextGuard
91    where
92        K: Into<String>,
93        V: Into<String>,
94    {
95        let mut context = self.context.lock().unwrap();
96        context.push((key.into(), value.into()));
97        ContextGuard(context.len() - 1)
98    }
99
100    pub fn should_log(&self, level: LogLevel) -> bool {
101        let min_level = self.min_level.load(Ordering::Relaxed);
102        level as u8 >= min_level
103    }
104
105    pub fn write_log(
106        &self,
107        level: LogLevel,
108        message: &str,
109        file: &str,
110        line: u32,
111    ) -> io::Result<()> {
112        let format = self.format.lock().unwrap();
113        let mut output = String::new();
114
115        for part in &format.parts {
116            match part {
117                FormatPlaceholder::Level => {
118                    output.push_str(&format!("{:?}", level));
119                }
120                FormatPlaceholder::Symbol => {
121                    output.push_str(level.symbol());
122                }
123                FormatPlaceholder::Message => {
124                    output.push_str(message);
125                }
126                FormatPlaceholder::Time => {
127                    output.push_str(&Local::now().format("%H:%M:%S").to_string());
128                }
129                FormatPlaceholder::Date => {
130                    output.push_str(&Local::now().format("%Y-%m-%d").to_string());
131                }
132                FormatPlaceholder::DateTime => {
133                    output.push_str(&Local::now().format("%Y-%m-%d %H:%M:%S").to_string());
134                }
135                FormatPlaceholder::ThreadName => {
136                    let name = thread::current().name().map_or_else(
137                        || format!("Thread-{:?}", thread::current().id()),
138                        ToString::to_string,
139                    );
140                    output.push_str(&name);
141                }
142                FormatPlaceholder::ThreadId => {
143                    output.push_str(&format!("{:?}", thread::current().id()));
144                }
145                FormatPlaceholder::ProcessId => {
146                    output.push_str(&process::id().to_string());
147                }
148                FormatPlaceholder::File => {
149                    output.push_str(file);
150                }
151                FormatPlaceholder::Line => {
152                    output.push_str(&line.to_string());
153                }
154                FormatPlaceholder::Context => {
155                    let context = self.context.lock().unwrap();
156                    if !context.is_empty() {
157                        output.push('[');
158                        for (i, (key, value)) in context.iter().enumerate() {
159                            if i > 0 {
160                                output.push_str(", ");
161                            }
162                            output.push_str(&format!("{}={}", key, value));
163                        }
164                        output.push_str("] ");
165                    }
166                }
167                FormatPlaceholder::Text(text) => {
168                    output.push_str(text);
169                }
170            }
171        }
172
173        output.push('\n');
174
175        // Color the output
176        let colored_output = format!(
177            "{}{}{}",
178            color::Fg(level.color()),
179            output,
180            color::Fg(color::Reset)
181        );
182
183        // Write to configured output
184        let mut writer = self.writer.lock().unwrap();
185        writer.write_all(colored_output.as_bytes())?;
186        writer.flush()?;
187
188        Ok(())
189    }
190
191    #[cfg(feature = "structured")]
192    pub fn structured_format(&self) -> &Self {
193        self.set_format("{datetime} {level} {message}")
194    }
195
196    #[cfg(feature = "structured")]
197    pub fn write_structured_event(&self, event: &LogEvent) -> io::Result<()> {
198        let mut fields_str = String::new();
199        if !event.fields.is_empty() {
200            fields_str.push('[');
201            for (i, (key, value)) in event.fields.iter().enumerate() {
202                if i > 0 {
203                    fields_str.push_str(", ");
204                }
205                fields_str.push_str(&format!("{}={}", key, value));
206            }
207            fields_str.push(']');
208        }
209
210        self.write_log(
211            event.level,
212            &if fields_str.is_empty() {
213                event.message.clone()
214            } else {
215                format!("{} {}", event.message, fields_str)
216            },
217            &event.file,
218            event.line,
219        )
220    }
221}
222
223// Context guard for automatic cleanup
224pub struct ContextGuard(usize);
225
226impl Drop for ContextGuard {
227    fn drop(&mut self) {
228        if let Some(logger) = LOGGER.get() {
229            let mut context = logger.context.lock().unwrap();
230            if self.0 < context.len() {
231                context.remove(self.0);
232            }
233        }
234    }
235}
236
237// Error handling
238#[derive(Debug)]
239pub struct LogError {
240    message: String,
241}
242
243impl LogError {
244    pub fn new<S: Into<String>>(message: S) -> Self {
245        Self {
246            message: message.into(),
247        }
248    }
249}
250
251impl fmt::Display for LogError {
252    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
253        write!(f, "{}", self.message)
254    }
255}
256
257impl std::error::Error for LogError {}
258
259impl From<io::Error> for LogError {
260    fn from(err: io::Error) -> Self {
261        LogError::new(err.to_string())
262    }
263}
264
265// Progress indicator
266pub struct Progress {
267    message: String,
268    total: Option<u64>,
269    current: u64,
270}
271
272impl Progress {
273    pub fn new<S: Into<String>>(message: S) -> Self {
274        Self {
275            message: message.into(),
276            total: None,
277            current: 0,
278        }
279    }
280
281    pub fn with_total<S: Into<String>>(message: S, total: u64) -> Self {
282        Self {
283            message: message.into(),
284            total: Some(total),
285            current: 0,
286        }
287    }
288
289    pub fn inc(&mut self, amount: u64) {
290        self.current += amount;
291        let message = self.message.clone();
292        self.update(&message);
293    }
294
295    pub fn update<S: Into<String>>(&mut self, message: S) {
296        self.message = message.into();
297        if let Some(total) = self.total {
298            logger()
299                .write_log(
300                    LogLevel::INFO,
301                    &format!("{} [{}/{}]", self.message, self.current, total),
302                    file!(),
303                    line!(),
304                )
305                .ok();
306        } else {
307            logger()
308                .write_log(
309                    LogLevel::INFO,
310                    &format!("{} [{}]", self.message, self.current),
311                    file!(),
312                    line!(),
313                )
314                .ok();
315        }
316    }
317
318    pub fn finish(self) {
319        logger()
320            .write_log(
321                LogLevel::SUCCESS,
322                &format!("{} [Complete]", self.message),
323                file!(),
324                line!(),
325            )
326            .ok();
327    }
328
329    pub fn finish_with_message<S: Into<String>>(self, message: S) {
330        logger()
331            .write_log(LogLevel::SUCCESS, &message.into(), file!(), line!())
332            .ok();
333    }
334}