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 .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#[derive(Clone)]
164pub struct JsonFormatter(PhantomData<()>);
165
166impl JsonFormatter {
167 #[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 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}