easy_logging/
lib.rs

1mod context;
2
3use std::fmt;
4use std::io::{self, Write};
5use std::sync::Mutex;
6
7use ansi_term::Color;
8use fern::{Dispatch, FormatCallback};
9use lazy_static::lazy_static;
10use log::{Level, LevelFilter, SetLoggerError};
11
12pub use fern;
13pub use log;
14pub use crate::context::GlobalContext;
15
16pub struct LoggingConfig {
17    module_name: &'static str,
18    level: Level,
19    get_level_name: fn (level: Level) -> &'static str,
20}
21
22impl LoggingConfig {
23    pub fn new(module_name: &'static str, level: Level) -> Self {
24        LoggingConfig {
25            module_name, level,
26            get_level_name: |level| {
27                match level {
28                    Level::Error => "E: ",
29                    Level::Warn  => "W: ",
30                    Level::Info  => "I: ",
31                    Level::Debug => "D: ",
32                    Level::Trace => "T: ",
33                }
34            }
35        }
36    }
37
38    pub fn minimal(mut self) -> Self {
39        if self.level < Level::Debug {
40            self.get_level_name = |_: Level| "";
41        }
42        self
43    }
44
45    pub fn level_names(mut self, get: fn (level: Level) -> &'static str) -> Self {
46        self.get_level_name = get;
47        self
48    }
49
50    pub fn dispatch(self) -> Dispatch {
51        let stdout_dispatcher =
52            self.configure_formatter(Dispatch::new(), atty::is(atty::Stream::Stdout))
53            .filter(|metadata| metadata.level() >= Level::Info)
54            .chain(io::stdout());
55
56        let stderr_dispatcher =
57            self.configure_formatter(Dispatch::new(), atty::is(atty::Stream::Stderr))
58            .filter(|metadata| metadata.level() < Level::Info)
59            .chain(io::stderr());
60
61        Dispatch::new()
62            .level(if self.level >= Level::Debug {
63                LevelFilter::Warn
64            } else {
65                LevelFilter::Off
66            })
67            .level_for(self.module_name, self.level.to_level_filter())
68            .chain(stdout_dispatcher)
69            .chain(stderr_dispatcher)
70    }
71
72    pub fn build(self) -> Result<(), SetLoggerError> {
73        self.dispatch().apply()
74    }
75
76    fn configure_formatter(&self, dispatcher: Dispatch, colored_output: bool) -> Dispatch {
77        let max_level = self.level;
78        let get_level_name = self.get_level_name;
79
80        if self.level < Level::Debug {
81            dispatcher.format(move |out, message, record| {
82                let level = record.level();
83                let level_name = get_level_name(level);
84                let context = GlobalContext::get(max_level);
85
86                if colored_output {
87                    let color = get_level_color(level);
88                    write_log(out, level, format_args!(
89                        "{color_prefix}{level_name}{context}{message}{color_suffix}",
90                        color_prefix=color.prefix(), color_suffix=color.suffix(),
91                    ));
92                } else {
93                    write_log(out, level, format_args!("{level_name}{context}{message}"));
94                }
95            })
96        } else {
97            dispatcher.format(move |out, message, record| {
98                let time = chrono::Local::now().format("[%T%.3f]");
99                let level = record.level();
100                let level_name = get_level_name(level);
101                let context = GlobalContext::get(max_level);
102
103                let file = if let (Some(mut file), Some(line)) = (record.file(), record.line()) {
104                    let mut file_width = 10;
105                    let mut line_width = 3;
106                    let mut line_extra_width = line / 1000;
107
108                    while line_extra_width > 0 && file_width > 0 {
109                        line_width += 1;
110                        file_width -= 1;
111                        line_extra_width /= 10;
112                    }
113
114                    if file.starts_with("src/") {
115                        file = &file[4..];
116                    }
117
118                    if file.len() > file_width {
119                        file = &file[file.len() - file_width..]
120                    }
121
122                    format!(" [{file:>file_width$}:{line:0line_width$}]",
123                            file=file, file_width=file_width, line=line, line_width=line_width)
124                } else {
125                    String::new()
126                };
127
128                if colored_output {
129                    let color = get_level_color(level);
130                    write_log(out, level, format_args!(
131                        "{color_prefix}{time}{file} {level_name}{context}{message}{color_suffix}",
132                        color_prefix=color.prefix(), color_suffix=color.suffix()
133                    ));
134                } else {
135                    write_log(out, level, format_args!("{time}{file} {level_name}{context}{message}"));
136                }
137            })
138        }
139    }
140}
141
142pub fn init(module_name: &'static str, level: Level) -> Result<(), SetLoggerError> {
143    LoggingConfig::new(module_name, level).build()
144}
145
146fn get_level_color(level: Level) -> Color {
147    match level {
148        Level::Error => Color::Red,
149        Level::Warn  => Color::Yellow,
150        Level::Info  => Color::Green,
151        Level::Debug => Color::Cyan,
152        Level::Trace => Color::Purple,
153    }
154}
155
156fn write_log(out: FormatCallback, level: Level, formatted_message: fmt::Arguments) {
157    lazy_static! {
158        static ref OUTPUT_MUTEX: Mutex<()> = Mutex::new(());
159    }
160
161    // Since we write into stdout and stderr we should guard any write with a mutex to not get the
162    // output interleaved.
163    let _lock = OUTPUT_MUTEX.lock();
164
165    out.finish(formatted_message);
166
167    let _ = if level >= Level::Info {
168        io::stdout().flush()
169    } else {
170        io::stderr().flush()
171    };
172}