s_structured_log/
lib.rs

1//! # Basic usage
2//!
3//! Cargo.toml:
4//!
5//! ```toml
6//! ...
7//!
8//! [dependencies]
9//! log = "*"
10//! serde = "*"
11//! serde_json = "*"
12//! s-structured-log = "*"
13//! ```
14//!
15//! main.rs:
16//!
17//! ```
18//! #[macro_use]
19//! extern crate log;
20//! #[macro_use]
21//! extern crate s_structured_log;
22//! extern crate serde_json;
23//!
24//! use s_structured_log::{JsonLogger, LoggerOutput, q};
25//!
26//! fn main() {
27//!     JsonLogger::init(LoggerOutput::Stdout, log::LogLevelFilter::Info);
28//!
29//!     s_trace!(json_object! {
30//!         "trace_key1" => 1,
31//!         "trace_key2" => "value2"
32//!     });
33//!     s_debug!(json_object! {
34//!         "debug_key1" => 1,
35//!         "debug_key2" => "value2"
36//!     });
37//!     s_info!(json_object! {
38//!         "info_key1" => 1,
39//!         "info_key2" => "value2"
40//!     });
41//!     s_warn!(json_object! {
42//!         "warn_key1" => 1,
43//!         "warn_key2" => "value2"
44//!     });
45//!     s_error!(json_object! {
46//!         "error_key1" => 1,
47//!         "error_key2" => "value2"
48//!     });
49//!
50//!     trace!("{:?}",
51//!            json_object! {
52//!         "trace_key1" => 1,
53//!         "trace_key2" => "value2"
54//!     });
55//!     error!("{}",
56//!            json_format! {
57//!         "error_key1" => 1,
58//!         "error_key2" => q("value2"),
59//!         "error_key3" => json_format![q("value3"),4]
60//!     });
61//!
62//!     // Output:
63//!     // {"level":"INFO","meta":{"target":"json:basic","location":{"module_path":"basic","file":"examples\\basic.rs","line":20}},"value":{"info_key1":1,"info_key2":"value2"}}
64//!     // {"level":"WARN","meta":{"target":"json:basic","location":{"module_path":"basic","file":"examples\\basic.rs","line":24}},"value":{"warn_key1":1,"warn_key2":"value2"}}
65//!     // {"level":"ERROR","meta":{"target":"json:basic","location":{"module_path":"basic","file":"examples\\basic.rs","line":28}},"value":{"error_key1":1,"error_key2":"value2"}}
66//!     // {"level":"ERROR","meta":{"target":"basic","location":{"module_path":"basic","file":"examples\\basic.rs","line":38}},"value":"{\"error_key1\":1,\"error_key2\":\"value2\",\"error_key3\":[\"value3\",4]}"}
67//! }
68//! ```
69//!
70//! # More complicated JSON
71//!
72//! ```
73//! #[macro_use]
74//! extern crate log;
75//! #[macro_use]
76//! extern crate s_structured_log;
77//! extern crate serde_json;
78//!
79//! use s_structured_log::{JsonLogger, LoggerOutput, q};
80//!
81//! fn main() {
82//!     JsonLogger::init(LoggerOutput::Stderr, log::LogLevelFilter::Info);
83//!
84//!     // use json_object!
85//!     s_info!(json_object! {
86//!         "Fruits" => json_object! {
87//!             "on_the_table" => json_object! {
88//!                 "Apple" => 1,
89//!                 "Orange" => "two",
90//!                 "Grape" => 1.2
91//!             },
92//!             "in_the_basket" => ["Banana", "Strawberry"]
93//!         },
94//!         "Pets" => [
95//!             json_object! {
96//!                 "name" => "Tama",
97//!                 "kind" => "cat",
98//!                 "age" => 3
99//!             },
100//!             json_object! {
101//!                 "name" => "Pochi",
102//!                 "kind" => "dog",
103//!                 "age" => 5
104//!             }
105//!         ]
106//!     });
107//!
108//!     // use json_format! and target with `json:` prefix.
109//!     info!(target: &format!("json:{}", module_path!()),
110//!           "{}",
111//!           json_format! {
112//!         "Fruits" => json_format! {
113//!             "on_the_table" => json_format! {
114//!                 "Apple" => 1,
115//!                 "Orange" => q("two"),
116//!                 "Grape" => 1.2
117//!             },
118//!             "in_the_basket" => json_format![q("Banana"), q("Strawberry")]
119//!         },
120//!         "Pets" => json_format![
121//!             json_format! {
122//!                 "name" => q("Tama"),
123//!                 "kind" => q("cat"),
124//!                 "age" => 3
125//!             },
126//!             json_format! {
127//!                 "name" => q("Pochi"),
128//!                 "kind" => q("dog"),
129//!                 "age" => 5
130//!             }
131//!         ]
132//!     });
133//!
134//!     // use json_format! and default target.
135//!     info!("{}",
136//!           json_format! {
137//!         "Fruits" => json_format! {
138//!             "on_the_table" => json_format! {
139//!                 "Apple" => 1,
140//!                 "Orange" => 2,
141//!                 "Grape" => 1.2
142//!             },
143//!             "in_the_basket" => json_format![q("Banana"), q("Strawberry")]
144//!         },
145//!         "Pets" => json_format![
146//!             json_format! {
147//!                 "name" => q("Tama"),
148//!                 "kind" => q("cat")
149//!             },
150//!             json_format! {
151//!                 "name" => q("Pochi"),
152//!                 "kind" => q("dog")
153//!             }
154//!         ]
155//!     });
156//!
157//!     // Output:
158//!     // {"level":"INFO","meta":{"target":"json:complicated_json","location":{"module_path":"complicated_json","file":"examples\\complicated_json.rs","line":13}},"value":{"Fruits":{"in_the_basket":["Banana","Strawberry"],"on_the_table":{"Apple":1,"Grape":1.2,"Orange":"two"}},"Pets":[{"age":3,"kind":"cat","name":"Tama"},{"age":5,"kind":"dog","name":"Pochi"}]}}
159//!     // {"level":"INFO","meta":{"target":"json:complicated_json","location":{"module_path":"complicated_json","file":"examples\\complicated_json.rs","line":37}},"value":{"Fruits":{"on_the_table":{"Apple":1,"Orange":"two","Grape":1.2},"in_the_basket":["Banana","Strawberry"]},"Pets":[{"name":"Tama","kind":"cat","age":3},{"name":"Pochi","kind":"dog","age":5}]}}
160//!     // {"level":"INFO","meta":{"target":"complicated_json","location":{"module_path":"complicated_json","file":"examples\\complicated_json.rs","line":63}},"value":"{\"Fruits\":{\"on_the_table\":{\"Apple\":1,\"Orange\":2,\"Grape\":1.2},\"in_the_basket\":[\"Banana\",\"Strawberry\"]},\"Pets\":[{\"name\":\"Tama\",\"kind\":\"cat\"},{\"name\":\"Pochi\",\"kind\":\"dog\"}]}"}
161//!
162//! }
163//! ```
164//!
165//! The `json_object!` macro make `serde_json::Map` object.
166//! Values are required `serde::Serialize` implement.
167//!
168//! The `json_format!` macro make JSON text directly.
169//! Values are required `std::fmt::Display` implement.
170//! All Text values are required to add double quote manually
171//! because `json_format` don't add double quote to values automatically.
172//!
173//! `json:` prefix is a tag for indicate to JsonLogger that input text is JSON.
174//!
175
176#[macro_use]
177extern crate log;
178extern crate serde;
179extern crate serde_json;
180
181use serde::{Serialize, Serializer};
182use serde_json::to_string;
183use std::fmt::{Debug, Display};
184use std::io::{Write, stderr, stdout};
185
186#[macro_export]
187macro_rules! json_object {
188    ( $( $key:expr => $value:expr ),* ) => {
189        {
190            use serde_json::{Value, to_value, Map};
191            let mut m: Map<String, Value> = Map::new();
192            $(
193                m.insert($key.to_owned(), to_value(&$value));
194            )*
195            m
196        }
197    }
198}
199
200#[macro_export]
201macro_rules! json_format {
202    ( $( $key:expr => $value:expr ),* ) => {
203        {
204            let mut s = String::with_capacity(128);
205            s.push('{');
206            $(
207                s.push_str(&format!(r#""{}":{},"#, $key, $value));
208            )*
209            let _ = s.pop();
210            s.push('}');
211            s
212        }
213    };
214    ( $( $value:expr ),* ) => {
215        {
216            let mut s = String::with_capacity(128);
217            s.push('[');
218            $(
219                s.push_str(&format!("{},", $value));
220            )*
221            let _ = s.pop();
222            s.push(']');
223            s
224        }
225    }
226}
227
228/// Make a quoted and escaped string for JSON.
229///
230/// ```
231/// use s_structured_log::q;
232///
233/// let quoted = q("abc");
234/// assert_eq!(quoted, "\"abc\"");
235/// ```
236///
237/// ```
238/// use s_structured_log::q;
239///
240/// let x = "ab\ncdef\x02\x03猫\"bbb🐈";
241/// let expected = r#""ab\ncdef\u0002\u0003猫\"bbb🐈""#;
242/// assert_eq!(q(x), expected.to_owned());
243/// ```
244pub fn q<T: Display + ?Sized>(x: &T) -> String {
245    format!("\"{}\"", escape_str(&x.to_string()))
246}
247
248pub trait StructuredLog {
249    fn slog(&self) -> String;
250}
251
252#[inline]
253pub fn serialize<T>(value: &T) -> String
254    where T: StructuredLog
255{
256    value.slog()
257}
258
259#[derive(Debug)]
260pub struct SLogJson<'a, T: 'a>(&'a T);
261
262impl<'a, T: Serialize> SLogJson<'a, T> {
263    pub fn new<'b>(value: &'b T) -> SLogJson<'b, T> {
264        SLogJson(value)
265    }
266}
267
268impl<'a, T: Serialize> Serialize for SLogJson<'a, T> {
269    fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
270        where S: Serializer
271    {
272        self.0.serialize(serializer)
273    }
274}
275
276impl<'a, T: Serialize + Debug> StructuredLog for SLogJson<'a, T> {
277    fn slog(&self) -> String {
278        to_string(self).unwrap_or_else(|err| {
279            json_format! {
280                "format_error" => q(&format!("{:?}", err)),
281                "value" => q(&format!("{:?}", self))
282            }
283        })
284    }
285}
286
287#[macro_export]
288macro_rules! s_error {
289    (target: $target:expr, $value:expr) => {
290        {
291            error!(target: &format!("json:{}", $target), "{}", $crate::serialize(&$crate::SLogJson::new(&$value)));
292        }
293    };
294    ($value:expr) => {
295        {
296            s_error!(target: module_path!(), $value);
297        }
298    };
299}
300
301#[macro_export]
302macro_rules! s_warn {
303    (target: $target:expr, $value:expr) => {
304        {
305            warn!(target: &format!("json:{}", $target), "{}", $crate::serialize(&$crate::SLogJson::new(&$value)));
306        }
307    };
308    ($value:expr) => {
309        {
310            s_warn!(target: module_path!(), $value);
311        }
312    };
313}
314
315#[macro_export]
316macro_rules! s_info {
317    (target: $target:expr, $value:expr) => {
318        {
319            info!(target: &format!("json:{}", $target), "{}", $crate::serialize(&$crate::SLogJson::new(&$value)));
320        }
321    };
322    ($value:expr) => {
323        {
324            s_info!(target: module_path!(), $value);
325        }
326    };
327}
328
329#[macro_export]
330macro_rules! s_debug {
331    (target: $target:expr, $value:expr) => {
332        {
333            debug!(target: &format!("json:{}", $target), "{}", $crate::serialize(&$crate::SLogJson::new(&$value)));
334        }
335    };
336    ($value:expr) => {
337        {
338            s_debug!(target: module_path!(), $value);
339        }
340    };
341}
342
343#[macro_export]
344macro_rules! s_trace {
345    (target: $target:expr, $value:expr) => {
346        {
347            trace!(target: &format!("json:{}", $target), "{}", $crate::serialize(&$crate::SLogJson::new(&$value)));
348        }
349    };
350    ($value:expr) => {
351        {
352            s_trace!(target: module_path!(), $value);
353        }
354    };
355}
356
357/// Escape characters for JSON.
358///
359/// ```
360/// use s_structured_log::escape_str;
361///
362/// let x = "ab\ncdef\x02\x03猫\"bbb🐈";
363/// let expected = r#"ab\ncdef\u0002\u0003猫\"bbb🐈"#;
364/// assert_eq!(escape_str(x), expected.to_owned());
365/// ```
366pub fn escape_str(x: &str) -> String {
367    let mut v = Vec::new();
368    let mut l = 0;
369    let bytes = x.as_bytes();
370    for (i, b) in bytes.iter().enumerate() {
371        let escaped = match *b {
372            b'\x08' => r"\b".to_owned(),
373            b'\x09' => r"\t".to_owned(),
374            b'\x0a' => r"\n".to_owned(),
375            b'\x0c' => r"\f".to_owned(),
376            b'\x0d' => r"\r".to_owned(),
377            b'\\' => r"\\".to_owned(),
378            b'"' => r#"\""#.to_owned(),
379            a if a < b'\x20' => format!(r"\u{:04x}", a),
380            _ => {
381                continue;
382            }
383        };
384
385        if l < i {
386            v.extend_from_slice(&bytes[l..i]);
387        }
388
389        v.extend_from_slice(escaped.as_bytes());
390
391        l = i + 1;
392    }
393
394    v.extend_from_slice(&bytes[l..]);
395
396    String::from_utf8(v).unwrap()
397}
398
399/// This enum indicates where the JsonLogger output to.
400pub enum LoggerOutput {
401    Stdout,
402    Stderr,
403}
404
405/// This logger is a implementation for `log::Log` trait.
406pub struct JsonLogger {
407    filter: log::LogLevelFilter,
408    output: LoggerOutput,
409}
410
411impl JsonLogger {
412    /// ```
413    /// #[macro_use]
414    /// extern crate log;
415    /// #[macro_use]
416    /// extern crate s_structured_log;
417    /// extern crate serde_json;
418    ///
419    /// use log::LogLevelFilter;
420    /// use s_structured_log::{JsonLogger, LoggerOutput};
421    ///
422    /// fn main() {
423    ///     JsonLogger::init(LoggerOutput::Stderr, LogLevelFilter::Info);
424    ///
425    ///     s_info!(json_object! {
426    ///         "key" => "value"
427    ///     });
428    /// }
429    /// ```
430    pub fn init(output: LoggerOutput, filter: log::LogLevelFilter) {
431        let logger = JsonLogger {
432            filter: filter,
433            output: output,
434        };
435        log::set_logger(|max_log_level| {
436                max_log_level.set(logger.filter);
437                Box::new(logger)
438            })
439            .unwrap();
440    }
441}
442
443impl log::Log for JsonLogger {
444    fn enabled(&self, metadata: &log::LogMetadata) -> bool {
445        metadata.level() <= self.filter
446    }
447
448    fn log(&self, record: &log::LogRecord) {
449        if !self.enabled(record.metadata()) {
450            return;
451        }
452
453        let json = json_format! {
454            "level" => q(&record.level()),
455            "meta" => json_format! {
456                "target" => q(&record.target()),
457                "location" => json_format! {
458                    "module_path" => q(&record.location().module_path()),
459                    "file" => q(&record.location().file()),
460                    "line" => record.location().line()
461                }
462            },
463            "value" => if record.target().starts_with("json:") {
464                format!("{}", record.args())
465            } else {
466                q(&record.args().to_string())
467            }
468        };
469
470        let _ = match self.output {
471            LoggerOutput::Stderr => writeln!(stderr(), "{}", json),
472            LoggerOutput::Stdout => writeln!(stdout(), "{}", json),
473        };
474    }
475}
476
477#[cfg(test)]
478mod tests {
479    use q;
480
481    #[test]
482    fn simple_logger() {}
483
484    #[test]
485    fn only_escape_chars() {
486        let x = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\\\"";
487        let expected = r#"\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\b\t\n\u000b\f\r\u000e\u000f\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\\\""#.to_owned();
488        assert_eq!(::escape_str(x), expected);
489    }
490
491    #[test]
492    fn no_escape_chars() {
493        let x = "abcdefghijklmn猫🐈";
494        assert_eq!(::escape_str(x), x.to_owned());
495    }
496
497    #[test]
498    fn random_escape_chars() {
499        let x = "ab\ncdef\x02\x03猫\"bbb🐈";
500        let expected = r#"ab\ncdef\u0002\u0003猫\"bbb🐈"#;
501        assert_eq!(::escape_str(x), expected.to_owned());
502    }
503
504    #[test]
505    fn json_format() {
506        let obj = json_format! {
507            "key1" => q("value1"),
508            "key2" => 1
509        };
510
511        let array = json_format![q("value1"), 1];
512
513        assert_eq!(obj, r#"{"key1":"value1","key2":1}"#);
514        assert_eq!(array, r#"["value1",1]"#);
515    }
516}