From 93909d03f12ba1e1d5940ce200646ef9c7a516fa Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 18 Dec 2025 20:39:34 +0800 Subject: [PATCH 1/2] feat: support customizable timestamp format function Signed-off-by: tison --- layouts/json/src/lib.rs | 58 ++++++++++++++++++++++++++++------------- layouts/text/src/lib.rs | 55 ++++++++++++++++++++++++++++++++------ 2 files changed, 87 insertions(+), 26 deletions(-) diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 812f6ea..0d4aeda 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; @@ -52,12 +51,15 @@ use serde_json::Map; /// ``` #[derive(Default, Debug, Clone)] pub struct JsonLayout { - tz: Option, + timezone: Option, + timestamp_format: Option String>, } impl JsonLayout { /// Set the timezone for timestamps. /// + /// Defaults to the system timezone if not set. + /// /// # Examples /// /// ``` @@ -67,7 +69,30 @@ impl JsonLayout { /// let layout = JsonLayout::default().timezone(TimeZone::UTC); /// ``` pub fn timezone(mut self, tz: TimeZone) -> Self { - self.tz = Some(tz); + self.timezone = Some(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 +112,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 +131,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 +138,12 @@ 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 tz = self.timezone.clone().unwrap_or_else(TimeZone::system); + let timestamp = if let Some(format) = self.timestamp_format { + format(ts, tz) + } else { + default_timestamp_format(ts, tz) + }; 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..de1e408 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; @@ -65,7 +67,8 @@ use logforth_core::record::Record; pub struct TextLayout { colors: LevelColor, no_color: bool, - tz: Option, + timezone: Option, + timestamp_format: Option String>, } impl TextLayout { @@ -125,6 +128,8 @@ impl TextLayout { /// Set the timezone for timestamps. /// + /// Defaults to the system timezone if not set. + /// /// # Examples /// /// ``` @@ -134,7 +139,30 @@ impl TextLayout { /// let layout = TextLayout::default().timezone(TimeZone::UTC); /// ``` pub fn timezone(mut self, tz: TimeZone) -> Self { - self.tz = Some(tz); + self.timezone = Some(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 +185,22 @@ 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 tz = self.timezone.clone().unwrap_or_else(TimeZone::system); + let time = if let Some(format) = self.timestamp_format { + format(ts, tz) + } else { + default_timestamp_format(ts, tz) + }; let level = self.format_record_level(record.level()); let target = record.target(); @@ -172,9 +208,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)?; From dba7d5f977b6402ed1a32e8285b47c43c9cca9a3 Mon Sep 17 00:00:00 2001 From: tison Date: Thu, 18 Dec 2025 20:45:37 +0800 Subject: [PATCH 2/2] avoid create/clone timezone everytime Signed-off-by: tison --- layouts/json/src/lib.rs | 33 ++++++++++++++++++++------------- layouts/text/src/lib.rs | 33 +++++++++++++++++++++------------ 2 files changed, 41 insertions(+), 25 deletions(-) diff --git a/layouts/json/src/lib.rs b/layouts/json/src/lib.rs index 0d4aeda..39f01eb 100644 --- a/layouts/json/src/lib.rs +++ b/layouts/json/src/lib.rs @@ -49,16 +49,25 @@ use serde_json::Map; /// /// let json_layout = JsonLayout::default(); /// ``` -#[derive(Default, Debug, Clone)] +#[derive(Debug, Clone)] pub struct JsonLayout { - timezone: Option, - timestamp_format: Option String>, + 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 if not set. + /// Defaults to the system timezone. /// /// # Examples /// @@ -69,7 +78,7 @@ impl JsonLayout { /// let layout = JsonLayout::default().timezone(TimeZone::UTC); /// ``` pub fn timezone(mut self, tz: TimeZone) -> Self { - self.timezone = Some(tz); + self.timezone = tz; self } @@ -87,11 +96,10 @@ impl JsonLayout { /// 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))) - /// }); + /// 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 { + pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self { self.timestamp_format = Some(format); self } @@ -112,7 +120,7 @@ impl Visitor for KvCollector<'_> { } } -fn default_timestamp_format(ts: Timestamp, tz: TimeZone) -> String { +fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String { let offset = tz.to_offset(ts); format!("{:.6}", ts.display_with_offset(offset)) } @@ -138,11 +146,10 @@ 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.timezone.clone().unwrap_or_else(TimeZone::system); let timestamp = if let Some(format) = self.timestamp_format { - format(ts, tz) + format(ts, &self.timezone) } else { - default_timestamp_format(ts, tz) + default_timestamp_format(ts, &self.timezone) }; let mut kvs = Map::new(); diff --git a/layouts/text/src/lib.rs b/layouts/text/src/lib.rs index de1e408..ad1e3d2 100644 --- a/layouts/text/src/lib.rs +++ b/layouts/text/src/lib.rs @@ -63,12 +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, - timezone: Option, - timestamp_format: Option String>, + 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 { @@ -139,7 +150,7 @@ impl TextLayout { /// let layout = TextLayout::default().timezone(TimeZone::UTC); /// ``` pub fn timezone(mut self, tz: TimeZone) -> Self { - self.timezone = Some(tz); + self.timezone = tz; self } @@ -157,11 +168,10 @@ impl TextLayout { /// 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))) - /// }); + /// 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 { + pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self { self.timestamp_format = Some(format); self } @@ -185,7 +195,7 @@ impl Visitor for KvWriter { } } -fn default_timestamp_format(ts: Timestamp, tz: TimeZone) -> String { +fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String { let offset = tz.to_offset(ts); format!("{:.6}", ts.display_with_offset(offset)) } @@ -195,11 +205,10 @@ impl Layout for TextLayout { // 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.timezone.clone().unwrap_or_else(TimeZone::system); let time = if let Some(format) = self.timestamp_format { - format(ts, tz) + format(ts, &self.timezone) } else { - default_timestamp_format(ts, tz) + default_timestamp_format(ts, &self.timezone) }; let level = self.format_record_level(record.level());