slog_bunyan/
lib.rs

1//! [Bunyan](https://www.npmjs.com/package/bunyan) formatting for `slog-rs`
2//!
3//! ```
4//! #[macro_use]
5//! extern crate slog;
6//! extern crate slog_bunyan;
7//!
8//! use slog::Drain;
9//! use std::sync::Mutex;
10//!
11//! fn main() {
12//!     let root = slog::Logger::root(
13//!                 Mutex::new(
14//!                     slog_bunyan::default(
15//!                         std::io::stderr()
16//!                     )
17//!                 ).fuse(),
18//!                 o!("build-id" => "8dfljdf")
19//!     );
20//! }
21//! ```
22#![warn(missing_docs)]
23
24use slog::{o, FnValue, Level, Record};
25
26use std::io;
27
28const DEFAULT_NAME: &str = "slog-rs";
29
30fn get_hostname() -> String {
31    match hostname::get() {
32        Ok(h) => h.to_string_lossy().into_owned(),
33        Err(_) => "n/a".to_string(),
34    }
35}
36
37enum BunyanLevel {
38    Fatal = 60,
39    Error = 50,
40    Warn = 40,
41    Info = 30,
42    Debug = 20,
43    Trace = 10,
44}
45
46impl From<Level> for BunyanLevel {
47    fn from(l: Level) -> Self {
48        match l {
49            Level::Critical => BunyanLevel::Fatal,
50            Level::Error => BunyanLevel::Error,
51            Level::Warning => BunyanLevel::Warn,
52            Level::Info => BunyanLevel::Info,
53            Level::Debug => BunyanLevel::Debug,
54            Level::Trace => BunyanLevel::Trace,
55        }
56    }
57}
58
59fn new_with_ts_fn<F, W>(name: &'static str, io: W, ts_f: F) -> slog_json::JsonBuilder<W>
60where
61    F: Fn(&Record) -> Option<String> + Send + Sync + std::panic::RefUnwindSafe + 'static,
62    W: io::Write,
63{
64    slog_json::Json::new(io).add_key_value(o!(
65        "pid" => ::std::process::id(),
66        "hostname" => get_hostname(),
67        "time" => FnValue(ts_f),
68        "level" => FnValue(|rinfo : &Record| {
69            BunyanLevel::from(rinfo.level()) as i8
70        }),
71        "name" => name,
72        "v" => 0usize,
73        "msg" => FnValue(|rinfo : &Record| {
74            rinfo.msg().to_string()
75        })
76    ))
77}
78
79/// Create `slog_json::FormatBuilder` with bunyan key-values
80pub fn new<W>(io: W) -> slog_json::JsonBuilder<W>
81where
82    W: io::Write,
83{
84    with_name(DEFAULT_NAME, io)
85}
86
87/// Create `slog_json::Format` with bunyan key-values
88pub fn default<W>(io: W) -> slog_json::Json<W>
89where
90    W: io::Write,
91{
92    with_name(DEFAULT_NAME, io).build()
93}
94
95/// Create `slog_json::FormatBuilder` with keys for the bunyan [core
96/// fields](https://www.npmjs.com/package/bunyan#core-fields). The
97/// value of the `name` parameter is used to populate the bunyan `name` field.
98pub fn with_name<W>(name: &'static str, io: W) -> slog_json::JsonBuilder<W>
99where
100    W: io::Write,
101{
102    new_with_ts_fn(name, io, |_: &Record| {
103        time::OffsetDateTime::now_local()
104            .unwrap_or_else(|_| time::OffsetDateTime::now_utc())
105            .format(&time::format_description::well_known::Rfc3339)
106            .ok()
107    })
108}
109
110#[cfg(test)]
111mod test {
112    use super::get_hostname;
113    use super::new_with_ts_fn;
114    use super::DEFAULT_NAME;
115    use slog::{b, o};
116    use slog::{Drain, Level, Logger};
117    use slog::{Record, RecordLocation, RecordStatic};
118    use std::io;
119    use std::sync::{Arc, Mutex};
120    use time::macros::datetime;
121
122    struct V(Arc<Mutex<Vec<u8>>>);
123
124    impl io::Write for V {
125        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
126            self.0.lock().unwrap().write(buf)
127        }
128        fn flush(&mut self) -> io::Result<()> {
129            self.0.lock().unwrap().flush()
130        }
131    }
132
133    #[test]
134    fn trivial() {
135        let v = Arc::new(Mutex::new(vec![]));
136        {
137            let v = V(v.clone());
138            let drain = new_with_ts_fn(DEFAULT_NAME, v, |_: &Record| {
139                datetime!(2014-07-08 09:10:11 UTC)
140                    .format(&time::format_description::well_known::Rfc3339)
141                    .ok()
142            })
143            .build();
144
145            let rs = RecordStatic {
146                level: Level::Info,
147                location: &RecordLocation {
148                    file: "filepath",
149                    line: 11192,
150                    column: 0,
151                    function: "",
152                    module: "modulepath",
153                },
154                tag: "target",
155            };
156
157            let log = Logger::root(Mutex::new(drain).fuse(), o!());
158            log.log(&Record::new(&rs, &format_args!("message"), b!()));
159        }
160
161        assert_eq!(
162            String::from_utf8_lossy(&(*v).lock().unwrap()),
163            "{".to_string()
164                + "\"msg\":\"message\","
165                + "\"v\":0,"
166                + "\"name\":\"slog-rs\","
167                + "\"level\":30,"
168                + "\"time\":\"2014-07-08T09:10:11Z\","
169                + "\"hostname\":\""
170                + &get_hostname()
171                + "\","
172                + "\"pid\":"
173                + &::std::process::id().to_string()
174                + "}\n"
175        );
176    }
177
178    #[test]
179    fn custom_name_field() {
180        let v = Arc::new(Mutex::new(vec![]));
181        {
182            let v = V(v.clone());
183            let name = "test-name-123";
184            let drain = new_with_ts_fn(name, v, |_: &Record| {
185                datetime!(2014-07-08 09:10:11 UTC)
186                    .format(&time::format_description::well_known::Rfc3339)
187                    .ok()
188            })
189            .build();
190
191            let rs = RecordStatic {
192                level: Level::Info,
193                location: &RecordLocation {
194                    file: "filepath",
195                    line: 11192,
196                    column: 0,
197                    function: "",
198                    module: "modulepath",
199                },
200                tag: "target",
201            };
202
203            let log = Logger::root(Mutex::new(drain).fuse(), o!());
204            log.log(&Record::new(&rs, &format_args!("message"), b!()));
205        }
206
207        assert_eq!(
208            String::from_utf8_lossy(&(*v).lock().unwrap()),
209            "{".to_string()
210                + "\"msg\":\"message\","
211                + "\"v\":0,"
212                + "\"name\":\"test-name-123\","
213                + "\"level\":30,"
214                + "\"time\":\"2014-07-08T09:10:11Z\","
215                + "\"hostname\":\""
216                + &get_hostname()
217                + "\","
218                + "\"pid\":"
219                + &::std::process::id().to_string()
220                + "}\n"
221        );
222    }
223}