cmb_logger/
lib.rs

1// Copyright 2016-2019 Cryptape Technologies LLC.
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// http://www.apache.org/licenses/LICENSE-2.0>.
5// This file may not be copied, modified, or distributed
6// except according to those terms
7
8pub use log::{debug, error, info, log, log_enabled, trace, warn};
9
10use chrono::Local;
11use crossbeam_channel::{bounded, Receiver};
12use libc::c_int;
13use log::LevelFilter;
14use log4rs::append::console::ConsoleAppender;
15use log4rs::append::file::FileAppender;
16use log4rs::config::{Appender, Config, Logger, Root};
17use log4rs::encode::pattern::PatternEncoder;
18use std::env;
19use std::fs;
20use std::io::Error;
21use std::str::FromStr;
22use std::sync::Once;
23use std::thread;
24use std::vec::Vec;
25
26pub enum LogFavour<'a> {
27    Stdout(&'a str),
28    File(&'a str),
29}
30
31#[derive(Debug, Clone)]
32struct Directive {
33    // Module name
34    name: String,
35    // Log level
36    level: LevelFilter,
37}
38
39static INIT_LOG: Once = Once::new();
40
41fn notify(signals: &[c_int]) -> Result<Receiver<c_int>, Error> {
42    let (s, r) = bounded(100);
43    let mut signals = signal_hook::iterator::Signals::new(signals)?;
44    thread::spawn(move || {
45        for signal in signals.forever() {
46            let _ = s.send(signal);
47        }
48    });
49    Ok(r)
50}
51
52pub fn init_config(favour: &LogFavour) {
53    INIT_LOG.call_once(|| {
54        // Parse RUST_LOG
55        let directives: Vec<Directive> = match env::var("RUST_LOG") {
56            Ok(s) => parse_env(&s),
57            Err(_) => Vec::new(),
58        };
59
60        match favour {
61            LogFavour::Stdout(service_name) => {
62                let config = config_console_appender(service_name, directives);
63                log4rs::init_config(config).unwrap();
64            }
65            LogFavour::File(service_name) => {
66                // The config of log4rs
67                let log_name = format!("logs/{}.log", service_name);
68                let directives_clone = directives.clone();
69                let config = config_file_appender(&log_name, directives_clone);
70                let handle = log4rs::init_config(config).unwrap();
71
72                // Log rotate via signal(USR1)
73                let signal = notify(&[signal_hook::consts::SIGUSR1]).unwrap();
74
75                // Any and all threads spawned must come after the first call to notify (or notify_on).
76                // This is so all spawned threads inherit the blocked status of signals.
77                // If a thread starts before notify is called, it will not have the correct signal mask.
78                // When a signal is delivered, the result is indeterminate.
79                let service_name_clone = service_name.to_string();
80                thread::spawn(move || {
81                    loop {
82                        // Blocks until this process is sent an USR1 signal.
83                        signal.recv().unwrap();
84
85                        // Rotate current log file
86                        let time_stamp = Local::now().format("_%Y-%m-%d_%H-%M-%S");
87                        let log_rotate_name =
88                            format!("logs/{}{}.log", &service_name_clone, time_stamp);
89                        if let Err(e) = fs::rename(&log_name, log_rotate_name) {
90                            warn!("logrotate failed because of {:?}", e.kind());
91                            continue;
92                        }
93
94                        // Reconfig
95                        let directives_clone = directives.clone();
96                        let new_config = config_file_appender(&log_name, directives_clone);
97                        handle.set_config(new_config);
98                    }
99                });
100            }
101        }
102    });
103}
104
105// Used in tests
106pub fn init() {
107    init_config(&LogFavour::Stdout(""));
108}
109
110// Used in unit case
111pub fn silent() {
112    INIT_LOG.call_once(|| {
113        let config = Config::builder()
114            .build(Root::builder().build(LevelFilter::Off))
115            .unwrap();
116        log4rs::init_config(config).unwrap();
117    });
118}
119
120// Simple parse env (e.g: crate1,crate2::mod=debug,crate3::mod=trace)
121fn parse_env(env: &str) -> Vec<Directive> {
122    let mut directives = Vec::new();
123
124    for s in env.split(',') {
125        if s.is_empty() {
126            continue;
127        }
128        let mut parts = s.split('=');
129        let (log_level, name) = match (parts.next(), parts.next().map(str::trim), parts.next()) {
130            (Some(part0), None, None) => match LevelFilter::from_str(part0) {
131                Ok(num) => {
132                    println!(
133                        "warning: log level '{}' need explicit crate or module name.",
134                        num
135                    );
136                    continue;
137                }
138                Err(_) => (LevelFilter::Info, part0),
139            },
140            (Some(part0), Some(""), None) => (LevelFilter::Info, part0),
141            (Some(part0), Some(part1), None) => match LevelFilter::from_str(part1) {
142                Ok(num) => (num, part0),
143                _ => {
144                    println!(
145                        "warning: invalid logging spec '{}', \
146                         ignoring it",
147                        part1
148                    );
149                    continue;
150                }
151            },
152            _ => {
153                println!(
154                    "warning: invalid logging spec '{}', \
155                     ignoring it",
156                    s
157                );
158                continue;
159            }
160        };
161
162        if !name.is_empty() {
163            directives.push(Directive {
164                name: name.to_string(),
165                level: log_level,
166            });
167        }
168    }
169
170    directives
171}
172
173fn create_loggers(directives: Vec<Directive>, appender: &str) -> Vec<Logger> {
174    let mut loggers = Vec::new();
175
176    if directives.is_empty() {
177        return loggers;
178    }
179
180    // Create loggers via module/crate and log level
181    for directive in directives {
182        let appender_clone = appender.to_string();
183        let logger = Logger::builder()
184            .appender(appender_clone)
185            .additive(false)
186            .build(directive.name, directive.level);
187        loggers.push(logger);
188    }
189
190    loggers
191}
192
193// FileAppender config
194fn config_file_appender(file_path: &str, directives: Vec<Directive>) -> Config {
195    let pattern = "{{\"_CMB_LOG_SPEC_VERSION\": \"2.0\",\"method\": \"{t:20.20} - {L:5}\", \"type\": \"BASETYPE\", \"level\": \"{l:5}\", \"tid\": \"{I}\", \"ts\": \"{d(%Y-%m-%dT%H:%M:%S%.9fZ)}\", \"content\": \"{m}\"}}{n}";
196    let requests = FileAppender::builder()
197        .encoder(Box::new(PatternEncoder::new(&pattern)))
198        .build(file_path)
199        .unwrap();
200
201    let mut config_builder =
202        Config::builder().appender(Appender::builder().build("requests", Box::new(requests)));
203
204    let loggers = create_loggers(directives, "requests");
205
206    // Config crate or module log level
207    if !loggers.is_empty() {
208        config_builder = config_builder.loggers(loggers.into_iter());
209    }
210
211    // Config global log level
212    config_builder
213        .build(
214            Root::builder()
215                .appender("requests")
216                .build(LevelFilter::Info),
217        )
218        .unwrap()
219}
220
221// ConsoleAppender config
222fn config_console_appender(_service_name: &str, directives: Vec<Directive>) -> Config {
223    let pattern = "{{\"_CMB_LOG_SPEC_VERSION\": \"2.0\",\"method\": \"{t:20.20} - {L:5}\", \"type\": \"BASETYPE\", \"level\": \"{l:5}\", \"tid\": \"{I}\", \"ts\": \"{d(%Y-%m-%dT%H:%M:%S%.9fZ)}\", \"content\": \"{m}\"}}{n}";
224    let stdout = ConsoleAppender::builder()
225        .encoder(Box::new(PatternEncoder::new(&pattern)))
226        .build();
227
228    let mut config_builder =
229        Config::builder().appender(Appender::builder().build("stdout", Box::new(stdout)));
230
231    let loggers = create_loggers(directives, "stdout");
232
233    // Config crate or module log level
234    if !loggers.is_empty() {
235        config_builder = config_builder.loggers(loggers.into_iter());
236    }
237
238    // Config global log level
239    config_builder
240        .build(Root::builder().appender("stdout").build(LevelFilter::Info))
241        .unwrap()
242}
243
244#[cfg(test)]
245mod tests {
246
247    use super::parse_env;
248    use log::LevelFilter;
249
250    #[test]
251    fn parse_env_valid() {
252        let directives = parse_env("crate1::mod1,crate1::mod2=debug,crate2=trace");
253        assert_eq!(directives.len(), 3);
254        assert_eq!(directives[0].name, "crate1::mod1".to_string());
255        assert_eq!(directives[0].level, LevelFilter::Info);
256
257        assert_eq!(directives[1].name, "crate1::mod2".to_string());
258        assert_eq!(directives[1].level, LevelFilter::Debug);
259
260        assert_eq!(directives[2].name, "crate2".to_string());
261        assert_eq!(directives[2].level, LevelFilter::Trace);
262    }
263
264    #[test]
265    fn parse_env_invalid_crate() {
266        let directives = parse_env("crate1::mod=warn=info,crate2=warn");
267        assert_eq!(directives.len(), 1);
268        assert_eq!(directives[0].name, "crate2".to_string());
269        assert_eq!(directives[0].level, LevelFilter::Warn);
270    }
271
272    #[test]
273    fn parse_env_invalid_level() {
274        let directives = parse_env("crate1::mod=wrong,crate2=error");
275        assert_eq!(directives.len(), 1);
276        assert_eq!(directives[0].name, "crate2".to_string());
277        assert_eq!(directives[0].level, LevelFilter::Error);
278    }
279
280    #[test]
281    fn parse_env_empty() {
282        let directives = parse_env("crate1::mod=,=trace");
283        assert_eq!(directives.len(), 1);
284        assert_eq!(directives[0].name, "crate1::mod".to_string());
285        assert_eq!(directives[0].level, LevelFilter::Info);
286    }
287}