structured_logger/
lib.rs

1// (c) 2023-present, IO Rust. All rights reserved.
2// See the file LICENSE for licensing terms.
3
4//! # Structured Logger
5//!
6//! A logging implementation for the [`log`] crate that logs structured values
7//! either synchronous or asynchronous, in JSON, CBOR, or any other format,
8//! to a file, stderr, stdout, or any other destination.
9//! To initialize the logger use the [`Builder`] struct.
10//! It is inspired by [std-logger](https://github.com/Thomasdezeeuw/std-logger).
11//!
12//! This crate provides only a logging implementation. To do actual logging use
13//! the [`log`] crate and it's various macros.
14//!
15//! ## Limiting logging targets
16//! You can use [`Builder::with_target_writer`] method to log messages related specific target to a specific writer.
17//!
18//! ## Crate features
19//!
20//! This crate has three features:
21//! * `log-panic`, enabled by default.
22//!
23//! ### Log-panic feature
24//!
25//! The `log-panic` feature will log all panics using the `error` severity,
26//! rather then using the default panic handler. It will log the panic message
27//! as well as the location and a backtrace, see the log output for an
28//! [`panic_log`] example.
29//!
30//! ## Examples
31//!
32//! * Log panics example: <https://github.com/iorust/structured-logger/blob/main/examples/panic_log.rs>
33//! * Async log example: <https://github.com/iorust/structured-logger/blob/main/examples/async_log.rs>
34//!
35//! Simple example:
36//! ```rust
37//! use serde::Serialize;
38//! use structured_logger::{async_json::new_writer, unix_ms, Builder};
39//!
40//! #[tokio::main]
41//! async fn main() {
42//!     // Initialize the logger.
43//!     Builder::with_level("info")
44//!         .with_target_writer("*", new_writer(tokio::io::stdout()))
45//!         .init();
46//!
47//!     // Or use the default:
48//!     // structured_logger::init();
49//!
50//!     let kv = ContextLog {
51//!         uid: "user123".to_string(),
52//!         action: "upate_book".to_string(),
53//!     };
54//!
55//!     log::info!("hello world");
56//!     // This log will be written to stdout:
57//!     // {"level":"INFO","message":"hello world","target":"simple","timestamp":1679745592127}
58//!
59//!     log::info!(target: "api",
60//!         method = "GET",
61//!         path = "/hello",
62//!         status = 200_u16,
63//!         start = unix_ms(),
64//!         elapsed = 10_u64,
65//!         kv:serde = kv;
66//!         "",
67//!     );
68//!     // This log will be written to stdout:
69//!     // {"elapsed":10,"kv":{"uid":"user123","action":"upate_book"},"level":"INFO","message":"","method":"GET","path":"/https/docs.rs/hello","start":1679745592127,"status":200,"target":"api","timestamp":1679745592127}
70//! }
71//!
72//! #[derive(Serialize)]
73//! struct ContextLog {
74//!     uid: String,
75//!     action: String,
76//! }
77//! ```
78//!
79//! [`panic_log`]: https://github.com/iorust/structured-logger/blob/main/examples/panic_log.rs
80//! [`log`]: https://crates.io/crates/log
81//!
82
83#![doc(html_root_url = "https://docs.rs/structured-logger/latest")]
84#![allow(clippy::needless_doctest_main)]
85
86use json::new_writer;
87use log::{kv::*, Level, LevelFilter, Metadata, Record, SetLoggerError};
88use std::{
89    collections::BTreeMap,
90    env, io,
91    time::{SystemTime, UNIX_EPOCH},
92};
93
94// /// A type alias for BTreeMap<Key<'a>, Value<'a>>.
95// /// BTreeMap is used to keep the order of the keys.
96// type Log<'a> = BTreeMap<Key<'a>, Value<'a>>;
97
98/// A trait that defines how to write a log. You can implement this trait for your custom formatting and writing destination.
99///
100/// Implementation examples:
101/// * <https://github.com/iorust/structured-logger/blob/main/src/json.rs>
102/// * <https://github.com/iorust/structured-logger/blob/main/src/async_json.rs>
103pub trait Writer {
104    /// Writes a structured log to the underlying io::Write instance.
105    fn write_log(&self, value: &BTreeMap<Key, Value>) -> Result<(), io::Error>;
106}
107
108pub mod async_json;
109pub mod json;
110
111/// A struct to initialize the logger for [`log`] crate.
112pub struct Builder {
113    filter: LevelFilter,
114    default_writer: Box<dyn Writer>,
115    writers: Vec<(Target, Box<dyn Writer>)>,
116}
117
118impl Default for Builder {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl Builder {
125    /// Returns a [`Builder`] with default configuration.
126    /// The default configuration is:
127    /// - level filter: get from the environment variable by `get_env_level()`.
128    /// - default writer: write to stderr in JSON format.
129    pub fn new() -> Self {
130        Builder {
131            filter: get_env_level(),
132            default_writer: new_writer(io::stderr()),
133            writers: Vec::new(),
134        }
135    }
136
137    /// Returns a [`Builder`] with a given level filter.
138    /// `level` is a string that can be parsed to `log::LevelFilter`.
139    /// Such as "OFF", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", ignore ascii case.
140    pub fn with_level(level: &str) -> Self {
141        Builder {
142            filter: level.parse().unwrap_or(LevelFilter::Info),
143            default_writer: new_writer(io::stderr()),
144            writers: Vec::new(),
145        }
146    }
147
148    /// Returns a [`Builder`] with a given `writer` as default writer.
149    pub fn with_default_writer(self, writer: Box<dyn Writer>) -> Self {
150        Builder {
151            filter: self.filter,
152            default_writer: writer,
153            writers: self.writers,
154        }
155    }
156
157    /// Returns a [`Builder`] with a given `targets` pattern and `writer`.
158    /// `targets` is a pattern that be used to test log target, if true, the log will be written to the `writer`.
159    /// `writer` is a boxed struct that implements the `Writer` trait.
160    /// You can call this method multiple times in order to add multiple writers.
161    ///
162    /// `targets` pattern examples:
163    /// - `"api"`: match the target "api".
164    /// - `"api,db"`: match the target "api" or "db".
165    /// - `"api*,db"`: match the target "db", "api", "api::v1", "api::v2", etc.
166    /// - `"*"`: match all targets.
167    pub fn with_target_writer(self, targets: &str, writer: Box<dyn Writer>) -> Self {
168        let mut cfg = Builder {
169            filter: self.filter,
170            default_writer: self.default_writer,
171            writers: self.writers,
172        };
173
174        cfg.writers.push((Target::from(targets), writer));
175        cfg
176    }
177
178    /// Builds the logger without registering it in the [`log`] crate.
179    ///
180    /// Unlike [`Builder::init`] and [`Builder::try_init`] this does not register
181    /// the logger into the [`log`] system, allowing it to be combined with
182    /// other logging crates.
183    pub fn build(self) -> impl log::Log {
184        Logger {
185            filter: self.filter,
186            default_writer: self.default_writer,
187            writers: self
188                .writers
189                .into_iter()
190                .map(|(t, w)| (InnerTarget::from(t), w))
191                .collect(),
192        }
193    }
194
195    /// Initialize the logger for [`log`] crate.
196    ///
197    /// See the [crate level documentation] for more.
198    ///
199    /// [crate level documentation]: index.html
200    ///
201    /// # Panics
202    ///
203    /// This will panic if the logger fails to initialize. Use [`Builder::try_init`] if
204    /// you want to handle the error yourself.
205    pub fn init(self) {
206        self.try_init()
207            .unwrap_or_else(|err| panic!("failed to initialize the logger: {}", err));
208    }
209
210    /// Try to initialize the logger for [`log`] crate.
211    ///
212    /// Unlike [`Builder::init`] this doesn't panic when the logger fails to initialize.
213    /// See the [crate level documentation] for more.
214    ///
215    /// [`init`]: fn.init.html
216    /// [crate level documentation]: index.html
217    pub fn try_init(self) -> Result<(), SetLoggerError> {
218        let filter = self.filter;
219        let logger = Box::new(self.build());
220
221        log::set_boxed_logger(logger)?;
222        log::set_max_level(filter);
223
224        #[cfg(feature = "log-panic")]
225        std::panic::set_hook(Box::new(log_panic));
226        Ok(())
227    }
228}
229
230/// Initializes the logger for [`log`] crate with default configuration.
231pub fn init() {
232    Builder::new().init();
233}
234
235/// Returns the current unix timestamp in milliseconds.
236#[inline]
237pub fn unix_ms() -> u64 {
238    let ts = SystemTime::now()
239        .duration_since(UNIX_EPOCH)
240        .expect("system time before Unix epoch");
241    ts.as_millis() as u64
242}
243
244/// Returns the log level from the environment variables: `LOG`, `LOG_LEVEL`, `RUST_LOG`, `TRACE` or `DEBUG`.
245/// Default is `INFO`.
246pub fn get_env_level() -> LevelFilter {
247    for var in &["LOG", "LOG_LEVEL", "RUST_LOG"] {
248        if let Ok(level) = env::var(var) {
249            if let Ok(level) = level.parse() {
250                return level;
251            }
252        }
253    }
254
255    if env::var("TRACE").is_ok() {
256        LevelFilter::Trace
257    } else if env::var("DEBUG").is_ok() {
258        LevelFilter::Debug
259    } else {
260        LevelFilter::Info
261    }
262}
263
264struct Logger {
265    filter: LevelFilter,
266    default_writer: Box<dyn Writer>,
267    writers: Box<[(InnerTarget, Box<dyn Writer>)]>,
268}
269
270impl Logger {
271    fn get_writer(&self, target: &str) -> &dyn Writer {
272        for t in self.writers.iter() {
273            if t.0.test(target) {
274                return t.1.as_ref();
275            }
276        }
277
278        self.default_writer.as_ref()
279    }
280
281    fn try_log(&self, record: &Record) -> Result<(), io::Error> {
282        let kvs = record.key_values();
283        let mut visitor = KeyValueVisitor(BTreeMap::new());
284        let _ = kvs.visit(&mut visitor);
285
286        visitor
287            .0
288            .insert(Key::from("target"), Value::from(record.target()));
289
290        let args = record.args();
291        let msg: String;
292        if let Some(msg) = args.as_str() {
293            visitor.0.insert(Key::from("message"), Value::from(msg));
294        } else {
295            msg = args.to_string();
296            visitor.0.insert(Key::from("message"), Value::from(&msg));
297        }
298
299        let level = record.level();
300        visitor
301            .0
302            .insert(Key::from("level"), Value::from(level.as_str()));
303
304        if level <= Level::Warn {
305            if let Some(val) = record.module_path() {
306                visitor.0.insert(Key::from("module"), Value::from(val));
307            }
308            if let Some(val) = record.file() {
309                visitor.0.insert(Key::from("file"), Value::from(val));
310            }
311            if let Some(val) = record.line() {
312                visitor.0.insert(Key::from("line"), Value::from(val));
313            }
314        }
315
316        visitor
317            .0
318            .insert(Key::from("timestamp"), Value::from(unix_ms()));
319        self.get_writer(record.target()).write_log(&visitor.0)?;
320        Ok(())
321    }
322}
323
324unsafe impl Sync for Logger {}
325unsafe impl Send for Logger {}
326
327impl log::Log for Logger {
328    fn enabled(&self, metadata: &Metadata) -> bool {
329        self.filter >= metadata.level()
330    }
331
332    fn log(&self, record: &Record) {
333        if self.enabled(record.metadata()) {
334            if let Err(err) = self.try_log(record) {
335                // should never happen, but if it does, we log it.
336                log_failure(format!("Logger failed to log: {}", err).as_str());
337            }
338        }
339    }
340
341    fn flush(&self) {}
342}
343
344struct Target {
345    all: bool,
346    prefix: Vec<String>,
347    items: Vec<String>,
348}
349
350impl Target {
351    fn from(targets: &str) -> Self {
352        let mut target = Target {
353            all: false,
354            prefix: Vec::new(),
355            items: Vec::new(),
356        };
357        for t in targets.split(',') {
358            let t = t.trim();
359            if t == "*" {
360                target.all = true;
361                break;
362            } else if t.ends_with('*') {
363                target.prefix.push(t.trim_end_matches('*').to_string());
364            } else {
365                target.items.push(t.to_string());
366            }
367        }
368        target
369    }
370}
371
372struct InnerTarget {
373    all: bool,
374    prefix: Box<[Box<str>]>,
375    items: Box<[Box<str>]>,
376}
377
378impl InnerTarget {
379    fn from(t: Target) -> Self {
380        InnerTarget {
381            all: t.all,
382            prefix: t.prefix.into_iter().map(|s| s.into_boxed_str()).collect(),
383            items: t.items.into_iter().map(|s| s.into_boxed_str()).collect(),
384        }
385    }
386
387    fn test(&self, target: &str) -> bool {
388        if self.all {
389            return true;
390        }
391        if self.items.iter().any(|i| i.as_ref() == target) {
392            return true;
393        }
394        if self.prefix.iter().any(|p| target.starts_with(p.as_ref())) {
395            return true;
396        }
397        false
398    }
399}
400
401struct KeyValueVisitor<'kvs>(BTreeMap<Key<'kvs>, Value<'kvs>>);
402
403impl<'kvs> VisitSource<'kvs> for KeyValueVisitor<'kvs> {
404    #[inline]
405    fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), Error> {
406        self.0.insert(key, value);
407        Ok(())
408    }
409}
410
411/// A fallback logging function that is used in case of logging failure in [`Writer`] implementation.
412/// It will write failure information in JSON to `stderr`.
413pub fn log_failure(msg: &str) {
414    match serde_json::to_string(msg) {
415        Ok(msg) => {
416            eprintln!(
417                "{{\"level\":\"ERROR\",\"message\":{},\"target\":\"structured_logger\",\"timestamp\":{}}}",
418                &msg,
419                unix_ms()
420            );
421        }
422        Err(err) => {
423            // should never happen
424            panic!("log_failure serialize error: {}", err)
425        }
426    }
427}
428
429/// Panic hook that logs the panic using [`log::error!`].
430#[cfg(feature = "log-panic")]
431fn log_panic(info: &std::panic::PanicHookInfo<'_>) {
432    use std::backtrace::Backtrace;
433    use std::thread;
434
435    let mut record = log::Record::builder();
436    let thread = thread::current();
437    let thread_name = thread.name().unwrap_or("unnamed");
438    let backtrace = Backtrace::force_capture();
439
440    let key_values = [
441        ("backtrace", Value::from_debug(&backtrace)),
442        ("thread_name", Value::from(thread_name)),
443    ];
444    let key_values = key_values.as_slice();
445
446    let _ = record
447        .level(log::Level::Error)
448        .target("panic")
449        .key_values(&key_values);
450
451    if let Some(location) = info.location() {
452        let _ = record
453            .file(Some(location.file()))
454            .line(Some(location.line()));
455    };
456
457    log::logger().log(
458        &record
459            .args(format_args!("thread '{thread_name}' {info}"))
460            .build(),
461    );
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467    use gag::BufferRedirect;
468    use serde_json::{de, value};
469    use std::io::Read;
470
471    #[test]
472    fn unix_ms_works() {
473        let now = unix_ms();
474        assert!(now > 1670123456789_u64);
475    }
476
477    #[test]
478    fn get_env_level_works() {
479        assert_eq!(Level::Info, get_env_level());
480
481        env::set_var("LOG", "error");
482        assert_eq!(Level::Error, get_env_level());
483        env::remove_var("LOG");
484
485        env::set_var("LOG_LEVEL", "Debug");
486        assert_eq!(Level::Debug, get_env_level());
487        env::remove_var("LOG_LEVEL");
488
489        env::set_var("RUST_LOG", "WARN");
490        assert_eq!(Level::Warn, get_env_level());
491        env::remove_var("RUST_LOG");
492
493        env::set_var("TRACE", "");
494        assert_eq!(Level::Trace, get_env_level());
495        env::remove_var("TRACE");
496
497        env::set_var("DEBUG", "");
498        assert_eq!(Level::Debug, get_env_level());
499        env::remove_var("DEBUG");
500    }
501
502    #[test]
503    fn target_works() {
504        let target = InnerTarget::from(Target::from("*"));
505        assert!(target.test(""));
506        assert!(target.test("api"));
507        assert!(target.test("hello"));
508
509        let target = InnerTarget::from(Target::from("api*, file,db"));
510        assert!(!target.test(""));
511        assert!(!target.test("apx"));
512        assert!(!target.test("err"));
513        assert!(!target.test("dbx"));
514        assert!(target.test("api"));
515        assert!(target.test("apiinfo"));
516        assert!(target.test("apierr"));
517        assert!(target.test("file"));
518        assert!(target.test("db"));
519
520        let target = InnerTarget::from(Target::from("api*, file, *"));
521        assert!(target.test(""));
522        assert!(target.test("apx"));
523        assert!(target.test("err"));
524        assert!(target.test("api"));
525        assert!(target.test("apiinfo"));
526        assert!(target.test("apierr"));
527        assert!(target.test("error"));
528    }
529
530    #[test]
531    fn log_failure_works() {
532        let cases: Vec<&str> = vec!["", "\"", "hello", "\"hello >", "hello\n", "hello\r"];
533        for case in cases {
534            let buf = BufferRedirect::stderr().unwrap();
535            log_failure(case);
536            let mut msg: String = String::new();
537            buf.into_inner().read_to_string(&mut msg).unwrap();
538            let msg = msg.as_str();
539            // println!("JSON: {}", msg);
540            assert_eq!('\n', msg.chars().last().unwrap());
541
542            let res = de::from_str::<BTreeMap<String, value::Value>>(msg);
543            assert!(res.is_ok());
544            let res = res.unwrap();
545            assert_eq!("ERROR", res.get("level").unwrap());
546            assert_eq!(case, res.get("message").unwrap());
547            assert_eq!("structured_logger", res.get("target").unwrap());
548            assert!(unix_ms() - 999 <= res.get("timestamp").unwrap().as_u64().unwrap());
549        }
550    }
551}