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)]
8pub struct EnvLoggerFormatConfig {
9  pub thread              : bool,
10  pub target              : bool,
11  pub file                : bool,
12  pub timestamp_precision : env_logger::TimestampPrecision
13}
14
15/// Formats log messages as a json object string
16pub fn env_logger_json_formatter (config : EnvLoggerFormatConfig)
17  -> impl Fn(&mut env_logger::fmt::Formatter, &log::Record<'_>) -> io::Result<()>
18{
19  use io::Write;
20  #[derive(Default)]
21  struct KVVisitor(pub String);
22  impl <'kvs> log::kv::VisitSource <'kvs> for KVVisitor {
23    fn visit_pair (&mut self, key : log::kv::Key <'kvs>, value : log::kv::Value <'kvs>)
24      -> Result <(), log::kv::Error>
25    {
26      self.0 += format!(",\"{}\":{}", key, serde_json::to_string (&value).unwrap())
27        .as_str();
28      Ok(())
29    }
30  }
31  move |buf : &mut env_logger::fmt::Formatter, record : &log::Record|{
32    let thread_string = if config.thread {
33      thread::current().name().map_or_else (
34        || format!(",\"thread\":\"{:?}\"", thread::current().id())
35          .replace ("ThreadId", "unnamed"),
36        |name| format!(",\"thread\":\"{name}\""))
37    } else {
38      "".to_string()
39    };
40    let target_string = if config.target {
41      format!(",\"target\":\"{}\"", record.target())
42    } else {
43      "".to_string()
44    };
45    let file_string = if config.file {
46      format!(",\"file\":\"{}:{}\"",
47        record.file().unwrap_or ("<unknown>"), record.line().unwrap_or (0))
48    } else {
49      "".to_string()
50    };
51    let mut kvv = KVVisitor::default();
52    record.key_values().visit (&mut kvv).unwrap();
53    let ts = match config.timestamp_precision {
54      env_logger::TimestampPrecision::Seconds => buf.timestamp_seconds(),
55      env_logger::TimestampPrecision::Millis  => buf.timestamp_millis(),
56      env_logger::TimestampPrecision::Micros  => buf.timestamp_micros(),
57      env_logger::TimestampPrecision::Nanos   => buf.timestamp_nanos()
58    };
59    writeln!(buf, "{{\"ts\":\"{ts}\",\"level\":\"{}\"{}{}{},\"msg\":\"{}\"{}}}",
60      record.level(), thread_string, target_string, file_string, record.args(), kvv.0
61    )
62  }
63}
64
65/// A custom `env_logger` format adding thread, target, and file information:
66/// ```text
67/// <ts> <level> <thread> <target> <file>: <msg> [ <key>=<value>]
68/// ```
69pub fn env_logger_custom_formatter (config : EnvLoggerFormatConfig)
70  -> impl Fn(&mut env_logger::fmt::Formatter, &log::Record<'_>) -> io::Result<()>
71{
72  use io::Write;
73  #[derive(Default)]
74  struct KVVisitor(pub String);
75  impl <'kvs> log::kv::VisitSource <'kvs> for KVVisitor {
76    fn visit_pair (&mut self, key : log::kv::Key <'kvs>, value : log::kv::Value <'kvs>)
77      -> Result <(), log::kv::Error>
78    {
79      let mut value_string = serde_json::to_string (&value).unwrap();
80      let mut value_str = value_string.as_str();
81      if !value_str.contains (char::is_whitespace) {
82        // if there is no whitespace we can remove outer quotes and un-escape
83        // any internal quotes
84        value_string = value_str.replace ("\\\"", "\"");
85        value_str = if value_string.starts_with ('"') {
86          &value_string[1..value_string.len()-1]
87        } else {
88          &value_string[..]
89        };
90      }
91      self.0 += format!(" {key}={value_str}").as_str();
92      Ok(())
93    }
94  }
95  move |buf : &mut env_logger::fmt::Formatter, record : &log::Record|{
96    let mut kvv = KVVisitor::default();
97    record.key_values().visit (&mut kvv).unwrap();
98    let kvs = if kvv.0.is_empty() {
99      "".to_string()
100    } else {
101      format!(" {}", kvv.0)
102    };
103    let ts = match config.timestamp_precision {
104      env_logger::TimestampPrecision::Seconds => buf.timestamp_seconds(),
105      env_logger::TimestampPrecision::Millis  => buf.timestamp_millis(),
106      env_logger::TimestampPrecision::Micros  => buf.timestamp_micros(),
107      env_logger::TimestampPrecision::Nanos   => buf.timestamp_nanos()
108    };
109    if !config.file && !config.thread && !config.target {
110      let level_string = format!("{}:", record.level());
111      writeln!(buf, "{ts} {level_string:6} {}{kvs}", record.args())
112    } else {
113      let thread_string = if config.thread {
114        thread::current().name().map_or_else (
115          || format!(" {:?}", thread::current().id())
116            .replace ("ThreadId", "unnamed"),
117          |name| format!(" {name}"))
118      } else {
119        "".to_string()
120      };
121      let target_string = if config.target {
122        format!(" {}", record.target())
123      } else {
124        "".to_string()
125      };
126      let file_string = if config.file {
127        format!(" {}:{}",
128          record.file().unwrap_or ("<unknown>"), record.line().unwrap_or (0))
129      } else {
130        "".to_string()
131      };
132      writeln!(buf, "{ts} {:5}{}{}{}: {}{kvs}",
133        record.level(), thread_string, target_string, file_string, record.args())
134    }
135  }
136}
137
138impl EnvLoggerFormatConfig {
139  pub const fn thread (&mut self, thread : bool) -> &mut Self {
140    self.thread = thread;
141    self
142  }
143
144  pub const fn target (&mut self, target : bool) -> &mut Self {
145    self.target = target;
146    self
147  }
148
149  pub const fn file (&mut self, file : bool) -> &mut Self {
150    self.file = file;
151    self
152  }
153
154  pub const fn timestamp_precision (&mut self, precision : env_logger::TimestampPrecision)
155    -> &mut Self
156  {
157    self.timestamp_precision = precision;
158    self
159  }
160
161  pub const fn build (&mut self) -> Self {
162    *self
163  }
164}
165
166impl Default for EnvLoggerFormatConfig {
167  fn default() -> Self {
168    EnvLoggerFormatConfig {
169      thread: true,
170      target: true,
171      file:   true,
172      timestamp_precision: env_logger::TimestampPrecision::Seconds
173    }
174  }
175}