Skip to main content

rs_utils/log/kv/
format.rs

1//! Custom `env_logger` formats
2
3use std::{io, thread};
4use env_logger;
5use log;
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub struct EnvLoggerFormatConfig {
9  pub thread : bool,
10  pub target : bool,
11  pub file   : bool
12}
13
14/// Formats log messages as a json object string
15pub fn env_logger_json_formatter (config : EnvLoggerFormatConfig)
16  -> impl Fn(&mut env_logger::fmt::Formatter, &log::Record<'_>) -> io::Result<()>
17{
18  use io::Write;
19  #[derive(Default)]
20  struct KVVisitor(pub String);
21  impl <'kvs> log::kv::VisitSource <'kvs> for KVVisitor {
22    fn visit_pair (&mut self, key : log::kv::Key <'kvs>, value : log::kv::Value <'kvs>)
23      -> Result <(), log::kv::Error>
24    {
25      self.0 += format!(",\"{}\":{}", key, serde_json::to_string (&value).unwrap())
26        .as_str();
27      Ok(())
28    }
29  }
30  move |buf : &mut env_logger::fmt::Formatter, record : &log::Record|{
31    let thread_string = if config.thread {
32      thread::current().name().map_or_else (
33        || format!(",\"thread\":\"{:?}\"", thread::current().id())
34          .replace ("ThreadId", "unnamed"),
35        |name| format!(",\"thread\":\"{name}\""))
36    } else {
37      "".to_string()
38    };
39    let target_string = if config.target {
40      format!(",\"target\":\"{}\"", record.target())
41    } else {
42      "".to_string()
43    };
44    let file_string = if config.file {
45      format!(",\"file\":\"{}:{}\"",
46        record.file().unwrap_or ("<unknown>"), record.line().unwrap_or (0))
47    } else {
48      "".to_string()
49    };
50    let mut kvv = KVVisitor::default();
51    record.key_values().visit (&mut kvv).unwrap();
52    writeln!(buf, "{{\"ts\":\"{}\",\"level\":\"{}\"{}{}{},\"msg\":\"{}\"{}}}",
53      buf.timestamp(), record.level(), thread_string, target_string,
54      file_string, record.args(), kvv.0
55    )
56  }
57}
58
59/// A custom `env_logger` format adding thread, target, and file information:
60/// ```text
61/// <ts> <level> <thread> <target> <file>: <msg> [ <key>=<value>]
62/// ```
63pub fn env_logger_custom_formatter (config : EnvLoggerFormatConfig)
64  -> impl Fn(&mut env_logger::fmt::Formatter, &log::Record<'_>) -> io::Result<()>
65{
66  use io::Write;
67  #[derive(Default)]
68  struct KVVisitor(pub String);
69  impl <'kvs> log::kv::VisitSource <'kvs> for KVVisitor {
70    fn visit_pair (&mut self, key : log::kv::Key <'kvs>, value : log::kv::Value <'kvs>)
71      -> Result <(), log::kv::Error>
72    {
73      let mut value_string = serde_json::to_string (&value).unwrap();
74      let mut value_str = value_string.as_str();
75      if !value_str.contains (char::is_whitespace) {
76        // if there is no whitespace we can remove outer quotes and un-escape
77        // any internal quotes
78        value_string = value_str.replace ("\\\"", "\"");
79        value_str = if value_string.starts_with ('"') {
80          &value_string[1..value_string.len()-1]
81        } else {
82          &value_string[..]
83        };
84      }
85      self.0 += format!(" {key}={value_str}").as_str();
86      Ok(())
87    }
88  }
89  move |buf : &mut env_logger::fmt::Formatter, record : &log::Record|{
90    let mut kvv = KVVisitor::default();
91    record.key_values().visit (&mut kvv).unwrap();
92    let kvs = if kvv.0.is_empty() {
93      "".to_string()
94    } else {
95      format!(" {}", kvv.0)
96    };
97    if !config.file && !config.thread && !config.target {
98      let level_string = format!("{}:", record.level());
99      writeln!(buf, "{} {:6} {}{}", buf.timestamp(), level_string, record.args(), kvs)
100    } else {
101      let thread_string = if config.thread {
102        thread::current().name().map_or_else (
103          || format!(" {:?}", thread::current().id())
104            .replace ("ThreadId", "unnamed"),
105          |name| format!(" {name}"))
106      } else {
107        "".to_string()
108      };
109      let target_string = if config.target {
110        format!(" {}", record.target())
111      } else {
112        "".to_string()
113      };
114      let file_string = if config.file {
115        format!(" {}:{}",
116          record.file().unwrap_or ("<unknown>"), record.line().unwrap_or (0))
117      } else {
118        "".to_string()
119      };
120      writeln!(buf, "{} {:5}{}{}{}: {}{}",
121        buf.timestamp(), record.level(), thread_string, target_string, file_string,
122        record.args(), kvs)
123    }
124  }
125}
126
127impl Default for EnvLoggerFormatConfig {
128  fn default() -> Self {
129    EnvLoggerFormatConfig {
130      thread: true,
131      target: true,
132      file:   true
133    }
134  }
135}