Skip to content

Commit 4bc900b

Browse files
authored
Implement JsonFormatter (#69)
1 parent 084b75d commit 4bc900b

File tree

8 files changed

+306
-2
lines changed

8 files changed

+306
-2
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ jobs:
4949
fail-fast: false
5050
matrix:
5151
os: ['ubuntu-latest', 'windows-latest', 'macos-latest']
52-
fn_features: ['', 'log native libsystemd multi-thread runtime-pattern']
52+
fn_features: ['', 'log native libsystemd multi-thread runtime-pattern serde_json']
5353
cfg_feature: ['', 'flexible-string', 'source-location']
5454
runs-on: ${{ matrix.os }}
5555
steps:
@@ -212,7 +212,7 @@ jobs:
212212
- name: Restore cargo caches
213213
uses: Swatinem/rust-cache@v2
214214
- name: Run benchmark
215-
run: cargo +nightly bench --features "multi-thread,runtime-pattern" --bench spdlog_rs --bench spdlog_rs_pattern | tee bench-results.txt
215+
run: cargo +nightly bench --features "multi-thread,runtime-pattern,serde_json" --bench spdlog_rs --bench spdlog_rs_pattern | tee bench-results.txt
216216
- name: Discard irrelevant changes
217217
run: git checkout -- spdlog/Cargo.toml
218218
- name: Process results

spdlog/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ native = []
3737
libsystemd = ["libsystemd-sys"]
3838
multi-thread = ["crossbeam"]
3939
runtime-pattern = ["spdlog-internal"]
40+
serde_json = ["serde", "dep:serde_json"]
4041

4142
[dependencies]
4243
arc-swap = "1.5.1"
@@ -50,6 +51,8 @@ if_chain = "1.0.2"
5051
is-terminal = "0.4"
5152
log = { version = "0.4.8", optional = true }
5253
once_cell = "1.16.0"
54+
serde = { version = "1.0.163", optional = true, features = ["derive"] }
55+
serde_json = { version = "1.0.120", optional = true }
5356
spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = true }
5457
spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" }
5558
spin = "0.9.8"
@@ -106,6 +109,7 @@ harness = false
106109
[[bench]]
107110
name = "spdlog_rs_pattern"
108111
path = "benches/spdlog-rs/pattern.rs"
112+
required-features = ["runtime-pattern", "serde_json"]
109113
[[bench]]
110114
name = "fast_log"
111115
path = "benches/fast_log/main.rs"

spdlog/benches/spdlog-rs/pattern.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ extern crate test;
55
use std::{cell::RefCell, sync::Arc};
66

