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 .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#[derive(Clone)]
139pub struct JsonFormatter(PhantomData<()>);
140
141impl JsonFormatter {
142 #[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 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}