#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
pub extern crate jiff;
use jiff::Timestamp;
use jiff::tz::TimeZone;
use logforth_core::Diagnostic;
use logforth_core::Error;
use logforth_core::kv::KeyView;
use logforth_core::kv::ValueView;
use logforth_core::kv::Visitor;
use logforth_core::layout::Layout;
use logforth_core::record::Record;
use serde::Serialize;
use serde_json::Map;
#[derive(Debug, Clone)]
pub struct JsonLayout {
timezone: TimeZone,
timestamp_format: Option<fn(Timestamp, &TimeZone) -> String>,
}
impl Default for JsonLayout {
fn default() -> Self {
Self {
timezone: TimeZone::system(),
timestamp_format: None,
}
}
}
impl JsonLayout {
pub fn timezone(mut self, tz: TimeZone) -> Self {
self.timezone = tz;
self
}
pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self {
self.timestamp_format = Some(format);
self
}
}
struct KvCollector<'a> {
kvs: &'a mut Map<String, serde_json::Value>,
}
impl Visitor for KvCollector<'_> {
fn visit(&mut self, key: KeyView, value: ValueView) -> 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(())
}
}
fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String {
let offset = tz.to_offset(ts);
format!("{:.6}", ts.display_with_offset(offset))
}
#[derive(Debug, Clone, Serialize)]
struct RecordLine<'a> {
timestamp: String,
level: &'a str,
target: &'a str,
file: &'a str,
line: u32,
message: std::fmt::Arguments<'a>,
#[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>,
}
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 timestamp = if let Some(format) = self.timestamp_format {
format(ts, &self.timezone)
} else {
default_timestamp_format(ts, &self.timezone)
};
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())
}
}