77
use paste::paste;
8+
#[cfg(feature = "serde_json")]
9+
use spdlog::formatter::JsonFormatter;
810
use spdlog::{
911
formatter::{pattern, Formatter, FullFormatter, Pattern, PatternFormatter},
1012
prelude::*,
@@ -104,6 +106,12 @@ fn bench_1_full_formatter(bencher: &mut Bencher) {
104106
bench_formatter(bencher, FullFormatter::new())
105107
}
106108

109+
#[cfg(feature = "serde_json")]
110+
#[bench]
111+
fn bench_1_json_formatter(bencher: &mut Bencher) {
112+
bench_formatter(bencher, JsonFormatter::new())
113+
}
114+
107115
#[bench]
108116
fn bench_2_full_pattern_ct(bencher: &mut Bencher) {
109117
bench_full_pattern(

spdlog/src/error.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ pub enum Error {
9898
#[error("failed to build pattern at runtime: {0}")]
9999
BuildPattern(BuildPatternError),
100100

101+
/// Returned by [`Formatter`]s when an error occurs in serializing a log.
102+
///
103+
/// [`Formatter`]: crate::formatter::Formatter
104+
#[cfg(feature = "serde")]
105+
#[error("failed to serialize log: {0}")]
106+
SerializeRecord(io::Error),
107+
101108
/// Returned when multiple errors occurred.
102109
#[error("{0:?}")]
103110
Multiple(Vec<Error>),
Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
use std::{
2+
fmt::{self, Write},
3+
marker::PhantomData,
4+
time::SystemTime,
5+
};
6+
7+
use cfg_if::cfg_if;
8+
use serde::{ser::SerializeStruct, Serialize};
9+
10+
use crate::{
11+
formatter::{FmtExtraInfo, Formatter},
12+
Error, Record, StringBuf, __EOL,
13+
};
14+
15+
struct JsonRecord<'a>(&'a Record<'a>);
16+
17+
impl<'a> Serialize for JsonRecord<'a> {
18+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
19+
where
20+
S: serde::Serializer,
21+
{
22+
let src_loc = self.0.source_location();
23+
24+
let mut record =
25+
serializer.serialize_struct("JsonRecord", if src_loc.is_none() { 4 } else { 5 })?;
26+
27+
record.serialize_field("level", &self.0.level())?;
28+
record.serialize_field(
29+
"timestamp",
30+
&self
31+
.0
32+
.time()
33+
.duration_since(SystemTime::UNIX_EPOCH)
34+
.ok()
35+
// https://github.com/SpriteOvO/spdlog-rs/pull/69#discussion_r1694063293
36+
.and_then(|dur| u64::try_from(dur.as_millis()).ok())
37+
.expect("invalid timestamp"),
38+
)?;
39+
record.serialize_field("payload", self.0.payload())?;
40+
if let Some(logger_name) = self.0.logger_name() {
41+
record.serialize_field("logger", logger_name)?;
42+
}
43+
record.serialize_field("tid", &self.0.tid())?;
44+
if let Some(src_loc) = src_loc {
45+
record.serialize_field("source", src_loc)?;
46+
}
47+
48+
record.end()
49+
}
50+
}
51+
52+
impl<'a> From<&'a Record<'a>> for JsonRecord<'a> {
53+
fn from(value: &'a Record<'a>) -> Self {
54+
JsonRecord(value)
55+
}
56+
}
57+
58+
enum JsonFormatterError {
59+
Fmt(fmt::Error),
60+
Serialization(serde_json::Error),
61+
}
62+
63+
impl From<fmt::Error> for JsonFormatterError {
64+
fn from(value: fmt::Error) -> Self {
65+
JsonFormatterError::Fmt(value)
66+
}
67+
}
68+
69+
impl From<serde_json::Error> for JsonFormatterError {
70+
fn from(value: serde_json::Error) -> Self {
71+
JsonFormatterError::Serialization(value)
72+
}
73+
}
74+
75+
impl From<JsonFormatterError> for crate::Error {
76+
fn from(value: JsonFormatterError) -> Self {
77+
match value {
78+
JsonFormatterError::Fmt(e) => Error::FormatRecord(e),
79+
JsonFormatterError::Serialization(e) => Error::SerializeRecord(e.into()),
80+
}
81+
}
82+
}
83+
84+
#[rustfmt::skip]
85+
/// JSON logs formatter.
86+
///
87+
/// Each log will be serialized into a single line of JSON object with the following schema.
88+
///
89+
/// ## Schema
90+
///
91+
/// | Field | Type | Description |
92+
/// |-------------|--------------|--------------------------------------------------------------------------------------------------------------------------------|
93+
/// | `level` | String | The level of the log. Same as the return of [`Level::as_str`]. |
94+
/// | `timestamp` | Integer(u64) | The timestamp when the log was generated, in milliseconds since January 1, 1970 00:00:00 UTC. |
95+
/// | `payload` | String | The contents of the log. |
96+
/// | `logger` | String/Null | The name of the logger. Null if the logger has no name. |
97+
/// | `tid` | Integer(u64) | The thread ID when the log was generated. |
98+
/// | `source` | Object/Null | The source location of the log. See [`SourceLocation`] for its schema. Null if crate feature `source-location` is not enabled. |
99+
///
100+
/// <div class="warning">
101+
///
102+
/// - If the type of a field is Null, the field will not be present or be `null`.
103+
///
104+
/// - The order of the fields is not guaranteed.
105+
///
106+
/// </div>
107+
///
108+
/// ---
109+
///
110+
/// ## Examples
111+
///
112+
/// - Default:
113+
///
114+
/// ```json
115+
/// {"level":"Info","timestamp":1722817424798,"payload":"hello, world!","tid":3472525}
116+
/// {"level":"Error","timestamp":1722817424798,"payload":"something went wrong","tid":3472525}
117+
/// ```
118+
///
119+
/// - If the logger has a name:
120+
///
121+
/// ```json
122+
/// {"level":"Info","timestamp":1722817541459,"payload":"hello, world!","logger":"app-component","tid":3478045}
123+
/// {"level":"Error","timestamp":1722817541459,"payload":"something went wrong","logger":"app-component","tid":3478045}
124+
/// ```
125+
///
126+
/// - If crate feature `source-location` is enabled:
127+
///
128+
/// ```json
129+
/// {"level":"Info","timestamp":1722817572709,"payload":"hello, world!","tid":3479856,"source":{"module_path":"my_app::say_hi","file":"src/say_hi.rs","line":4,"column":5}}
130+
/// {"level":"Error","timestamp":1722817572709,"payload":"something went wrong","tid":3479856,"source":{"module_path":"my_app::say_hi","file":"src/say_hi.rs","line":5,"column":5}}
131+
/// ```
132+
///
133+
/// [`Level::as_str`]: crate::Level::as_str
134+
/// [`SourceLocation`]: crate::SourceLocation
135+
#[derive(Clone)]
136+
pub struct JsonFormatter(PhantomData<()>);
137+
138+
impl JsonFormatter {
139+
/// Constructs a `JsonFormatter`.
140+
#[must_use]
141+
pub fn new() -> JsonFormatter {
142+
JsonFormatter(PhantomData)
143+
}
144+
145+
fn format_impl(
146+
&self,
147+
record: &Record,
148+
dest: &mut StringBuf,
149+
) -> Result<FmtExtraInfo, JsonFormatterError> {
150+
cfg_if! {
151+
if #[cfg(not(feature = "flexible-string"))] {
152+
dest.reserve(crate::string_buf::RESERVE_SIZE);
153+
}
154+
}
155+
156+
let json_record: JsonRecord = record.into();
157+
158+
// TODO: https://github.com/serde-rs/json/issues/863
159+
//
160+
// The performance can be significantly optimized here if the issue can be
161+
// solved.
162+
dest.write_str(&serde_json::to_string(&json_record)?)?;
163+
164+
dest.write_str(__EOL)?;
165+
166+
Ok(FmtExtraInfo { style_range: None })
167+
}
168+
}
169+
170+
impl Formatter for JsonFormatter {
171+
fn format(&self, record: &Record, dest: &mut StringBuf) -> crate::Result<FmtExtraInfo> {
172+
self.format_impl(record, dest).map_err(Into::into)
173+
}
174+
}
175+
176+
impl Default for JsonFormatter {
177+
fn default() -> Self {
178+
JsonFormatter::new()
179+
}
180+
}
181+
182+
#[cfg(test)]
183+
mod tests {
184+
use chrono::prelude::*;
185+
186+
use super::*;
187+
use crate::{Level, SourceLocation, __EOL};
188+
189+
#[test]
190+
fn should_format_json() {
191+
let mut dest = StringBuf::new();
192+
let formatter = JsonFormatter::new();
193+
let record = Record::builder(Level::Info, "payload").build();
194+
let extra_info = formatter.format(&record, &mut dest).unwrap();
195+
196+
let local_time: DateTime<Local> = record.time().into();
197+
198+
assert_eq!(extra_info.style_range, None);
199+
assert_eq!(
200+
dest.to_string(),
201+
format!(
202+
r#"{{"level":"info","timestamp":{},"payload":"{}","tid":{}}}{}"#,
203+
local_time.timestamp_millis(),
204+
"payload",
205+
record.tid(),
206+
__EOL
207+
)
208+
);
209+
}
210+
211+
#[test]
212+
fn should_format_json_with_logger_name() {
213+
let mut dest = StringBuf::new();
214+
let formatter = JsonFormatter::new();
215+
let record = Record::builder(Level::Info, "payload")
216+
.logger_name("my-component")
217+
.build();
218+
let extra_info = formatter.format(&record, &mut dest).unwrap();
219+
220+
let local_time: DateTime<Local> = record.time().into();
221+
222+
assert_eq!(extra_info.style_range, None);
223+
assert_eq!(
224+
dest.to_string(),
225+
format!(
226+
r#"{{"level":"info","timestamp":{},"payload":"{}","logger":"my-component","tid":{}}}{}"#,
227+
local_time.timestamp_millis(),
228+
"payload",
229+
record.tid(),
230+
__EOL
231+
)
232+
);
233+
}
234+
235+
#[test]
236+
fn should_format_json_with_src_loc() {
237+
let mut dest = StringBuf::new();
238+
let formatter = JsonFormatter::new();
239+
let record = Record::builder(Level::Info, "payload")
240+
.source_location(Some(SourceLocation::__new("module", "file.rs", 1, 2)))
241+
.build();
242+
let extra_info = formatter.format(&record, &mut dest).unwrap();
243+
244+
let local_time: DateTime<Local> = record.time().into();
245+
246+
assert_eq!(extra_info.style_range, None);
247+
assert_eq!(
248+
dest.to_string(),
249+
format!(
250+
r#"{{"level":"info","timestamp":{},"payload":"{}","tid":{},"source":{{"module_path":"module","file":"file.rs","line":1,"column":2}}}}{}"#,
251+
local_time.timestamp_millis(),
252+
"payload",
253+
record.tid(),
254+
__EOL
255+
)
256+
);
257+
}
258+
}

spdlog/src/formatter/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ mod full_formatter;
5454
all(doc, not(doctest))
5555
))]
5656
mod journald_formatter;
57+
#[cfg(feature = "serde_json")]
58+
mod json_formatter;
5759
mod local_time_cacher;
5860
mod pattern_formatter;
5961

