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