simplog/
lib.rs

1#![deny(missing_docs)]
2
3//! `simplog` is as its name suggests a very simpler logging implementation for rust
4//! It provides three main features
5//!    - Settable log level (or verbosity) (default is Log::Level::Error)
6//!    - Optional prefix each log line with the Level it corresponds to (after timestamp if present)
7//!    - Optional timestamp prefixed to each line
8
9use std::io;
10use std::io::{stderr, stdout, Write};
11use std::str::FromStr;
12
13use log::{Level, Log, Metadata, Record};
14use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
15use atty::Stream;
16use std::time::Instant;
17
18/// Use the `SimpleLogger` struct to initialize a logger. From then on, the rust `log` framework
19/// should be used to output log statements as usual.
20///
21/// # Example
22/// ```
23/// use log::{info, error};
24/// use simplog::SimpleLogger;
25///
26/// SimpleLogger::init(None); // Log level defaults to `Error`
27/// info!("Hello World!");
28/// // Produces nothing
29/// error!("Goodbye World!");
30/// // Produces "Goodbye World"
31/// ```
32#[derive(Clone)]
33pub struct SimpleLogger {
34    log_level: Level,
35    prefix: bool,
36    start: Instant,
37    timestamp: bool,
38}
39
40const DEFAULT_LOG_LEVEL: Level = Level::Error;
41
42impl SimpleLogger {
43    /// Initialize the logger, with an optionally provided log level (`verbosity`) in a `&str`
44    /// If `None` is provided -> The log level will be set to `Error`
45    /// If 'Some(`verbosity') is a &str with a valid log level, the string will be parsed and if
46    /// valid set as the log level.
47    ///
48    /// # Example
49    /// ```
50    /// use log::info;
51    /// use simplog::SimpleLogger;
52    ///
53    /// SimpleLogger::init(Some("info"));
54    /// info!("Hello World!");
55    /// // Produces "Hello World"
56    /// ```
57    pub fn init(verbosity: Option<&str>) {
58        Self::init_prefix(verbosity, true)
59    }
60
61    /// Initialize the logger, with an optionally provided log level (`verbosity`) in a &str
62    /// The default log level is Error if `None` is provided.
63    /// `prefix` determines whether each log line output is prefixed with the level that produced it
64    ///
65    /// # Example
66    /// ```
67    /// use log::info;
68    /// use simplog::SimpleLogger;
69    ///
70    /// SimpleLogger::init_prefix(Some("info"), true);
71    /// info!("Hello World!");
72    /// // Produces "INFO   - Hello World"
73    /// ```
74    pub fn init_prefix(verbosity: Option<&str>, prefix: bool) {
75        Self::init_prefix_timestamp(verbosity, prefix, false);
76    }
77
78    /// Initialize the logger, with an optionally provided log level (`verbosity`) in a &str
79    /// The default log level is Error if `None` is provided.
80    /// `prefix` determines whether each log line output is prefixed with the level that produced it
81    /// if `timestamp` is true, each log line will be prefixed with the elapsed time since the
82    /// logger was initialized
83    ///
84    /// # Example
85    /// ```
86    /// use log::info;
87    /// use simplog::SimpleLogger;
88    ///
89    /// let mut logger = SimpleLogger::init_prefix_timestamp(Some("info"), false, true);
90    /// info!("Hello World!");
91    /// // Produces "1.246717ms   Hello World"
92    /// ```
93    pub fn init_prefix_timestamp(verbosity: Option<&str>, prefix: bool, timestamp: bool) {
94        let log_level = parse_log_level(verbosity);
95        let simplogger = SimpleLogger {
96            log_level,
97            prefix,
98            start: Instant::now(),
99            timestamp,
100        };
101        let logger = Box::new(simplogger);
102        let _ = log::set_boxed_logger(logger);
103        log::set_max_level(log_level.to_level_filter());
104    }
105}
106
107/*
108    Parse an optional String argument ("debug", "info", "trace", "error") into a log level that
109    can be used to set verbosity of output. If none is supplied or there is an error parsing the
110    String, then the DEFAULT_LOG_LEVEL of "Error" is used.
111*/
112fn parse_log_level(arg: Option<&str>) -> Level {
113    match arg {
114        None => DEFAULT_LOG_LEVEL,
115        Some(arg) => match Level::from_str(arg) {
116            Ok(ll) => ll,
117            Err(_) => DEFAULT_LOG_LEVEL
118        }
119    }
120}
121
122/*
123    Implement the simpler logger.
124    - depending on the way Logger was created a prefix with the level of the output is printed or not
125    - "Error" level output is printed to STDERR, all other levels are printed to STDOUT
126*/
127impl Log for SimpleLogger {
128    fn enabled(&self, metadata: &Metadata) -> bool {
129        metadata.level() <= self.log_level
130    }
131
132    fn log(&self, record: &Record) {
133        if self.enabled(record.metadata()) {
134            let mut stdout = StandardStream::stdout(ColorChoice::Always);
135
136            let message = if self.prefix {
137                format!("{}\t- {}", record.level(), record.args())
138            } else {
139                format!("{}", record.args())
140            };
141
142            if atty::is(Stream::Stdout) {
143                match record.level() {
144                    Level::Error => stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red))).unwrap(),
145                    Level::Warn => stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow))).unwrap(),
146                    Level::Info=> stdout.set_color(ColorSpec::new().set_fg(Some(Color::Magenta))).unwrap(),
147                    Level::Debug=> stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue))).unwrap(),
148                    Level::Trace=> stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green))).unwrap()
149                }
150            }
151
152            if self.timestamp {
153                let stdout = io::stdout();
154                let mut handle = stdout.lock();
155                let _ = handle.write_all(
156                    format!("{:?} {}\n", self.start.elapsed(), message).as_bytes());
157            } else {
158                let stdout = io::stdout();
159                let mut handle = stdout.lock();
160                let _ = handle.write_all(
161                    format!("{}\n", message).as_bytes());
162            }
163        }
164    }
165
166    fn flush(&self) {
167        stdout().flush().unwrap();
168        stderr().flush().unwrap();
169    }
170}
171
172#[cfg(test)]
173mod test {
174    use log::Level;
175
176    use super::SimpleLogger;
177
178    #[test]
179    fn no_log_level_arg() {
180        assert_eq!(super::parse_log_level(None), super::DEFAULT_LOG_LEVEL);
181    }
182
183    #[test]
184    fn invalid_log_level_arg() {
185        assert_eq!(super::parse_log_level(Some("garbage")), super::DEFAULT_LOG_LEVEL);
186    }
187
188    #[test]
189    fn info_log_level_arg() {
190        assert_eq!(super::parse_log_level(Some("INFO")), Level::Info);
191    }
192
193    #[test]
194    fn error_log_level_arg() {
195        assert_eq!(super::parse_log_level(Some("ERROR")), Level::Error);
196    }
197
198    #[test]
199    fn parse_debug_log_level_arg() {
200        assert_eq!(super::parse_log_level(Some("DEBUG")), Level::Debug);
201        assert_eq!(super::parse_log_level(Some("debug")), Level::Debug);
202    }
203
204    #[test]
205    fn init_legacy_no_levels() {
206        SimpleLogger::init(None);
207    }
208
209    #[test]
210    fn init_legacy_debug_level() {
211        SimpleLogger::init(Some("DEBUG"));
212    }
213
214    #[test]
215    fn init_no_level_no_prefix() {
216        SimpleLogger::init_prefix(None, false);
217    }
218}