Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 48 additions & 19 deletions layouts/json/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: TimeZone,
timestamp_format: Option<fn(Timestamp, &TimeZone) -> 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
///
/// ```
Expand All @@ -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
}
}
Expand All @@ -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,
Expand All @@ -102,26 +139,18 @@ struct RecordLine<'a> {
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;

// 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 };
Expand Down
66 changes: 57 additions & 9 deletions layouts/text/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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: TimeZone,
timestamp_format: Option<fn(Timestamp, &TimeZone) -> String>,
}

impl Default for TextLayout {
fn default() -> Self {
Self {
colors: LevelColor::default(),
no_color: false,
timezone: TimeZone::system(),
Copy link
Contributor Author

@tisonkun tisonkun Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope avoiding calling TimeZone::system() or TimeZone::clone() on every logging record could improve performance a bit.

timestamp_format: None,
}
}
}

impl TextLayout {
Expand Down Expand Up @@ -125,6 +139,8 @@ impl TextLayout {

/// Set the timezone for timestamps.
///
/// Defaults to the system timezone if not set.
///
/// # Examples
///
/// ```
Expand All @@ -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
}

Expand All @@ -157,24 +195,34 @@ 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<dyn Diagnostic>]) -> Result<Vec<u8>, 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();
let file = record.filename();
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)?;
Expand Down