diff --git a/spdlog/Cargo.toml b/spdlog/Cargo.toml index 7458628..f698a1a 100644 --- a/spdlog/Cargo.toml +++ b/spdlog/Cargo.toml @@ -37,7 +37,7 @@ native = [] libsystemd = ["libsystemd-sys"] multi-thread = ["crossbeam"] runtime-pattern = ["spdlog-internal"] -serde_json = ["serde", "dep:serde_json"] +serde_json = ["serde", "dep:serde_json", "value-bag/serde1"] [dependencies] arc-swap = "1.5.1" diff --git a/spdlog/src/formatter/full_formatter.rs b/spdlog/src/formatter/full_formatter.rs index 97dc37a..125d81b 100644 --- a/spdlog/src/formatter/full_formatter.rs +++ b/spdlog/src/formatter/full_formatter.rs @@ -62,14 +62,18 @@ impl FullFormatter { } } - fmt_with_time(ctx, record, |mut time: TimeDate| { - dest.write_str("[")?; - dest.write_str(time.full_second_str())?; - dest.write_str(".")?; - write!(dest, "{:03}", time.millisecond())?; - dest.write_str("] [")?; - Ok(()) - })?; + fmt_with_time( + ctx, + record, + |mut time: TimeDate| -> Result<(), fmt::Error> { + dest.write_str("[")?; + dest.write_str(time.full_second_str())?; + dest.write_str(".")?; + write!(dest, "{:03}", time.millisecond())?; + dest.write_str("] [")?; + Ok(()) + }, + )?; if let Some(logger_name) = record.logger_name() { dest.write_str(logger_name)?; diff --git a/spdlog/src/formatter/json_formatter.rs b/spdlog/src/formatter/json_formatter.rs index 82238f0..5a94dc1 100644 --- a/spdlog/src/formatter/json_formatter.rs +++ b/spdlog/src/formatter/json_formatter.rs @@ -5,7 +5,10 @@ use std::{ }; use cfg_if::cfg_if; -use serde::{ser::SerializeStruct, Serialize}; +use serde::{ + ser::{SerializeMap, SerializeStruct}, + Serialize, Serializer, +}; use crate::{ formatter::{Formatter, FormatterContext}, @@ -21,7 +24,7 @@ struct JsonRecord<'a>(&'a Record<'a>); impl Serialize for JsonRecord<'_> { fn serialize(&self, serializer: S) -> Result where - S: serde::Serializer, + S: Serializer, { let fields_len = 4 + opt_to_num(self.0.logger_name()) + opt_to_num(self.0.source_location()); @@ -40,6 +43,11 @@ impl Serialize for JsonRecord<'_> { .expect("invalid timestamp"), )?; record.serialize_field("payload", self.0.payload())?; + + if !self.0.key_values().is_empty() { + record.serialize_field("kv", &JsonKV(&self.0))?; + } + if let Some(logger_name) = self.0.logger_name() { record.serialize_field("logger", logger_name)?; } @@ -52,6 +60,22 @@ impl Serialize for JsonRecord<'_> { } } +struct JsonKV<'a>(&'a Record<'a>); + +impl<'a> Serialize for JsonKV<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let kv = self.0.key_values(); + let mut map = serializer.serialize_map(Some(kv.len()))?; + for (key, value) in kv { + map.serialize_entry(key.as_str(), &value)?; + } + map.end() + } +} + impl<'a> From<&'a Record<'a>> for JsonRecord<'a> { fn from(value: &'a Record<'a>) -> Self { JsonRecord(value) @@ -96,6 +120,7 @@ impl From for crate::Error { /// | `level` | String | The level of the log. Same as the return of [`Level::as_str`]. | /// | `timestamp` | Integer(u64) | The timestamp when the log was generated, in milliseconds since January 1, 1970 00:00:00 UTC. | /// | `payload` | String | The contents of the log. | +/// | `kv` | Object/Null | The key-values of the log. Null if kv is not specified. | /// | `logger` | String/Null | The name of the logger. Null if the logger has no name. | /// | `tid` | Integer(u64) | The thread ID when the log was generated. | /// | `source` | Object/Null | The source location of the log. See [`SourceLocation`] for its schema. Null if crate feature `source-location` is not enabled. | @@ -126,6 +151,13 @@ impl From for crate::Error { /// {"level":"error","timestamp":1722817541459,"payload":"something went wrong","logger":"app-component","tid":3478045} /// ``` /// +/// - If key-values are present: +/// +/// ```json +/// {"level":"info","timestamp":1722817541459,"payload":"hello, world!","kv":{"k1":123,"k2":"cool"},"tid":3478045} +/// {"level":"error","timestamp":1722817541459,"payload":"something went wrong","kv":{"k1":123,"k2":"cool"},"tid":3478045} +/// ``` +/// /// - If crate feature `source-location` is enabled: /// /// ```json @@ -193,7 +225,7 @@ mod tests { use chrono::prelude::*; use super::*; - use crate::{Level, SourceLocation, __EOL}; + use crate::{kv, Level, SourceLocation, __EOL}; #[test] fn should_format_json() { @@ -269,4 +301,31 @@ mod tests { ) ); } + + #[test] + fn should_format_json_with_kv() { + let mut dest = StringBuf::new(); + let formatter = JsonFormatter::new(); + let kvs = [ + (kv::Key::__from_static_str("k1"), kv::Value::from(114)), + (kv::Key::__from_static_str("k2"), kv::Value::from("514")), + ]; + let record = Record::new(Level::Info, "payload", None, None, &kvs); + let mut ctx = FormatterContext::new(); + formatter.format(&record, &mut dest, &mut ctx).unwrap(); + + let local_time: DateTime = record.time().into(); + + assert_eq!(ctx.style_range(), None); + assert_eq!( + dest.to_string(), + format!( + r#"{{"level":"info","timestamp":{},"payload":"{}","kv":{{"k1":114,"k2":"514"}},"tid":{}}}{}"#, + local_time.timestamp_millis(), + "payload", + record.tid(), + __EOL + ) + ); + } }