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}