Skip to main content

rustlog/
local.rs

1use core::fmt::Arguments;
2use std::io::{self, IsTerminal, Write};
3use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
4use std::sync::{Arc, Mutex as StdMutex};
5use std::time::Instant;
6
7// Pull from crate root
8use crate::EMIT_LOCK;
9#[cfg(feature = "color")]
10use crate::{color, level_color};
11use crate::{ct_enabled, write_level, write_timestamp, ColorMode, HumanDuration, Level, Target};
12
13/// Local logger
14pub struct Logger {
15    level: AtomicU8,
16    show_tid: AtomicBool,
17    show_time: AtomicBool,
18    show_group: AtomicBool,
19    show_file_line: AtomicBool,
20    color_mode: AtomicU8,
21    sink: StdMutex<Sink>,
22}
23
24struct Sink {
25    target: Target,
26    writer: Option<Arc<StdMutex<Box<dyn Write + Send>>>>,
27}
28
29impl Default for Logger {
30    fn default() -> Self {
31        Self {
32            level: AtomicU8::new(Level::Info as u8),
33            show_tid: AtomicBool::new(cfg!(feature = "thread-id")),
34            show_time: AtomicBool::new(cfg!(feature = "timestamp")),
35            show_group: AtomicBool::new(true),
36            show_file_line: AtomicBool::new(cfg!(feature = "file-line")),
37            color_mode: AtomicU8::new(ColorMode::Auto as u8),
38            sink: StdMutex::new(Sink {
39                target: Target::Stderr,
40                writer: None,
41            }),
42        }
43    }
44}
45
46impl Logger {
47    #[inline]
48    #[must_use]
49    /// Create a new `LoggerBuilder`
50    pub fn builder() -> LoggerBuilder {
51        LoggerBuilder::default()
52    }
53
54    // configuration
55    #[inline]
56    /// Set the log level
57    pub fn set_level(&self, l: Level) {
58        self.level.store(l as u8, Ordering::Relaxed);
59    }
60    #[inline]
61    /// Set whether to show thread ids
62    pub fn set_show_thread_id(&self, on: bool) {
63        self.show_tid.store(on, Ordering::Relaxed);
64    }
65    #[inline]
66    /// Set whether to show timestamps
67    pub fn set_show_time(&self, on: bool) {
68        self.show_time.store(on, Ordering::Relaxed);
69    }
70    #[inline]
71    /// Set whether to show group
72    pub fn set_show_group(&self, on: bool) {
73        self.show_group.store(on, Ordering::Relaxed);
74    }
75    #[inline]
76    /// Set whether to show file and line
77    pub fn set_show_file_line(&self, on: bool) {
78        self.show_file_line.store(on, Ordering::Relaxed);
79    }
80    #[inline]
81    /// Set the color mode
82    pub fn set_color_mode(&self, m: ColorMode) {
83        self.color_mode.store(m as u8, Ordering::Relaxed);
84    }
85
86    #[inline]
87    /// Set the target
88    /// # Panics
89    /// This function will panic if locking the sink fails
90    pub fn set_target(&self, t: Target) {
91        self.sink.lock().unwrap().target = t;
92    }
93    /// Set the writer
94    /// # Panics
95    /// This function will panic if locking the sink fails
96    pub fn set_writer(&self, w: Box<dyn Write + Send>) {
97        let arc = Arc::new(StdMutex::new(w));
98        let mut s = self.sink.lock().unwrap();
99        s.writer = Some(arc);
100        s.target = Target::Writer;
101    }
102    /// Set the output target to a file.
103    /// # Errors
104    /// This function will return an error if the file cannot be opened for writing.
105    pub fn set_file(&self, path: impl AsRef<std::path::Path>) -> io::Result<()> {
106        let f = std::fs::OpenOptions::new()
107            .create(true)
108            .append(true)
109            .open(path)?;
110        self.set_writer(Box::new(f));
111        Ok(())
112    }
113
114    #[inline]
115    fn enabled(&self, l: Level) -> bool {
116        (l as u8) >= self.level.load(Ordering::Relaxed)
117    }
118
119    /// Emit a log message
120    /// # Panics
121    /// This function will panic if locking the sink fails
122    pub fn emit_to(
123        &self,
124        l: Level,
125        group: Option<&'static str>,
126        file: &'static str,
127        line_no: u32,
128        args: Arguments,
129    ) {
130        if !self.enabled(l) || !ct_enabled(l) {
131            return;
132        }
133
134        let (target, writer) = {
135            let s = self.sink.lock().unwrap();
136            (s.target, s.writer.clone())
137        };
138
139        let mut buf = Vec::<u8>::new();
140        let use_color = self.use_color_for_target(target);
141
142        if self.show_time.load(Ordering::Relaxed) {
143            write_timestamp(&mut buf);
144        }
145        write_level(&mut buf, l, use_color);
146
147        if self.show_tid.load(Ordering::Relaxed) {
148            #[cfg(feature = "thread-id")]
149            let _ = write!(&mut buf, " [{:?}]", std::thread::current().id());
150        }
151        if self.show_file_line.load(Ordering::Relaxed) {
152            let _ = write!(&mut buf, " <{file}:{line_no}>");
153        }
154
155        if self.show_group.load(Ordering::Relaxed) {
156            if let Some(g) = group {
157                #[cfg(feature = "color")]
158                if use_color {
159                    let _ = write!(
160                        &mut buf,
161                        " [{}{}{}{}]",
162                        color::BOLD,
163                        level_color(l),
164                        g,
165                        color::RST
166                    );
167                } else {
168                    let _ = write!(&mut buf, " [{g}]");
169                }
170                #[cfg(not(feature = "color"))]
171                {
172                    let _ = write!(&mut buf, " [{g}]");
173                }
174            }
175        }
176
177        let _ = buf.write_all(b" ");
178        let _ = buf.write_fmt(args);
179        let _ = buf.write_all(b"\n");
180
181        let _g = EMIT_LOCK.lock().unwrap();
182        match target {
183            Target::Stdout => {
184                let _ = io::stdout().lock().write_all(&buf);
185            }
186            Target::Stderr => {
187                let _ = io::stderr().lock().write_all(&buf);
188            }
189            Target::Writer => {
190                if let Some(w) = writer {
191                    let _ = w.lock().unwrap().write_all(&buf);
192                }
193            }
194        }
195    }
196
197    #[inline]
198    fn use_color_for_target(&self, target: Target) -> bool {
199        #[cfg(not(feature = "color"))]
200        {
201            return false;
202        }
203        #[cfg(feature = "color")]
204        match ColorMode::from(self.color_mode.load(Ordering::Relaxed)) {
205            ColorMode::Always => true,
206            ColorMode::Never => false,
207            ColorMode::Auto => match target {
208                Target::Stdout => io::stdout().is_terminal(),
209                Target::Stderr => io::stderr().is_terminal(),
210                Target::Writer => false,
211            },
212        }
213    }
214}
215
216/// Timer guard
217pub struct TimerGuard<'a> {
218    logger: &'a Logger,
219    label: &'static str,
220    start: Instant,
221    file: &'static str,
222    line: u32,
223}
224impl<'a> TimerGuard<'a> {
225    /// Create a new timer guard
226    #[inline]
227    #[must_use]
228    pub fn new_at(logger: &'a Logger, label: &'static str, file: &'static str, line: u32) -> Self {
229        Self {
230            logger,
231            label,
232            start: Instant::now(),
233            file,
234            line,
235        }
236    }
237}
238impl Drop for TimerGuard<'_> {
239    fn drop(&mut self) {
240        let elapsed = self.start.elapsed();
241        self.logger.emit_to(
242            Level::Info,
243            Some(self.label),
244            self.file,
245            self.line,
246            format_args!("took {}", HumanDuration(elapsed)),
247        );
248    }
249}
250#[macro_export]
251/// Macro for timing a scope
252macro_rules! __rustlog_local_scope_time {
253    ($lg:expr, $label:expr) => {
254        let _rustlog_scope_time_guard =
255            $crate::local::TimerGuard::new_at($lg, $label, file!(), line!());
256    };
257    ($lg:expr, $label:expr, $body:block) => {{
258        let _rustlog_scope_time_guard =
259            $crate::local::TimerGuard::new_at($lg, $label, file!(), line!());
260        $body
261    }};
262}
263
264// Helper conversions if you keep enums repr(u8)
265impl From<u8> for ColorMode {
266    fn from(x: u8) -> Self {
267        match x {
268            1 => Self::Always,
269            2 => Self::Never,
270            _ => Self::Auto,
271        }
272    }
273}
274
275/// Builder for `Logger`
276pub struct LoggerBuilder {
277    level: Level,
278    show_tid: Option<bool>,
279    show_time: Option<bool>,
280    show_group: Option<bool>,
281    show_file_line: Option<bool>,
282    color_mode: Option<ColorMode>,
283    target: Target,
284    writer: Option<Arc<StdMutex<Box<dyn Write + Send>>>>,
285    file_path: Option<std::path::PathBuf>,
286}
287impl Default for LoggerBuilder {
288    fn default() -> Self {
289        Self {
290            level: Level::Info,
291            show_tid: None,
292            show_time: None,
293            show_group: None,
294            show_file_line: None,
295            color_mode: None,
296            target: Target::Stderr,
297            writer: None,
298            file_path: None,
299        }
300    }
301}
302
303impl LoggerBuilder {
304    #[inline]
305    #[must_use]
306    /// Set the log level
307    pub const fn set_level(mut self, l: Level) -> Self {
308        self.level = l;
309        self
310    }
311    #[inline]
312    #[must_use]
313    /// Show the thread id
314    pub const fn set_show_thread_id(mut self, on: bool) -> Self {
315        self.show_tid = Some(on);
316        self
317    }
318    #[inline]
319    #[must_use]
320    /// Show the timestamp
321    pub const fn set_show_time(mut self, on: bool) -> Self {
322        self.show_time = Some(on);
323        self
324    }
325    #[inline]
326    #[must_use]
327    /// Show the log group
328    pub const fn set_show_group(mut self, on: bool) -> Self {
329        self.show_group = Some(on);
330        self
331    }
332    #[inline]
333    #[must_use]
334    /// Show the file and line number
335    pub const fn set_show_file_line(mut self, on: bool) -> Self {
336        self.show_file_line = Some(on);
337        self
338    }
339    #[inline]
340    #[must_use]
341    /// Set the color mode
342    pub const fn set_color_mode(mut self, m: ColorMode) -> Self {
343        self.color_mode = Some(m);
344        self
345    }
346    #[inline]
347    #[must_use]
348    /// Set the output target to stdout
349    pub const fn stdout(mut self) -> Self {
350        self.target = Target::Stdout;
351        self
352    }
353    #[inline]
354    #[must_use]
355    /// Set the output target to stderr
356    pub const fn stderr(mut self) -> Self {
357        self.target = Target::Stderr;
358        self
359    }
360    #[inline]
361    #[must_use]
362    /// Set the output target to a custom writer
363    pub fn set_writer(mut self, w: Box<dyn Write + Send>) -> Self {
364        self.target = Target::Writer;
365        self.writer = Some(Arc::new(StdMutex::new(w)));
366        self
367    }
368    #[inline]
369    #[must_use]
370    /// Set the output target to a file
371    pub fn file(mut self, p: impl AsRef<std::path::Path>) -> Self {
372        self.target = Target::Writer;
373        self.file_path = Some(p.as_ref().to_owned());
374        self
375    }
376
377    /// Build the logger
378    /// # Errors
379    /// This function will return an error if the file cannot be opened for writing
380    pub fn build(self) -> io::Result<Logger> {
381        let writer = match (self.target, self.file_path) {
382            (Target::Writer, Some(p)) => {
383                let f = std::fs::OpenOptions::new()
384                    .create(true)
385                    .append(true)
386                    .open(p)?;
387                Some(Arc::new(
388                    StdMutex::new(Box::new(f) as Box<dyn Write + Send>),
389                ))
390            }
391            _ => self.writer,
392        };
393        let lg = Logger {
394            sink: StdMutex::new(Sink {
395                target: self.target,
396                writer,
397            }),
398            ..Logger::default()
399        };
400        lg.set_level(self.level);
401        if let Some(x) = self.show_tid {
402            lg.set_show_thread_id(x);
403        }
404        if let Some(x) = self.show_time {
405            lg.set_show_time(x);
406        }
407        if let Some(x) = self.show_group {
408            lg.set_show_group(x);
409        }
410        if let Some(x) = self.show_file_line {
411            lg.set_show_file_line(x);
412        }
413        if let Some(x) = self.color_mode {
414            lg.set_color_mode(x);
415        }
416        Ok(lg)
417    }
418
419    /// Build the logger and leak it
420    /// # Errors
421    /// This function will return an error if the file cannot be opened for writing
422    pub fn build_static(self) -> io::Result<&'static Logger> {
423        Ok(Box::leak(Box::new(self.build()?)))
424    }
425}
426
427// ===== Macros (require a logger argument) ====================================
428// We purposely keep these inside the module and re-export them below. In most
429// editors, `use rustlog::local::info; info!(lg, "...")` is ergonomic and avoids
430// name collision with the root `rustlog::info!`.
431
432#[macro_export]
433/// Emit a log message
434macro_rules! __rustlog_local_log {
435    ($lg:expr, $lvl:expr, $grp:expr, $($t:tt)+) => {{
436        let __lg = $lg; // evaluate once
437        if $crate::ct_enabled($lvl) { __lg.emit_to($lvl, $grp, file!(), line!(), format_args!($($t)+)); }
438    }}
439}
440
441#[macro_export]
442/// Emit a trace log message
443macro_rules! __rustlog_local_trace { ($lg:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Trace, None, $($t)+) } }
444#[macro_export]
445/// Emit a debug log message
446macro_rules! __rustlog_local_debug { ($lg:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Debug, None, $($t)+) } }
447#[macro_export]
448/// Emit an info log message
449macro_rules! __rustlog_local_info  { ($lg:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Info,  None, $($t)+) } }
450#[macro_export]
451/// Emit a warning log message
452macro_rules! __rustlog_local_warn  { ($lg:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Warn,  None, $($t)+) } }
453#[macro_export]
454/// Emit an error log message
455macro_rules! __rustlog_local_error { ($lg:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Error, None, $($t)+) } }
456#[macro_export]
457/// Emit a fatal log message
458macro_rules! __rustlog_local_fatal { ($lg:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Fatal, None, $($t)+) } }
459
460#[macro_export]
461/// Emit a trace group
462macro_rules! __rustlog_local_trace_group { ($lg:expr, $grp:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Trace, Some($grp), $($t)+) } }
463#[macro_export]
464/// Emit a debug group
465macro_rules! __rustlog_local_debug_group { ($lg:expr, $grp:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Debug, Some($grp), $($t)+) } }
466#[macro_export]
467/// Emit an info group
468macro_rules! __rustlog_local_info_group  { ($lg:expr, $grp:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Info,  Some($grp), $($t)+) } }
469#[macro_export]
470/// Emit a warning group
471macro_rules! __rustlog_local_warn_group  { ($lg:expr, $grp:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Warn,  Some($grp), $($t)+) } }
472#[macro_export]
473/// Emit an error group
474macro_rules! __rustlog_local_error_group { ($lg:expr, $grp:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Error, Some($grp), $($t)+) } }
475#[macro_export]
476/// Emit a fatal group
477macro_rules! __rustlog_local_fatal_group { ($lg:expr, $grp:expr, $($t:tt)+) => { $crate::__rustlog_local_log!($lg, $crate::Level::Fatal, Some($grp), $($t)+) } }
478
479// Re-export ergonomic names under `rustlog::local`.
480// Import style: `use rustlog::local::info; info!(logger, "...");`
481// (Note: macro re-export keeps them callable after `use`; absolute path calling
482// as `rustlog::local::info!` may depend on toolchain; the import form is recommended.)
483pub use crate::__rustlog_local_debug as debug;
484pub use crate::__rustlog_local_error as error;
485pub use crate::__rustlog_local_fatal as fatal;
486pub use crate::__rustlog_local_info as info;
487pub use crate::__rustlog_local_trace as trace;
488pub use crate::__rustlog_local_warn as warn;
489
490pub use crate::__rustlog_local_debug_group as debug_group;
491pub use crate::__rustlog_local_error_group as error_group;
492pub use crate::__rustlog_local_fatal_group as fatal_group;
493pub use crate::__rustlog_local_info_group as info_group;
494pub use crate::__rustlog_local_trace_group as trace_group;
495pub use crate::__rustlog_local_warn_group as warn_group;
496
497pub use crate::__rustlog_local_scope_time as scope_time;