spdlog/formatter/
json_formatter.rs

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