slog_kickstarter/
lib.rs

1//! Slog Kickstarter. Easily sets up slog for structured logging.
2//!
3//! - enables JSON logging if `RUST_LOG_JSON=1` (i.e. set `RUST_LOG_JSON=1` for your deployment, or put `ENV RUST_LOG_JSON=1` into your Dockerfile)
4//! - inits and configures stdlogger, so crates using `info!()` from the (default) [log-crate](https://crates.io/crates/log) can log messages
5//! - allows to enable debugging for given modules (typically your own modules)
6//! - sets default loglevel 'Info'
7//! - supports well-known `[RUST_LOG](https://crates.io/crates/env_logger)`
8//!
9//! Usage:
10//! ```rust
11//! use slog_kickstarter::SlogKickstarter;
12//! use slog_scope::set_global_logger;
13//! use slog::o;
14//!
15//! let root_logger = SlogKickstarter::new("service-name").init();
16//! let _guard = set_global_logger(root_logger.new(o!("scope" => "global")));
17//! ```
18
19use chrono::prelude::*;
20use chrono::Local;
21use slog::Record;
22use slog::{o, Drain, FilterLevel, Fuse, Logger};
23use slog::{FnValue, PushFnValue};
24use slog_async::Async;
25use slog_envlogger::LogBuilder as EnvLogBuilder;
26use slog_json::Json;
27use slog_term::{CompactFormat, TermDecorator};
28use std::env;
29
30/// the actual slog builder
31pub struct SlogKickstarter {
32    default_filter_level: FilterLevel,
33    debug_modules: Vec<&'static str>,
34    service_name: String,
35    init_std_log: bool,
36    use_json_logging: bool,
37}
38
39impl SlogKickstarter {
40    #[must_use]
41    /// initialize the log-builder with a name for your service
42    pub fn new<S: Into<String>>(service_name: S) -> Self {
43        let use_json_logging = env::var("RUST_LOG_JSON")
44            .map(|v| v == "1")
45            .unwrap_or_default();
46
47        Self {
48            default_filter_level: FilterLevel::Info,
49            debug_modules: vec![],
50            service_name: service_name.into(),
51            init_std_log: true,
52            use_json_logging,
53        }
54    }
55
56    /// enable debug-log for the given module. May be called multiple times to add debug logging
57    /// for multiple modules
58    pub fn with_debug_log_for(&mut self, module_name: &'static str) -> &mut Self {
59        self.debug_modules.push(module_name);
60        self
61    }
62
63    /// set a default loglevel
64    pub fn with_default_level(&mut self, level: FilterLevel) -> &mut Self {
65        self.default_filter_level = level;
66        self
67    }
68
69    /// enforce JSON logging
70    ///
71    /// this should typically be set via `RUST_LOG_JSON=1`
72    pub fn with_json_logging(&mut self) -> &mut Self {
73        self.use_json_logging = true;
74        self
75    }
76
77    /// enforce **no** JSON logging
78    ///
79    /// this should typically be set via `RUST_LOG_JSON=0`, or just leaving out `RUST_LOG_JSON`,
80    /// as this is the default
81    pub fn without_json_logging(&mut self) -> &mut Self {
82        self.use_json_logging = false;
83        self
84    }
85
86    /// do not initialize stdlog, i.e. disable slog for logs from [log](https://crates.io/crates/log)
87    pub fn without_stdlog(&mut self) -> &mut Self {
88        self.init_std_log = false;
89        self
90    }
91
92    /// initialize the logger based on the builder
93    #[must_use]
94    pub fn init(&self) -> Logger {
95        // output in json-format iff RUST_LOG_JSON=1
96        let drain = if self.use_json_logging {
97            self.setup_json_logging()
98        } else {
99            self.setup_term_logging()
100        };
101
102        if self.init_std_log {
103            let _guard = slog_stdlog::init();
104        }
105
106        slog::Logger::root(
107            drain,
108            o!("service" => self.service_name.to_owned(), "log_type" => "application", "application_type" => "service", "module" => FnValue(move |info| {
109                info.module().to_string()
110            })
111            ),
112        )
113    }
114
115    fn setup_json_logging(&self) -> Fuse<Async> {
116        let drain = Json::new(std::io::stdout())
117            .add_key_value(o!(
118            "@timestamp" => PushFnValue(move |_ : &Record, ser| {
119                ser.emit(Local::now().to_rfc3339_opts(SecondsFormat::Secs, true))
120            }),
121            "loglevel" => FnValue(move |rinfo : &Record| {
122                rinfo.level().as_str()
123            }),
124            "msg" => PushFnValue(move |record : &Record, ser| {
125                ser.emit(record.msg())
126            }),
127            ))
128            .build()
129            .fuse();
130
131        let builder = EnvLogBuilder::new(drain)
132            // set default log-level 'info'…
133            .filter(None, self.default_filter_level);
134
135        let builder = self.debug_modules.iter().fold(builder, |b, &module_name| {
136            b.filter(Some(module_name), FilterLevel::Debug)
137        });
138
139        let drain = builder
140            //but override with RUST_LOG (if given)
141            .parse(env::var("RUST_LOG").unwrap_or_default().as_str())
142            .build()
143            .fuse();
144
145        slog_async::Async::new(drain).build().fuse()
146    }
147
148    fn setup_term_logging(&self) -> Fuse<Async> {
149        let decorator = TermDecorator::new().build();
150        let drain = CompactFormat::new(decorator).build().fuse();
151
152        // builder with given default loglevel as default for all modules
153        let builder = EnvLogBuilder::new(drain).filter(None, self.default_filter_level);
154
155        // set debug-exceptions for specific modules
156        let builder = self.debug_modules.iter().fold(builder, |b, &module_name| {
157            b.filter(Some(module_name), FilterLevel::Debug)
158        });
159
160        let drain = builder
161            // override with RUST_LOG (if given)
162            .parse(env::var("RUST_LOG").unwrap_or_default().as_str())
163            .build()
164            .fuse();
165
166        slog_async::Async::new(drain).build().fuse()
167    }
168}