#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
pub extern crate jiff;
use std::borrow::Cow;
use jiff::Timestamp;
use jiff::tz::TimeZone;
use logforth_core::Diagnostic;
use logforth_core::Error;
use logforth_core::kv::Key;
use logforth_core::kv::KeyView;
use logforth_core::kv::Value;
use logforth_core::kv::ValueView;
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: KeyView, value: ValueView) -> 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 = if let Some(msg) = record.payload_static() {
Cow::Borrowed(msg)
} else {
Cow::Owned(record.payload().to_string())
};
let mut visitor = KvFormatter {
text: format!("timestamp={time:.6}"),
};
visitor.visit(
Key::new("level").view(),
Value::static_str(level.name()).view(),
)?;
visitor.visit(Key::new("module").view(), Value::str(target).view())?;
visitor.visit(
Key::new("position").view(),
Value::display(&format_args!("{file}:{line}")).view(),
)?;
visitor.visit(
Key::new("message").view(),
Value::str(message.as_ref()).view(),
)?;
record.key_values().visit(&mut visitor)?;
for d in diags {
d.visit(&mut visitor)?;
}
Ok(visitor.text.into_bytes())
}
}