#![cfg_attr(docsrs, feature(doc_cfg))]
use jiff::Timestamp;
use jiff::tz::TimeZone;
use logforth_core::Diagnostic;
use logforth_core::Error;
use logforth_core::kv::Key;
use logforth_core::kv::Value;
use logforth_core::kv::Visitor;
use logforth_core::layout::Layout;
use logforth_core::record::Record;
#[derive(Default, Debug, Clone)]
pub struct LogfmtLayout {
tz: Option<TimeZone>,
}
impl LogfmtLayout {
pub fn timezone(mut self, tz: TimeZone) -> Self {
self.tz = Some(tz);
self
}
}
struct KvFormatter {
text: String,
}
impl Visitor for KvFormatter {
fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
use std::fmt::Write;
let key = key.as_str();
let value = value.to_string();
let value = value.as_str();
if key.contains([' ', '=', '"']) {
return Err(Error::new(format!("key contains special chars: {key}")));
}
if value.contains([' ', '=', '"']) {
write!(&mut self.text, " {key}=\"{}\"", value.escape_debug()).unwrap();
} else {
write!(&mut self.text, " {key}={value}").unwrap();
}
Ok(())
}
}
impl Layout for LogfmtLayout {
fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
let ts = Timestamp::try_from(record.time()).unwrap();
let tz = self.tz.clone().unwrap_or_else(TimeZone::system);
let offset = tz.to_offset(ts);
let time = ts.display_with_offset(offset);
let level = record.level();
let target = record.target();
let file = record.filename();
let line = record.line().unwrap_or_default();
let message = record.payload();
let mut visitor = KvFormatter {
text: format!("timestamp={time:.6}"),
};
visitor.visit(Key::new("level"), level.name().into())?;
visitor.visit(Key::new("module"), target.into())?;
visitor.visit(
Key::new("position"),
Value::from_display(&format_args!("{file}:{line}")),
)?;
visitor.visit(Key::new("message"), Value::from_str(message))?;
record.key_values().visit(&mut visitor)?;
for d in diags {
d.visit(&mut visitor)?;
}
Ok(visitor.text.into_bytes())
}
}