#![cfg_attr(docsrs, feature(doc_cfg))]
use jiff::Timestamp;
use jiff::TimestampDisplayWithOffset;
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;
use serde::Serialize;
use serde_json::Map;
#[derive(Default, Debug, Clone)]
pub struct JsonLayout {
tz: Option<TimeZone>,
}
impl JsonLayout {
pub fn timezone(mut self, tz: TimeZone) -> Self {
self.tz = Some(tz);
self
}
}
struct KvCollector<'a> {
kvs: &'a mut Map<String, serde_json::Value>,
}
impl Visitor for KvCollector<'_> {
fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
let key = key.to_string();
match serde_json::to_value(&value) {
Ok(value) => self.kvs.insert(key, value),
Err(_) => self.kvs.insert(key, value.to_string().into()),
};
Ok(())
}
}
#[derive(Debug, Clone, Serialize)]
struct RecordLine<'a> {
#[serde(serialize_with = "serialize_timestamp")]
timestamp: TimestampDisplayWithOffset,
level: &'a str,
target: &'a str,
file: &'a str,
line: u32,
message: &'a str,
#[serde(skip_serializing_if = "Map::is_empty")]
kvs: Map<String, serde_json::Value>,
#[serde(skip_serializing_if = "Map::is_empty")]
diags: Map<String, serde_json::Value>,
}
fn serialize_timestamp<S>(
timestamp: &TimestampDisplayWithOffset,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(&format_args!("{timestamp:.6}"))
}
impl Layout for JsonLayout {
fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
let diagnostics = diags;
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 timestamp = ts.display_with_offset(offset);
let mut kvs = Map::new();
let mut kvs_visitor = KvCollector { kvs: &mut kvs };
record.key_values().visit(&mut kvs_visitor)?;
let mut diags = Map::new();
let mut diags_visitor = KvCollector { kvs: &mut diags };
for d in diagnostics {
d.visit(&mut diags_visitor)?;
}
let record_line = RecordLine {
timestamp,
level: record.level().name(),
target: record.target(),
file: record.file().unwrap_or_default(),
line: record.line().unwrap_or_default(),
message: record.payload(),
kvs,
diags,
};
Ok(serde_json::to_vec(&record_line).unwrap())
}
}