From 19d02ad4f06743add06b3408d0cfbe113ac3347c Mon Sep 17 00:00:00 2001 From: daisuke8000 <55035595+daisuke8000@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:31:57 +0900 Subject: [PATCH] perf: reduce renderer timestamp allocations (DSK-141) --- .../src/render/default_renderer.rs | 6 +- .../src/render/ext_json_renderer.rs | 73 ++++++++++++++++++- .../rustern-core/src/render/json_renderer.rs | 28 ++++++- 3 files changed, 102 insertions(+), 5 deletions(-) diff --git a/crates/rustern-core/src/render/default_renderer.rs b/crates/rustern-core/src/render/default_renderer.rs index f9f7132..6744891 100644 --- a/crates/rustern-core/src/render/default_renderer.rs +++ b/crates/rustern-core/src/render/default_renderer.rs @@ -37,7 +37,11 @@ fn push_wall_prefix( } TimestampStyle::Rfc3339 => match zone { TimestampZone::Utc => { - buf.push_str(&dt_utc.to_rfc3339_opts(SecondsFormat::Nanos, false)); + dt_utc + .naive_utc() + .format("%Y-%m-%dT%H:%M:%S%.9f+00:00") + .write_to(buf) + .expect("timestamp"); } TimestampZone::Local => { buf.push_str( diff --git a/crates/rustern-core/src/render/ext_json_renderer.rs b/crates/rustern-core/src/render/ext_json_renderer.rs index 4a84b20..d36a66a 100644 --- a/crates/rustern-core/src/render/ext_json_renderer.rs +++ b/crates/rustern-core/src/render/ext_json_renderer.rs @@ -36,7 +36,10 @@ fn build_object(event: &LogEvent, all_namespaces: bool) -> Map { "message".into(), encode_message(&event.message, event.structured.as_ref()), ); - obj.insert("timestamp".into(), json!(event.timestamp.to_rfc3339())); + obj.insert( + "timestamp".into(), + Value::String(event.timestamp.to_rfc3339()), + ); if let Some(node) = &event.source.node { obj.insert("node".into(), json!(node)); } @@ -104,6 +107,74 @@ mod tests { } } + #[test] + fn renderers_preserve_rfc3339_timestamp_bytes() { + use crate::format_display::{TimestampStyle, TimestampZone}; + use crate::render::default_renderer::DefaultLineFormatter; + use crate::render::json_renderer::JsonLineFormatter; + use chrono::{DateTime, Local, SecondsFormat}; + + for timestamp in [ + "2024-06-15T12:00:00+00:00", + "2024-06-15T12:00:00.123+00:00", + "2024-06-15T12:00:00.123456+00:00", + "2024-06-15T12:00:00.123456789+00:00", + "2024-06-15T12:00:00.000000001+00:00", + "2016-12-31T23:59:60.123456789+00:00", + "1969-12-31T23:59:59.999999999+00:00", + "-0001-01-01T00:00:00+00:00", + "+10000-01-01T00:00:00+00:00", + ] { + let mut event = sample_event("x"); + event.timestamp = timestamp.parse::>().unwrap(); + assert_eq!(event.timestamp.to_rfc3339(), timestamp); + + assert_eq!( + JsonLineFormatter.format_line(&event), + format!( + "{{\"pod\":\"web-abc\",\"container\":\"app\",\"namespace\":\"production\",\"timestamp\":\"{timestamp}\",\"message\":\"x\",\"level\":\"info\"}}\n" + ) + ); + for pretty in [false, true] { + let output = formatter(true, pretty).format_line(&event); + let value: Value = serde_json::from_str(&output).unwrap(); + assert_eq!(value["timestamp"], timestamp); + } + + for zone in [ + TimestampZone::Utc, + TimestampZone::Local, + TimestampZone::Iana(chrono_tz::America::New_York), + TimestampZone::Iana(chrono_tz::Asia::Kolkata), + ] { + let expected = match zone { + TimestampZone::Utc => { + event.timestamp.to_rfc3339_opts(SecondsFormat::Nanos, false) + } + TimestampZone::Local => event + .timestamp + .with_timezone(&Local) + .to_rfc3339_opts(SecondsFormat::Nanos, false), + TimestampZone::Iana(tz) => event + .timestamp + .with_timezone(&tz) + .to_rfc3339_opts(SecondsFormat::Nanos, false), + }; + let default = DefaultLineFormatter { + timestamp_style: TimestampStyle::Rfc3339, + timestamp_zone: zone, + color_enabled: false, + pod_colors: false, + container_colors: false, + }; + assert_eq!( + default.format_line(&event), + format!("{expected} web-abc/app | x\n") + ); + } + } + } + #[test] fn extjson_compact_has_stern_core_keys_and_extensions() { let f = formatter(false, false); diff --git a/crates/rustern-core/src/render/json_renderer.rs b/crates/rustern-core/src/render/json_renderer.rs index eba3590..f38aebb 100644 --- a/crates/rustern-core/src/render/json_renderer.rs +++ b/crates/rustern-core/src/render/json_renderer.rs @@ -1,4 +1,5 @@ -use serde::Serialize; +use chrono::{DateTime, Utc}; +use serde::{Serialize, Serializer}; use crate::render::LineFormatter; use crate::source::LogEvent; @@ -8,12 +9,33 @@ struct JsonLine<'a> { pod: &'a str, container: &'a str, namespace: &'a str, - timestamp: String, + #[serde(serialize_with = "serialize_timestamp")] + timestamp: &'a DateTime, message: &'a str, #[serde(skip_serializing_if = "Option::is_none")] level: Option<&'a str>, } +fn serialize_timestamp( + timestamp: &DateTime, + serializer: S, +) -> Result { + struct Timestamp<'a>(&'a DateTime); + + impl std::fmt::Display for Timestamp<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Chrono's serde representation uses Z; retain to_rfc3339's +00:00. + // write_to avoids DelayedFormat's Display scratch String. + self.0 + .naive_utc() + .format("%Y-%m-%dT%H:%M:%S%.f+00:00") + .write_to(f) + } + } + + serializer.collect_str(&Timestamp(timestamp)) +} + pub struct JsonLineFormatter; impl LineFormatter for JsonLineFormatter { @@ -22,7 +44,7 @@ impl LineFormatter for JsonLineFormatter { pod: &event.source.pod, container: &event.source.container, namespace: &event.source.namespace, - timestamp: event.timestamp.to_rfc3339(), + timestamp: &event.timestamp, message: &event.message, level: event.level.as_ref().map(|l| l.as_str()), };