feat(zlog)!: JSON logging support

This commit is contained in:
lily 2025-04-19 13:44:43 -04:00 committed by BitSyndicate
parent 01c3699d86
commit e347fe6d54
Signed by: bitsyndicate
GPG key ID: 443E4198D6BBA6DE
5 changed files with 162 additions and 12 deletions

View file

@ -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"]

View file

@ -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<LogLevel>,
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
}
}
}

View file

@ -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<String, Value>,
}
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::<Utc>::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()
}