#![doc(html_root_url = "https://docs.rs/structured-logger/latest")]
#![allow(clippy::needless_doctest_main)]
use json::new_writer;
use log::{kv::*, Level, LevelFilter, Metadata, Record, SetLoggerError};
use std::{
collections::BTreeMap,
env, io,
time::{SystemTime, UNIX_EPOCH},
};
pub trait Writer {
fn write_log(&self, value: &BTreeMap<Key, Value>) -> Result<(), io::Error>;
}
pub mod async_json;
pub mod json;
pub struct Builder {
filter: LevelFilter,
default_writer: Box<dyn Writer>,
writers: Vec<(Target, Box<dyn Writer>)>,
with_msg: bool,
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
impl Builder {
pub fn new() -> Self {
Builder {
filter: get_env_level(),
default_writer: new_writer(io::stderr()),
writers: Vec::new(),
with_msg: false,
}
}
pub fn with_level(level: &str) -> Self {
Builder {
filter: level.parse().unwrap_or(LevelFilter::Info),
default_writer: new_writer(io::stderr()),
writers: Vec::new(),
with_msg: false,
}
}
pub fn with_default_writer(self, writer: Box<dyn Writer>) -> Self {
Builder {
filter: self.filter,
default_writer: writer,
writers: self.writers,
with_msg: false,
}
}
pub fn with_target_writer(self, targets: &str, writer: Box<dyn Writer>) -> Self {
let mut cfg = Builder {
filter: self.filter,
default_writer: self.default_writer,
writers: self.writers,
with_msg: false,
};
cfg.writers.push((Target::from(targets), writer));
cfg
}
pub fn with_msg_field(mut self) -> Self {
self.with_msg = true;
self
}
pub fn build(self) -> impl log::Log {
Logger {
filter: self.filter,
default_writer: self.default_writer,
writers: self
.writers
.into_iter()
.map(|(t, w)| (InnerTarget::from(t), w))
.collect(),
message_field: if self.with_msg {
"msg".to_string()
} else {
"message".to_string()
},
}
}
pub fn init(self) {
self.try_init()
.unwrap_or_else(|err| panic!("failed to initialize the logger: {}", err));
}
pub fn try_init(self) -> Result<(), SetLoggerError> {
let filter = self.filter;
let logger = Box::new(self.build());
log::set_boxed_logger(logger)?;
log::set_max_level(filter);
#[cfg(feature = "log-panic")]
std::panic::set_hook(Box::new(log_panic));
Ok(())
}
}
pub fn init() {
Builder::new().init();
}
#[inline]
pub fn unix_ms() -> u64 {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time before Unix epoch");
ts.as_millis() as u64
}
pub fn get_env_level() -> LevelFilter {
for var in &["LOG", "LOG_LEVEL", "RUST_LOG"] {
if let Ok(level) = env::var(var) {
if let Ok(level) = level.parse() {
return level;
}
}
}
if env::var("TRACE").is_ok() {
LevelFilter::Trace
} else if env::var("DEBUG").is_ok() {
LevelFilter::Debug
} else {
LevelFilter::Info
}
}
struct Logger {
filter: LevelFilter,
default_writer: Box<dyn Writer>,
writers: Box<[(InnerTarget, Box<dyn Writer>)]>,
message_field: String,
}
impl Logger {
fn get_writer(&self, target: &str) -> &dyn Writer {
for t in self.writers.iter() {
if t.0.test(target) {
return t.1.as_ref();
}
}
self.default_writer.as_ref()
}
fn try_log(&self, record: &Record) -> Result<(), io::Error> {
let kvs = record.key_values();
let mut visitor = KeyValueVisitor(BTreeMap::new());
let _ = kvs.visit(&mut visitor);
visitor
.0
.insert(Key::from("target"), Value::from(record.target()));
let args = record.args();
visitor.0.insert(
Key::from(self.message_field.as_str()),
if let Some(msg) = args.as_str() {
Value::from(msg)
} else {
Value::from_display(args)
},
);
let level = record.level();
visitor
.0
.insert(Key::from("level"), Value::from(level.as_str()));
if level <= Level::Warn {
if let Some(val) = record.module_path() {
visitor.0.insert(Key::from("module"), Value::from(val));
}
if let Some(val) = record.file() {
visitor.0.insert(Key::from("file"), Value::from(val));
}
if let Some(val) = record.line() {
visitor.0.insert(Key::from("line"), Value::from(val));
}
}
visitor
.0
.insert(Key::from("timestamp"), Value::from(unix_ms()));
self.get_writer(record.target()).write_log(&visitor.0)?;
Ok(())
}
}
unsafe impl Sync for Logger {}
unsafe impl Send for Logger {}
impl log::Log for Logger {
fn enabled(&self, metadata: &Metadata) -> bool {
self.filter >= metadata.level()
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
if let Err(err) = self.try_log(record) {
log_failure(format!("Logger failed to log: {}", err).as_str());
}
}
}
fn flush(&self) {}
}
struct Target {
all: bool,
prefix: Vec<String>,
items: Vec<String>,
}
impl Target {
fn from(targets: &str) -> Self {
let mut target = Target {
all: false,
prefix: Vec::new(),
items: Vec::new(),
};
for t in targets.split(',') {
let t = t.trim();
if t == "*" {
target.all = true;
break;
} else if t.ends_with('*') {
target.prefix.push(t.trim_end_matches('*').to_string());
} else {
target.items.push(t.to_string());
}
}
target
}
}
struct InnerTarget {
all: bool,
prefix: Box<[Box<str>]>,
items: Box<[Box<str>]>,
}
impl InnerTarget {
fn from(t: Target) -> Self {
InnerTarget {
all: t.all,
prefix: t.prefix.into_iter().map(|s| s.into_boxed_str()).collect(),
items: t.items.into_iter().map(|s| s.into_boxed_str()).collect(),
}
}
fn test(&self, target: &str) -> bool {
if self.all {
return true;
}
if self.items.iter().any(|i| i.as_ref() == target) {
return true;
}
if self.prefix.iter().any(|p| target.starts_with(p.as_ref())) {
return true;
}
false
}
}
struct KeyValueVisitor<'kvs>(BTreeMap<Key<'kvs>, Value<'kvs>>);
impl<'kvs> VisitSource<'kvs> for KeyValueVisitor<'kvs> {
#[inline]
fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), Error> {
self.0.insert(key, value);
Ok(())
}
}
pub fn log_failure(msg: &str) {
match serde_json::to_string(msg) {
Ok(msg) => {
eprintln!(
"{{\"level\":\"ERROR\",\"message\":{},\"target\":\"structured_logger\",\"timestamp\":{}}}",
&msg,
unix_ms()
);
}
Err(err) => {
panic!("log_failure serialize error: {}", err)
}
}
}
#[cfg(feature = "log-panic")]
fn log_panic(info: &std::panic::PanicHookInfo<'_>) {
use std::backtrace::Backtrace;
use std::thread;
let mut record = log::Record::builder();
let thread = thread::current();
let thread_name = thread.name().unwrap_or("unnamed");
let backtrace = Backtrace::force_capture();
let key_values = [
("backtrace", Value::from_display(&backtrace)),
("thread_name", Value::from(thread_name)),
];
let key_values = key_values.as_slice();
let _ = record
.level(log::Level::Error)
.target("panic")
.key_values(&key_values);
if let Some(location) = info.location() {
let _ = record
.file(Some(location.file()))
.line(Some(location.line()));
};
log::logger().log(
&record
.args(format_args!("thread '{thread_name}' {info}"))
.build(),
);
}
#[cfg(test)]
mod tests {
use super::*;
use gag::BufferRedirect;
use serde_json::{de, value};
use std::io::Read;
#[test]
fn unix_ms_works() {
let now = unix_ms();
assert!(now > 1670123456789_u64);
}
#[test]
fn get_env_level_works() {
assert_eq!(Level::Info, get_env_level());
env::set_var("LOG", "error");
assert_eq!(Level::Error, get_env_level());
env::remove_var("LOG");
env::set_var("LOG_LEVEL", "Debug");
assert_eq!(Level::Debug, get_env_level());
env::remove_var("LOG_LEVEL");
env::set_var("RUST_LOG", "WARN");
assert_eq!(Level::Warn, get_env_level());
env::remove_var("RUST_LOG");
env::set_var("TRACE", "");
assert_eq!(Level::Trace, get_env_level());
env::remove_var("TRACE");
env::set_var("DEBUG", "");
assert_eq!(Level::Debug, get_env_level());
env::remove_var("DEBUG");
}
#[test]
fn target_works() {
let target = InnerTarget::from(Target::from("*"));
assert!(target.test(""));
assert!(target.test("api"));
assert!(target.test("hello"));
let target = InnerTarget::from(Target::from("api*, file,db"));
assert!(!target.test(""));
assert!(!target.test("apx"));
assert!(!target.test("err"));
assert!(!target.test("dbx"));
assert!(target.test("api"));
assert!(target.test("apiinfo"));
assert!(target.test("apierr"));
assert!(target.test("file"));
assert!(target.test("db"));
let target = InnerTarget::from(Target::from("api*, file, *"));
assert!(target.test(""));
assert!(target.test("apx"));
assert!(target.test("err"));
assert!(target.test("api"));
assert!(target.test("apiinfo"));
assert!(target.test("apierr"));
assert!(target.test("error"));
}
#[test]
fn log_failure_works() {
let cases: Vec<&str> = vec!["", "\"", "hello", "\"hello >", "hello\n", "hello\r"];
for case in cases {
let buf = BufferRedirect::stderr().unwrap();
log_failure(case);
let mut msg: String = String::new();
buf.into_inner().read_to_string(&mut msg).unwrap();
let msg = msg.as_str();
assert_eq!('\n', msg.chars().last().unwrap());
let res = de::from_str::<BTreeMap<String, value::Value>>(msg);
assert!(res.is_ok());
let res = res.unwrap();
assert_eq!("ERROR", res.get("level").unwrap());
assert_eq!(case, res.get("message").unwrap());
assert_eq!("structured_logger", res.get("target").unwrap());
assert!(unix_ms() - 999 <= res.get("timestamp").unwrap().as_u64().unwrap());
}
}
}