1use core::fmt;
9
10use super::Format;
11use crate::record::Record;
12use crate::value::Value;
13
14#[derive(Debug, Clone, Copy, Default)]
38pub struct JsonFormat {
39 pretty: bool,
40 timestamp_rfc3339: bool,
41}
42
43impl JsonFormat {
44 pub const fn new() -> Self {
46 Self {
47 pretty: false,
48 timestamp_rfc3339: false,
49 }
50 }
51
52 pub const fn pretty(mut self, enabled: bool) -> Self {
54 self.pretty = enabled;
55 self
56 }
57
58 pub const fn timestamp_rfc3339(mut self, enabled: bool) -> Self {
64 self.timestamp_rfc3339 = enabled;
65 self
66 }
67}
68
69impl Format for JsonFormat {
70 fn write_record<W: fmt::Write + ?Sized>(
71 &self,
72 record: &Record<'_>,
73 writer: &mut W,
74 ) -> fmt::Result {
75 let nl = if self.pretty { "\n" } else { "" };
76 let ind = if self.pretty { " " } else { "" };
77 let sp = if self.pretty { " " } else { "" };
78
79 writer.write_char('{')?;
80 writer.write_str(nl)?;
81
82 let mut first = true;
83
84 if let Some(ts) = record.metadata.timestamp_unix_nanos {
85 comma(writer, &mut first, nl, ind)?;
86 writer.write_str("\"timestamp\":")?;
87 writer.write_str(sp)?;
88 if self.timestamp_rfc3339 {
89 writer.write_char('"')?;
90 crate::format::rfc3339::write_rfc3339_nanos(ts, writer)?;
91 writer.write_char('"')?;
92 } else {
93 write!(writer, "{ts}")?;
94 }
95 }
96
97 comma(writer, &mut first, nl, ind)?;
98 writer.write_str("\"level\":")?;
99 writer.write_str(sp)?;
100 writer.write_char('"')?;
101 writer.write_str(record.metadata.level.as_str())?;
102 writer.write_char('"')?;
103
104 comma(writer, &mut first, nl, ind)?;
105 writer.write_str("\"target\":")?;
106 writer.write_str(sp)?;
107 write_json_str(writer, record.metadata.target)?;
108
109 comma(writer, &mut first, nl, ind)?;
110 writer.write_str("\"message\":")?;
111 writer.write_str(sp)?;
112 write_json_str(writer, record.message)?;
113
114 if let Some(file) = record.metadata.file {
115 comma(writer, &mut first, nl, ind)?;
116 writer.write_str("\"file\":")?;
117 writer.write_str(sp)?;
118 write_json_str(writer, file)?;
119 }
120 if let Some(line) = record.metadata.line {
121 comma(writer, &mut first, nl, ind)?;
122 writer.write_str("\"line\":")?;
123 writer.write_str(sp)?;
124 write!(writer, "{line}")?;
125 }
126
127 for field in record.all_fields() {
128 comma(writer, &mut first, nl, ind)?;
129 write_json_str(writer, field.key)?;
130 writer.write_char(':')?;
131 writer.write_str(sp)?;
132 write_json_value(writer, field.value)?;
133 }
134
135 if self.pretty {
136 writer.write_char('\n')?;
137 }
138 writer.write_char('}')?;
139 writer.write_char('\n')?;
140 Ok(())
141 }
142}
143
144fn comma<W: fmt::Write + ?Sized>(w: &mut W, first: &mut bool, nl: &str, ind: &str) -> fmt::Result {
145 if *first {
146 *first = false;
147 } else {
148 w.write_char(',')?;
149 w.write_str(nl)?;
150 }
151 w.write_str(ind)?;
152 Ok(())
153}
154
155fn write_json_str<W: fmt::Write + ?Sized>(w: &mut W, s: &str) -> fmt::Result {
156 w.write_char('"')?;
157 let bytes = s.as_bytes();
162 let mut start = 0;
163 let mut i = 0;
164 while i < bytes.len() {
165 let b = bytes[i];
166 let escape: Option<&'static str> = match b {
167 b'"' => Some("\\\""),
168 b'\\' => Some("\\\\"),
169 b'\n' => Some("\\n"),
170 b'\r' => Some("\\r"),
171 b'\t' => Some("\\t"),
172 0x08 => Some("\\b"),
173 0x0c => Some("\\f"),
174 0x00..=0x1F => None, _ => {
176 i += 1;
177 continue;
178 }
179 };
180 if start < i {
181 w.write_str(&s[start..i])?;
186 }
187 if let Some(seq) = escape {
188 w.write_str(seq)?;
189 } else {
190 write!(w, "\\u{:04x}", b)?;
191 }
192 i += 1;
193 start = i;
194 }
195 if start < bytes.len() {
196 w.write_str(&s[start..])?;
197 }
198 w.write_char('"')
199}
200
201fn write_json_value<W: fmt::Write + ?Sized>(w: &mut W, v: Value<'_>) -> fmt::Result {
202 match v {
203 Value::Null => w.write_str("null"),
204 Value::Bool(true) => w.write_str("true"),
205 Value::Bool(false) => w.write_str("false"),
206 Value::I64(n) => write!(w, "{n}"),
207 Value::U64(n) => write!(w, "{n}"),
208 Value::F64(n) => {
209 if n.is_finite() {
210 write!(w, "{n}")
213 } else {
214 w.write_str("null")
215 }
216 }
217 Value::Str(s) => write_json_str(w, s),
218 Value::Char(c) => {
219 let mut buf = [0u8; 4];
222 let s: &str = c.encode_utf8(&mut buf);
223 write_json_str(w, s)
224 }
225 }
226}
227
228#[cfg(all(test, feature = "std"))]
229mod tests {
230 use super::*;
231 use crate::record::{Field, Metadata};
232 use crate::Level;
233
234 fn fmt_one(json: JsonFormat, record: &Record<'_>) -> String {
235 let mut s = String::new();
236 json.write_record(record, &mut s).unwrap();
237 s
238 }
239
240 #[test]
241 fn compact_object_with_trailing_newline() {
242 let record = Record::new(Metadata::new(Level::Info, "app"), "hello", &[]);
243 let out = fmt_one(JsonFormat::new(), &record);
244 assert_eq!(
245 out,
246 "{\"level\":\"info\",\"target\":\"app\",\"message\":\"hello\"}\n"
247 );
248 }
249
250 #[test]
251 fn fields_appear_after_message() {
252 let fields = [
253 Field::new("port", Value::U64(8080)),
254 Field::new("host", Value::Str("0.0.0.0")),
255 ];
256 let record = Record::new(Metadata::new(Level::Info, "app"), "up", &fields);
257 let out = fmt_one(JsonFormat::new(), &record);
258 let msg_pos = out.find("\"message\":\"up\"").unwrap();
259 let port_pos = out.find("\"port\":8080").unwrap();
260 assert!(msg_pos < port_pos);
261 assert!(out.contains("\"host\":\"0.0.0.0\""));
262 }
263
264 #[test]
265 fn context_appears_before_fields() {
266 let ctx = [Field::new("trace_id", Value::Str("abc"))];
267 let fields = [Field::new("port", Value::U64(80))];
268 let record = Record::with_context(Metadata::new(Level::Info, "app"), "msg", &fields, &ctx);
269 let out = fmt_one(JsonFormat::new(), &record);
270 let trace_pos = out.find("\"trace_id\"").unwrap();
271 let port_pos = out.find("\"port\"").unwrap();
272 assert!(trace_pos < port_pos);
273 }
274
275 #[test]
276 fn escapes_control_and_quotes() {
277 let fields = [Field::new("data", Value::Str("a\"b\nc\td"))];
278 let record = Record::new(Metadata::new(Level::Info, "t"), "x", &fields);
279 let out = fmt_one(JsonFormat::new(), &record);
280 assert!(out.contains(r#""data":"a\"b\nc\td""#), "{out}");
281 }
282
283 #[test]
284 fn escapes_low_codepoint_to_unicode() {
285 let fields = [Field::new("data", Value::Str("\x01"))];
286 let record = Record::new(Metadata::new(Level::Info, "t"), "x", &fields);
287 let out = fmt_one(JsonFormat::new(), &record);
288 let backslash = "\\";
289 let expected = format!("\"data\":\"{}u0001\"", backslash);
290 assert!(out.contains(&expected), "{}", out);
291 }
292
293 #[test]
294 fn nan_inf_become_null() {
295 let fields = [
296 Field::new("a", Value::F64(f64::NAN)),
297 Field::new("b", Value::F64(f64::INFINITY)),
298 Field::new("c", Value::F64(1.5)),
299 ];
300 let record = Record::new(Metadata::new(Level::Info, "t"), "x", &fields);
301 let out = fmt_one(JsonFormat::new(), &record);
302 assert!(out.contains("\"a\":null"));
303 assert!(out.contains("\"b\":null"));
304 assert!(out.contains("\"c\":1.5"));
305 }
306
307 #[test]
308 fn timestamp_unix_nanos_default() {
309 let metadata = Metadata::new(Level::Info, "t").with_timestamp(1_700_000_000_000_000_000);
310 let record = Record::new(metadata, "x", &[]);
311 let out = fmt_one(JsonFormat::new(), &record);
312 assert!(out.contains("\"timestamp\":1700000000000000000"));
313 }
314
315 #[test]
316 fn timestamp_rfc3339_when_enabled() {
317 let metadata = Metadata::new(Level::Info, "t").with_timestamp(1_700_000_000_000_000_000);
318 let record = Record::new(metadata, "x", &[]);
319 let out = fmt_one(JsonFormat::new().timestamp_rfc3339(true), &record);
320 assert!(out.contains("\"timestamp\":\"2023-11-14T22:13:20"), "{out}");
321 }
322
323 #[test]
324 fn pretty_indents_and_breaks_lines() {
325 let fields = [Field::new("port", Value::U64(80))];
326 let record = Record::new(Metadata::new(Level::Info, "t"), "x", &fields);
327 let out = fmt_one(JsonFormat::new().pretty(true), &record);
328 assert!(out.contains("\n \"level\": \"info\""), "{out}");
329 assert!(out.ends_with("}\n"));
330 }
331}