lite_log/
logger.rs

1//! A logger that prints all messages with a simple, readable output format.
2//!
3//! Optional features include timestamps, colored output and logging to stderr.
4//!
5//! ```rust
6//! lite_log::LiteLogger::new().env().init().unwrap();
7//!
8//! log::warn!("This is an example message.");
9//! ```
10//!
11//! Some shortcuts are available for common use cases.
12//!
13//! Just initialize logging without any configuration:
14//!
15//! ```rust
16//! lite_log::init().unwrap();
17//! ```
18//!
19//! Set the log level from the `RUST_LOG` environment variable:
20//!
21//! ```rust
22//! lite_log::init_with_env().unwrap();
23//! ```
24//!
25//! Hardcode a default log level:
26//!
27//! ```rust
28//! lite_log::init_with_level(log::Level::Warn).unwrap();
29//! ```
30
31#![cfg_attr(feature = "nightly", feature(thread_id_value))]
32
33#[cfg(feature = "colored")]
34use colored::*;
35use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
36use std::{collections::HashMap, str::FromStr};
37
38#[cfg(feature = "timestamps")]
39use chrono::{Local, Utc,FixedOffset};
40
41#[cfg(feature = "timestamps")]
42const TIMESTAMP_FORMAT_OFFSET: &str = "%Y-%m-%d %H:%M:%S";
43
44#[cfg(feature = "timestamps")]
45const TIMESTAMP_FORMAT_UTC: &str = "%Y-%m-%dT%H:%M:%S%.3f %z";
46
47#[cfg(feature = "timestamps")]
48#[derive(PartialEq)]
49enum Timestamps {
50    None,
51    Local,
52    Utc,
53    UtcOffset(i32),
54}
55
56/// Implements [`Log`] and a set of simple builder methods for configuration.
57///
58/// Use the various "builder" methods on this struct to configure the logger,
59/// then call [`init`] to configure the [`log`] crate.
60pub struct Logger {
61    /// The default logging level
62    default_level: LevelFilter,
63
64    /// The specific logging level for each module
65    ///
66    /// This is used to override the default value for some specific modules.
67    /// After initialization, the vector is sorted so that the first (prefix) match
68    /// directly gives us the desired log level.
69    module_levels: Vec<(String, LevelFilter)>,
70
71    /// Whether to include thread names (and IDs) or not
72    ///
73    /// This field is only available if the `threads` feature is enabled.
74    #[cfg(feature = "threads")]
75    threads: bool,
76
77    /// Control how timestamps are displayed.
78    ///
79    /// This field is only available if the `timestamps` feature is enabled.
80    #[cfg(feature = "timestamps")]
81    timestamps: Timestamps,
82    #[cfg(feature = "timestamps")]
83    timestamps_format: Option<String>,
84
85    /// Whether to use color output or not.
86    ///
87    /// This field is only available if the `color` feature is enabled.
88    #[cfg(feature = "colored")]
89    colors: bool,
90}
91
92impl Logger {
93    /// Initializes the global logger with a Logger instance with
94    /// default log level set to `Level::Trace`.
95    ///
96    /// ```no_run
97    /// use lite_log::LiteLogger as Logger;
98    /// Logger::new().env().init().unwrap();
99    /// log::warn!("This is an example message.");
100    /// ```
101    ///
102    /// [`init`]: #method.init
103    #[must_use = "You must call init() to begin logging"]
104    pub fn new() -> Logger {
105        Logger {
106            default_level: LevelFilter::Trace,
107            module_levels: Vec::new(),
108
109            #[cfg(feature = "threads")]
110            threads: false,
111
112            #[cfg(feature = "timestamps")]
113            timestamps: Timestamps::Utc,
114
115            #[cfg(feature = "timestamps")]
116            timestamps_format: None,
117
118            #[cfg(feature = "colored")]
119            colors: true,
120        }
121    }
122
123    /// Simulates env_logger behavior, which enables the user to choose log level by
124    /// setting a `RUST_LOG` environment variable. The `RUST_LOG` is not set or its value is not
125    /// recognized as one of the log levels, this function will use the `Error` level by default.
126    ///
127    /// You may use the various builder-style methods on this type to configure
128    /// the logger, and you must call [`init`] in order to start logging messages.
129    ///
130    /// ```no_run
131    /// use lite_log::LiteLogger as Logger;
132    /// Logger::from_env().init().unwrap();
133    /// log::warn!("This is an example message.");
134    /// ```
135    ///
136    /// [`init`]: #method.init
137    #[must_use = "You must call init() to begin logging"]
138    #[deprecated(
139        since = "1.12.0",
140        note = "Use [`env`](#method.env) instead. Will be removed in version 2.0.0."
141    )]
142    pub fn from_env() -> Logger {
143        Logger::new().with_level(log::LevelFilter::Error).env()
144    }
145
146    /// Simulates env_logger behavior, which enables the user to choose log
147    /// level by setting a `RUST_LOG` environment variable. This will use
148    /// the default level set by [`with_level`] if `RUST_LOG` is not set or
149    /// can't be parsed as a standard log level.
150    ///
151    /// This must be called after [`with_level`]. If called before
152    /// [`with_level`], it will have no effect.
153    ///
154    /// [`with_level`]: #method.with_level
155    #[must_use = "You must call init() to begin logging"]
156    pub fn env(mut self) -> Logger {
157        self.default_level = std::env::var("RUST_LOG")
158            .ok()
159            .as_deref()
160            .map(log::LevelFilter::from_str)
161            .and_then(Result::ok)
162            .unwrap_or(self.default_level);
163
164        self
165    }
166
167    /// Set the 'default' log level.
168    ///
169    /// You can override the default level for specific modules and their sub-modules using [`with_module_level`]
170    ///
171    /// This must be called before [`env`]. If called after [`env`], it will override the value loaded from the environment.
172    ///
173    /// [`env`]: #method.env
174    /// [`with_module_level`]: #method.with_module_level
175    #[must_use = "You must call init() to begin logging"]
176    pub fn with_level(mut self, level: LevelFilter) -> Logger {
177        self.default_level = level;
178        self
179    }
180
181    /// Override the log level for some specific modules.
182    ///
183    /// This sets the log level of a specific module and all its sub-modules.
184    /// When both the level for a parent module as well as a child module are set,
185    /// the more specific value is taken. If the log level for the same module is
186    /// specified twice, the resulting log level is implementation defined.
187    ///
188    /// # Examples
189    ///
190    /// Silence an overly verbose crate:
191    ///
192    /// ```no_run
193    /// use lite_log::Logger;
194    /// use log::LevelFilter;
195    ///
196    /// Logger::new().with_module_level("chatty_dependency", LevelFilter::Warn).init().unwrap();
197    /// ```
198    ///
199    /// Disable logging for all dependencies:
200    ///
201    /// ```no_run
202    /// use lite_log::Logger;
203    /// use log::LevelFilter;
204    ///
205    /// Logger::new()
206    ///     .with_level(LevelFilter::Off)
207    ///     .with_module_level("my_crate", LevelFilter::Info)
208    ///     .init()
209    ///     .unwrap();
210    /// ```
211    #[must_use = "You must call init() to begin logging"]
212    pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> Logger {
213        self.module_levels.push((target.to_string(), level));
214
215        /* Normally this is only called in `init` to avoid redundancy, but we can't initialize the logger in tests */
216        #[cfg(test)]
217        self.module_levels
218            .sort_by_key(|(name, _level)| name.len().wrapping_neg());
219
220        self
221    }
222
223    /// Override the log level for specific targets.
224    #[must_use = "You must call init() to begin logging"]
225    #[deprecated(
226        since = "1.11.0",
227        note = "Use [`with_module_level`](#method.with_module_level) instead. Will be removed in version 2.0.0."
228    )]
229    pub fn with_target_levels(mut self, target_levels: HashMap<String, LevelFilter>) -> Logger {
230        self.module_levels = target_levels.into_iter().collect();
231
232        /* Normally this is only called in `init` to avoid redundancy, but we can't initialize the logger in tests */
233        #[cfg(test)]
234        self.module_levels
235            .sort_by_key(|(name, _level)| name.len().wrapping_neg());
236
237        self
238    }
239
240    /// Control whether thread names (and IDs) are printed or not.
241    ///
242    /// This method is only available if the `threads` feature is enabled.
243    /// Thread names are disabled by default.
244    #[must_use = "You must call init() to begin logging"]
245    #[cfg(feature = "threads")]
246    pub fn with_threads(mut self, threads: bool) -> Logger {
247        self.threads = threads;
248        self
249    }
250
251    /// Control the format used for timestamps.
252    ///
253    /// Without this, a default format is used depending on the timestamps type.
254    ///
255    /// The syntax for the format can be found in the
256    /// [`chrono` crate book](https://docs.rs/chrono/latest/chrono/format/index.html).
257    ///
258    /// ```
259    /// lite_log::LiteLogger::new()
260    ///  .with_level(log::LevelFilter::Debug)
261    ///  .env()
262    ///  .with_timestamp_format(&String::from("%Y-%m-%d %H:%M:%S"))
263    ///  .init()
264    ///  .unwrap();
265    /// ```
266    #[must_use = "You must call init() to begin logging"]
267    #[cfg(feature = "timestamps")]
268    pub fn with_timestamp_format(mut self, format:&String) -> Logger {
269        self.timestamps_format = Some(format.clone());
270        self
271    }
272
273    /// Don't display any timestamps.
274    ///
275    /// This method is only available if the `timestamps` feature is enabled.
276    #[must_use = "You must call init() to begin logging"]
277    #[cfg(feature = "timestamps")]
278    pub fn without_timestamps(mut self) -> Logger {
279        self.timestamps = Timestamps::None;
280        self
281    }
282
283    /// Display timestamps using the local timezone.
284    ///
285    /// This method is only available if the `timestamps` feature is enabled.
286    #[must_use = "You must call init() to begin logging"]
287    #[cfg(feature = "timestamps")]
288    pub fn with_local_timestamps(mut self) -> Logger {
289        self.timestamps = Timestamps::Local;
290        self
291    }
292
293    /// Display timestamps using UTC.
294    ///
295    /// This method is only available if the `timestamps` feature is enabled.
296    #[must_use = "You must call init() to begin logging"]
297    #[cfg(feature = "timestamps")]
298    pub fn with_utc_timestamps(mut self) -> Logger {
299        self.timestamps = Timestamps::Utc;
300        self
301    }
302
303    /// Display timestamps using a static UTC offset.
304    ///
305    /// This method is only available if the `timestamps` feature is enabled.
306    #[must_use = "You must call init() to begin logging"]
307    #[cfg(feature = "timestamps")]
308    pub fn with_utc_offset(mut self, offset: i32) -> Logger {
309        self.timestamps = Timestamps::UtcOffset(offset);
310        self
311    }
312
313    /// Control whether messages are colored or not.
314    ///
315    /// This method is only available if the `colored` feature is enabled.
316    #[must_use = "You must call init() to begin logging"]
317    #[cfg(feature = "colored")]
318    pub fn with_colors(mut self, colors: bool) -> Logger {
319        self.colors = colors;
320        self
321    }
322
323    /// 'Init' the actual logger, instantiate it and configure it,
324    /// this method MUST be called in order for the logger to be effective.
325    pub fn init(mut self) -> Result<(), SetLoggerError> {
326        #[cfg(all(windows, feature = "colored"))]
327        set_up_color_terminal();
328
329        /* Sort all module levels from most specific to least specific. The length of the module
330         * name is used instead of its actual depth to avoid module name parsing.
331         */
332        self.module_levels
333            .sort_by_key(|(name, _level)| name.len().wrapping_neg());
334        let max_level = self.module_levels.iter().map(|(_name, level)| level).copied().max();
335        let max_level = max_level
336            .map(|lvl| lvl.max(self.default_level))
337            .unwrap_or(self.default_level);
338        log::set_max_level(max_level);
339        log::set_boxed_logger(Box::new(self))?;
340        Ok(())
341    }
342}
343
344impl Default for Logger {
345    /// See [this](struct.Logger.html#method.new)
346    fn default() -> Self {
347        Logger::new()
348    }
349}
350
351impl Log for Logger {
352    fn enabled(&self, metadata: &Metadata) -> bool {
353        &metadata.level().to_level_filter()
354            <= self
355                .module_levels
356                .iter()
357                /* At this point the Vec is already sorted so that we can simply take
358                 * the first match
359                 */
360                .find(|(name, _level)| metadata.target().starts_with(name))
361                .map(|(_name, level)| level)
362                .unwrap_or(&self.default_level)
363    }
364
365    fn log(&self, record: &Record) {
366        if self.enabled(record.metadata()) {
367            let level_string = {
368                #[cfg(feature = "colored")]
369                {
370                    if self.colors {
371                        match record.level() {
372                            Level::Error => format!("{:<5}", record.level().to_string()).red().to_string(),
373                            Level::Warn => format!("{:<5}", record.level().to_string()).yellow().to_string(),
374                            Level::Info => format!("{:<5}", record.level().to_string()).cyan().to_string(),
375                            Level::Debug => format!("{:<5}", record.level().to_string()).purple().to_string(),
376                            Level::Trace => format!("{:<5}", record.level().to_string()).normal().to_string(),
377                        }
378                    } else {
379                        format!("{:<5}", record.level().to_string())
380                    }
381                }
382                #[cfg(not(feature = "colored"))]
383                {
384                    format!("{:<5}", record.level().to_string())
385                }
386            };
387
388            let target = if !record.target().is_empty() {
389                record.target()
390            } else {
391                record.module_path().unwrap_or_default()
392            };
393
394            let thread = {
395                #[cfg(feature = "threads")]
396                if self.threads {
397                    let thread = std::thread::current();
398
399                    format!("@{}", {
400                        #[cfg(feature = "nightly")]
401                        {
402                            thread.name().unwrap_or(&thread.id().as_u64().to_string())
403                        }
404
405                        #[cfg(not(feature = "nightly"))]
406                        {
407                            thread.name().unwrap_or("?")
408                        }
409                    })
410                } else {
411                    "".to_string()
412                }
413
414                #[cfg(not(feature = "threads"))]
415                ""
416            };
417
418            let timestamp = {
419                #[cfg(feature = "timestamps")]
420                match self.timestamps {
421                    Timestamps::None => "".to_string(),
422                    Timestamps::Local => format!( "{} ", Local::now().format(&self.timestamps_format.clone().unwrap_or(String::from(TIMESTAMP_FORMAT_OFFSET)))),
423                    Timestamps::Utc => format!("{} ", Utc::now().format(&self.timestamps_format.clone().unwrap_or(String::from(TIMESTAMP_FORMAT_UTC)))),
424                    Timestamps::UtcOffset(offset) => {
425                        let offset = FixedOffset::east_opt(offset).unwrap();
426                        let now_with_offset = Utc::now().with_timezone(&offset);
427                        let fmt = self.timestamps_format.clone().unwrap_or(String::from(TIMESTAMP_FORMAT_OFFSET));
428                        format!("{} ", now_with_offset.format(&fmt))
429                    },
430                }
431
432                #[cfg(not(feature = "timestamps"))]
433                ""
434            };
435
436            let message = format!("{}{} [{}{}] {}", timestamp, level_string, target, thread, record.args());
437
438            #[cfg(not(feature = "stderr"))]
439            println!("{}", message);
440
441            #[cfg(feature = "stderr")]
442            eprintln!("{}", message);
443        }
444    }
445
446    fn flush(&self) {}
447}
448
449#[cfg(all(windows, feature = "colored"))]
450fn set_up_color_terminal() {
451    use std::io::{stdout, IsTerminal};
452
453    if stdout().is_terminal() {
454        unsafe {
455            use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE;
456            use windows_sys::Win32::System::Console::{
457                GetConsoleMode, GetStdHandle, SetConsoleMode, CONSOLE_MODE, ENABLE_VIRTUAL_TERMINAL_PROCESSING,
458                STD_OUTPUT_HANDLE,
459            };
460
461            let stdout = GetStdHandle(STD_OUTPUT_HANDLE);
462
463            if stdout == INVALID_HANDLE_VALUE {
464                return;
465            }
466
467            let mut mode: CONSOLE_MODE = 0;
468
469            if GetConsoleMode(stdout, &mut mode) == 0 {
470                return;
471            }
472
473            SetConsoleMode(stdout, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
474        }
475    }
476}
477
478/// Initialise the logger with its default configuration.
479///
480/// Log messages will not be filtered.
481/// The `RUST_LOG` environment variable is not used.
482pub fn init() -> Result<(), SetLoggerError> {
483    Logger::new().init()
484}
485
486/// Initialise the logger with its default configuration.
487///
488/// Log messages will not be filtered.
489/// The `RUST_LOG` environment variable is not used.
490///
491/// This function is only available if the `timestamps` feature is enabled.
492#[cfg(feature = "timestamps")]
493pub fn init_utc() -> Result<(), SetLoggerError> {
494    Logger::new().with_utc_timestamps().init()
495}
496
497/// Initialise the logger with the `RUST_LOG` environment variable.
498///
499/// Log messages will be filtered based on the `RUST_LOG` environment variable.
500pub fn init_with_env() -> Result<(), SetLoggerError> {
501    Logger::new().env().init()
502}
503
504/// Initialise the logger with a specific log level.
505///
506/// Log messages below the given [`Level`] will be filtered.
507/// The `RUST_LOG` environment variable is not used.
508pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
509    Logger::new().with_level(level.to_level_filter()).init()
510}
511
512/// Use [`init_with_env`] instead.
513///
514/// This does the same as [`init_with_env`] but unwraps the result.
515#[deprecated(
516    since = "1.12.0",
517    note = "Use [`init_with_env`] instead, which does not unwrap the result. Will be removed in version 2.0.0."
518)]
519pub fn init_by_env() {
520    init_with_env().unwrap()
521}