diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 812f6ea..39f01eb 100644 --- a/layouts/json/src/lib.rs +++ b/layouts/json/src/lib.rs @@ -19,7 +19,6 @@ pub extern crate jiff; use jiff::Timestamp; -use jiff::TimestampDisplayWithOffset; use jiff::tz::TimeZone; use logforth_core::Diagnostic; use logforth_core::Error; @@ -50,14 +49,26 @@ use serde_json::Map; /// /// let json_layout = JsonLayout::default(); /// ``` -#[derive(Default, Debug, Clone)] +#[derive(Debug, Clone)] pub struct JsonLayout { - tz: Option, + timezone: TimeZone, + timestamp_format: Option String>, +} + +impl Default for JsonLayout { + fn default() -> Self { + Self { + timezone: TimeZone::system(), + timestamp_format: None, + } + } } impl JsonLayout { /// Set the timezone for timestamps. /// + /// Defaults to the system timezone. + /// /// # Examples /// /// ``` @@ -67,7 +78,29 @@ impl JsonLayout { /// let layout = JsonLayout::default().timezone(TimeZone::UTC); /// ``` pub fn timezone(mut self, tz: TimeZone) -> Self { - self.tz = Some(tz); + self.timezone = tz; + self + } + + /// Set a user-defined timestamp format function. + /// + /// Default to formatting the timestamp with offset as ISO 8601. See the example below. + /// + /// For other formatting options, refer to the [jiff::fmt::strtime] documentation. + /// + /// # Examples + /// + /// ``` + /// use jiff::Timestamp; + /// use jiff::tz::TimeZone; + /// use logforth_layout_json::JsonLayout; + /// + /// // This is equivalent to the default timestamp format. + /// let layout = JsonLayout::default() + /// .timestamp_format(|ts, tz| format!("{:.6}", ts.display_with_offset(tz.to_offset(ts)))); + /// ``` + pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self { + self.timestamp_format = Some(format); self } } @@ -87,10 +120,14 @@ impl Visitor for KvCollector<'_> { } } +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> { - #[serde(serialize_with = "serialize_timestamp")] - timestamp: TimestampDisplayWithOffset, + timestamp: String, level: &'a str, target: &'a str, file: &'a str, @@ -102,16 +139,6 @@ struct RecordLine<'a> { diags: Map, } -fn serialize_timestamp( - timestamp: &TimestampDisplayWithOffset, - serializer: S, -) -> Result -where - S: serde::Serializer, -{ - serializer.collect_str(&format_args!("{timestamp:.6}")) -} - impl Layout for JsonLayout { fn format(&self, record: &Record, diags: &[Box]) -> Result, Error> { let diagnostics = diags; @@ -119,9 +146,11 @@ impl Layout for JsonLayout { // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is // very unlikely if the system clock is correct. 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 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 }; diff --git a/layouts/text/src/lib.rs b/layouts/text/src/lib.rs index 548ed16..ad1e3d2 100644 --- a/layouts/text/src/lib.rs +++ b/layouts/text/src/lib.rs @@ -19,6 +19,8 @@ pub extern crate colored; pub extern crate jiff; +use std::fmt::Write; + use colored::Color; use colored::ColoredString; use colored::Colorize; @@ -61,11 +63,23 @@ use logforth_core::record::Record; /// /// let layout = TextLayout::default(); /// ``` -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct TextLayout { colors: LevelColor, no_color: bool, - tz: Option, + timezone: TimeZone, + timestamp_format: Option String>, +} + +impl Default for TextLayout { + fn default() -> Self { + Self { + colors: LevelColor::default(), + no_color: false, + timezone: TimeZone::system(), + timestamp_format: None, + } + } } impl TextLayout { @@ -125,6 +139,8 @@ impl TextLayout { /// Set the timezone for timestamps. /// + /// Defaults to the system timezone if not set. + /// /// # Examples /// /// ``` @@ -134,7 +150,29 @@ impl TextLayout { /// let layout = TextLayout::default().timezone(TimeZone::UTC); /// ``` pub fn timezone(mut self, tz: TimeZone) -> Self { - self.tz = Some(tz); + self.timezone = tz; + self + } + + /// Set a user-defined timestamp format function. + /// + /// Default to formatting the timestamp with offset as ISO 8601. See the example below. + /// + /// For other formatting options, refer to the [jiff::fmt::strtime] documentation. + /// + /// # Examples + /// + /// ``` + /// use jiff::Timestamp; + /// use jiff::tz::TimeZone; + /// use logforth_layout_text::TextLayout; + /// + /// // This is equivalent to the default timestamp format. + /// let layout = TextLayout::default() + /// .timestamp_format(|ts, tz| format!("{:.6}", ts.display_with_offset(tz.to_offset(ts)))); + /// ``` + pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self { + self.timestamp_format = Some(format); self } @@ -157,14 +195,21 @@ impl Visitor for KvWriter { } } +fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String { + let offset = tz.to_offset(ts); + format!("{:.6}", ts.display_with_offset(offset)) +} + impl Layout for TextLayout { fn format(&self, record: &Record, diags: &[Box]) -> Result, Error> { // SAFETY: jiff::Timestamp::try_from only fails if the time is out of range, which is // very unlikely if the system clock is correct. 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 time = if let Some(format) = self.timestamp_format { + format(ts, &self.timezone) + } else { + default_timestamp_format(ts, &self.timezone) + }; let level = self.format_record_level(record.level()); let target = record.target(); @@ -172,9 +217,12 @@ impl Layout for TextLayout { let line = record.line().unwrap_or_default(); let message = record.payload(); - let mut visitor = KvWriter { - text: format!("{time:.6} {level:>6} {target}: {file}:{line} {message}"), - }; + let mut visitor = KvWriter { text: time }; + write!( + &mut visitor.text, + " {level:>6} {target}: {file}:{line} {message}" + ) + .unwrap(); record.key_values().visit(&mut visitor)?; for d in diags { d.visit(&mut visitor)?;