spdlog/formatter/
json_formatter.rs

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