From 0d98f633a0b65714e565630dba81532c40267e23 Mon Sep 17 00:00:00 2001 From: lily Date: Sat, 19 Apr 2025 13:44:43 -0400 Subject: [PATCH] feat(zlog)!: JSON logging support --- Cargo.lock | 53 +++++++++++++++++++++++++++ src/main.rs | 9 +++-- subcrates/zlog/Cargo.toml | 8 +++++ subcrates/zlog/src/config.rs | 35 ++++++++++++++++++ subcrates/zlog/src/lib.rs | 69 ++++++++++++++++++++++++++++++------ 5 files changed, 162 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4dd8a95..8cc3820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,6 +85,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -497,6 +503,20 @@ dependencies = [ "num-traits", ] +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "codespan-reporting" version = "0.12.0" @@ -990,6 +1010,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "image" version = "0.25.6" @@ -3268,6 +3312,12 @@ dependencies = [ "syn", ] +[[package]] +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + [[package]] name = "windows-result" version = "0.2.0" @@ -3694,7 +3744,10 @@ dependencies = [ name = "zlog" version = "0.1.0" dependencies = [ + "chrono", "pretty_assertions", + "serde", + "serde_json", "tracing", "tracing-subscriber", ] diff --git a/src/main.rs b/src/main.rs index 54e5339..771109a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -485,8 +485,13 @@ impl ApplicationHandler for App<'_> { } pub fn main() -> Result<(), terminator::Terminator> { let config = LoggerConfig::default() - .colored_stdout(true) - .log_to_stdout(true) + .colored_stdout(false) + .log_to_stdout(false) + .log_use_json(true) + .log_json_show_timestamp(true) + .log_json_show_level(true) + .log_json_show_message(true) + .log_json_show_additional_fields(true) .file_include_time(true) .log_to_file(true) .level(LogLevel::Info) diff --git a/subcrates/zlog/Cargo.toml b/subcrates/zlog/Cargo.toml index f55e6de..b8e90fc 100644 --- a/subcrates/zlog/Cargo.toml +++ b/subcrates/zlog/Cargo.toml @@ -6,6 +6,14 @@ edition = "2024" [dependencies] tracing = "0.1.41" tracing-subscriber = "0.3.19" +serde = { version = "1.0.219", optional = true } +serde_json = { version = "1.0.140", optional = true } +chrono = { version = "0.4.40", optional = true } [dev-dependencies] pretty_assertions = "1.4.1" + +[features] +default = ["json"] +json = ["dep:serde_json", "dep:chrono", "serde"] +serde = ["dep:serde"] \ No newline at end of file diff --git a/subcrates/zlog/src/config.rs b/subcrates/zlog/src/config.rs index bb3fb76..dc50fbe 100644 --- a/subcrates/zlog/src/config.rs +++ b/subcrates/zlog/src/config.rs @@ -12,6 +12,11 @@ pub struct LoggerConfig { pub(crate) stdout_include_time: bool, pub(crate) file_include_time: bool, pub(crate) crate_max_level: Option, + pub(crate) log_use_json: bool, + pub(crate) log_json_show_timestamp: bool, + pub(crate) log_json_show_level: bool, + pub(crate) log_json_show_message: bool, + pub(crate) log_json_show_additional_fields: bool, } impl LoggerConfig { @@ -49,6 +54,31 @@ impl LoggerConfig { self.file_include_time = i; self } + + pub fn log_use_json(mut self, i: bool) -> Self { + self.log_use_json = i; + self + } + + pub fn log_json_show_timestamp(mut self, i: bool) -> Self { + self.log_json_show_timestamp = i; + self + } + + pub fn log_json_show_level(mut self, i: bool) -> Self { + self.log_json_show_level = i; + self + } + + pub fn log_json_show_message(mut self, i: bool) -> Self { + self.log_json_show_message = i; + self + } + + pub fn log_json_show_additional_fields(mut self, i: bool) -> Self { + self.log_json_show_additional_fields = i; + self + } } impl Default for LoggerConfig { @@ -62,6 +92,11 @@ impl Default for LoggerConfig { stdout_color: true, stdout_include_time: false, file_include_time: false, + log_use_json: false, + log_json_show_timestamp: false, + log_json_show_level: false, + log_json_show_message: false, + log_json_show_additional_fields: false } } } diff --git a/subcrates/zlog/src/lib.rs b/subcrates/zlog/src/lib.rs index c069b34..e6b64a4 100644 --- a/subcrates/zlog/src/lib.rs +++ b/subcrates/zlog/src/lib.rs @@ -25,6 +25,15 @@ use tracing_subscriber::{ util::SubscriberInitExt, }; +#[cfg(feature = "json")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "json")] +use serde_json::Value; + +#[cfg(feature = "json")] +use chrono::{DateTime, Utc}; + #[derive(Debug, Clone, PartialEq, Eq)] enum LogEvent { Log(LogEntry), @@ -36,6 +45,8 @@ pub struct LogEntry { timestamp: SystemTime, level: Level, message: String, + #[cfg(feature = "json")] + additional_fields: serde_json::Map, } impl PartialOrd for LogEntry { @@ -91,6 +102,8 @@ where let metadata = event.metadata(); let level = *metadata.level(); let timestamp = SystemTime::now(); + #[cfg(feature = "json")] + let additional_fields = serde_json::Map::new(); let mut message = String::new(); let mut visitor = LogVisitor::new(&mut message); event.record(&mut visitor); @@ -99,6 +112,8 @@ where timestamp, level, message, + #[cfg(feature = "json")] + additional_fields }); if let LogEvent::Log(ref entry) = log_entry { @@ -189,20 +204,19 @@ impl Logger { let mut senders = Vec::new(); let mut handles = Vec::new(); - if config.log_to_stdout { + if config.log_to_stdout || config.log_use_json { let (tx, rx) = mpsc::channel(); senders.push(tx); let config_clone = config.clone(); let handle = thread::spawn(move || { for msg in rx { match msg { - LogEvent::Log(entry) => { + LogEvent::Log(mut entry) => { println!( "{}", format_entry( - &entry, - config_clone.stdout_color, - config_clone.stdout_include_time + &mut entry, + &config_clone ) ); } @@ -216,8 +230,8 @@ impl Logger { if config.log_to_file { let (tx, rx) = mpsc::channel(); senders.push(tx); + let config_clone = config.clone(); let path = config.log_file_path.clone(); - let include_time = config.file_include_time; let handle = thread::spawn(move || { let file = OpenOptions::new() .append(true) @@ -227,8 +241,8 @@ impl Logger { let mut writer = BufWriter::new(file); for msg in rx { match msg { - LogEvent::Log(entry) => { - let line = format_entry(&entry, false, include_time); + LogEvent::Log(mut entry) => { + let line = format_entry(&mut entry, &config_clone); writeln!(writer, "{}", line).expect("Failed to write to log file"); writer.flush().expect("Failed to flush log file"); } @@ -280,8 +294,20 @@ impl Drop for Logger { } } -fn format_entry(entry: &LogEntry, use_color: bool, _: bool) -> String { - let lvl = if use_color { +fn format_entry(entry: &mut LogEntry, log_config: &LoggerConfig) -> String { + if log_config.log_use_json { + return format_entry_json(entry, log_config); + } + + if log_config.log_to_stdout || log_config.log_to_file { + return format_entry_string(entry, log_config); + } else { + return String::new(); + } +} + +fn format_entry_string(entry: &LogEntry, log_config: &LoggerConfig) -> String { + let lvl = if log_config.stdout_color { match entry.level { Level::ERROR => "\x1b[31mERROR\x1b[0m", Level::WARN => "\x1b[33mWARN\x1b[0m", @@ -295,3 +321,26 @@ fn format_entry(entry: &LogEntry, use_color: bool, _: bool) -> String { format!("{} {}", lvl, entry.message) } + +/// Formats the log entry as a json object ([`serde_json`]) and returns it as a [`String`] +fn format_entry_json(entry: &mut LogEntry, log_config: &LoggerConfig) -> String { + let mut json_object = serde_json::Map::new(); + + if log_config.log_json_show_timestamp { + json_object.insert("timestamp".to_string(), Value::String(DateTime::::from(entry.timestamp).to_rfc3339())); + } + + if log_config.log_json_show_level { + json_object.insert("level".to_string(), Value::String(entry.level.to_string())); + } + + if log_config.log_json_show_message { + json_object.insert("message".to_string(), Value::String(entry.message.to_string())); + } + + if log_config.log_json_show_additional_fields { + json_object.append(&mut entry.additional_fields); + } + + serde_json::to_string(&json_object).unwrap() +} \ No newline at end of file