@@ -66,6 +68,8 @@ pub use full_formatter::*;
6668
all(doc, not(doctest))
6769
))]
6870
pub(crate) use journald_formatter::*;
71+
#[cfg(feature = "serde_json")]
72+
pub use json_formatter::*;
6973
pub(crate) use local_time_cacher::*;
7074
pub use pattern_formatter::*;
7175

spdlog/src/level.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ pub enum Level {
6767
Trace,
6868
}
6969

70+
#[cfg(feature = "serde")]
71+
impl serde::Serialize for Level {
72+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
73+
where
74+
S: serde::Serializer,
75+
{
76+
serializer.serialize_str(self.as_str())
77+
}
78+
}
79+
7080
cfg_if! {
7181
if #[cfg(test)] {
7282
crate::utils::const_assert!(atomic::Atomic::<Level>::is_lock_free());

spdlog/src/source_location.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,21 @@ use std::path;
55
/// Usually users don't need to construct it manually, but if you do, use macro
66
/// [`source_location_current`].
77
///
8+
/// ## Schema
9+
///
10+
/// This struct is implemented [`serde::Serialize`] if crate feature `serde` is
11+
/// enabled.
12+
///
13+
/// | Field | Type |
14+
/// |---------------|--------|
15+
/// | `module_path` | String |
16+
/// | `file` | String |
17+
/// | `line` | u32 |
18+
/// | `column` | u32 |
19+
///
820
/// [`source_location_current`]: crate::source_location_current
921
#[derive(Clone, Hash, Debug)]
22+
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
1023
pub struct SourceLocation {
1124
module_path: &'static str,
1225
file: &'static str,

0 commit comments

Comments
 (0)