1use core::fmt;
9use core::fmt::Write as _;
10
11use super::Format;
12use super::escape::{JsonEscaper, RESERVED_PREFIX, is_reserved_key, write_json_escaped};
13use crate::record::Record;
14use crate::value::{Value, write_hex};
15
16#[cfg_attr(all(feature = "json"), doc = "```")]
28#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
29#[derive(Debug, Clone, Copy, Default)]
46pub struct JsonFormat {
47 pretty: bool,
48 timestamp_rfc3339: bool,
49}
50
51impl JsonFormat {
52 #[must_use]
54 pub const fn new() -> Self {
55 Self {
56 pretty: false,
57 timestamp_rfc3339: false,
58 }
59 }
60
61 #[must_use]
64 pub const fn pretty(mut self, enabled: bool) -> Self {
65 self.pretty = enabled;
66 self
67 }
68
69 #[must_use]
77 pub const fn timestamp_rfc3339(mut self, enabled: bool) -> Self {
78 self.timestamp_rfc3339 = enabled;
79 self
80 }
81
82 #[inline(never)]
86 fn write_impl<W: fmt::Write + ?Sized>(&self, record: &Record<'_>, w: &mut W) -> fmt::Result {
87 let p = if self.pretty { &PRETTY } else { &COMPACT };
88 w.write_str(p.open)?;
89 if let Some(ts) = record.metadata.timestamp_unix_nanos {
90 w.write_str(p.timestamp)?;
91 if self.timestamp_rfc3339 {
92 w.write_char('"')?;
93 super::rfc3339::write_rfc3339_nanos(ts, w)?;
94 w.write_char('"')?;
95 } else {
96 write!(w, "{ts}")?;
97 }
98 w.write_str(p.next)?;
99 }
100 w.write_str(p.level)?;
101 w.write_str(record.metadata.level.as_str())?;
102 w.write_str(p.target)?;
103 write_string(w, record.metadata.target)?;
104 w.write_str(p.message)?;
105 write_string(w, record.message)?;
106 if let Some(file) = record.metadata.file {
107 w.write_str(p.file)?;
108 write_string(w, file)?;
109 }
110 if let Some(line) = record.metadata.line {
111 w.write_str(p.line)?;
112 write!(w, "{line}")?;
113 }
114 for field in record.all_fields() {
115 w.write_str(p.field)?;
116 if is_reserved_key(field.key) {
117 w.write_str(RESERVED_PREFIX)?;
118 }
119 write_json_escaped(w, field.key)?;
120 w.write_str(p.key_end)?;
121 write_value(w, field.value)?;
122 }
123 w.write_str(p.close)
124 }
125}
126
127struct Punctuation {
131 open: &'static str,
132 timestamp: &'static str,
133 next: &'static str,
134 level: &'static str,
135 target: &'static str,
136 message: &'static str,
137 file: &'static str,
138 line: &'static str,
139 field: &'static str,
140 key_end: &'static str,
141 close: &'static str,
142}
143
144const COMPACT: Punctuation = Punctuation {
145 open: "{",
146 timestamp: "\"timestamp\":",
147 next: ",",
148 level: "\"level\":\"",
149 target: "\",\"target\":",
150 message: ",\"message\":",
151 file: ",\"file\":",
152 line: ",\"line\":",
153 field: ",\"",
154 key_end: "\":",
155 close: "}
156",
157};
158
159const PRETTY: Punctuation = Punctuation {
160 open: "{
161 ",
162 timestamp: "\"timestamp\": ",
163 next: ",
164 ",
165 level: "\"level\": \"",
166 target: "\",
167 \"target\": ",
168 message: ",
169 \"message\": ",
170 file: ",
171 \"file\": ",
172 line: ",
173 \"line\": ",
174 field: ",
175 \"",
176 key_end: "\": ",
177 close: "
178}
179",
180};
181
182impl Format for JsonFormat {
183 fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
184 self.write_impl(record, writer)
185 }
186
187 #[inline]
188 fn write_record_generic<W: fmt::Write>(
189 &self,
190 record: &Record<'_>,
191 writer: &mut W,
192 ) -> fmt::Result {
193 self.write_impl(record, writer)
194 }
195}
196
197#[inline]
198fn write_string<W: fmt::Write + ?Sized>(w: &mut W, s: &str) -> fmt::Result {
199 w.write_char('"')?;
200 write_json_escaped(w, s)?;
201 w.write_char('"')
202}
203
204fn write_value<W: fmt::Write + ?Sized>(w: &mut W, v: Value<'_>) -> fmt::Result {
205 match v {
206 Value::Null => w.write_str("null"),
207 Value::Bool(true) => w.write_str("true"),
208 Value::Bool(false) => w.write_str("false"),
209 Value::I64(n) => write!(w, "{n}"),
210 Value::U64(n) => write!(w, "{n}"),
211 Value::I128(n) => write!(w, "{n}"),
212 Value::U128(n) => write!(w, "{n}"),
213 Value::F64(n) if n.is_finite() => write!(w, "{n:?}"),
217 Value::F64(_) => w.write_str("null"),
218 Value::Str(s) => write_string(w, s),
219 Value::Char(c) => {
220 let mut buf = [0_u8; 4];
221 write_string(w, c.encode_utf8(&mut buf))
222 }
223 Value::Bytes(b) => {
224 w.write_char('"')?;
225 write_hex(w, b)?;
226 w.write_char('"')
227 }
228 lazy @ (Value::Display(_) | Value::Debug(_) | Value::Error(_)) => {
229 w.write_char('"')?;
230 write!(JsonEscaper(&mut *w), "{lazy}")?;
231 w.write_char('"')
232 }
233 }
234}
235
236#[cfg(all(test, feature = "std"))]
237mod tests {
238 use super::*;
239 use crate::Level;
240 use crate::record::{Field, Metadata};
241
242 fn fmt_one(json: JsonFormat, record: &Record<'_>) -> String {
243 let mut s = String::new();
244 json.write_record_generic(record, &mut s).unwrap();
245 let mut d = String::new();
246 json.write_record(record, &mut d).unwrap();
247 assert_eq!(s, d);
248 s
249 }
250
251 fn field_json(v: Value<'_>) -> String {
252 let fields = [Field::new("v", v)];
253 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
254 let out = fmt_one(JsonFormat::new(), &record);
255 let start = out.find("\"v\":").unwrap() + 4;
256 out[start..out.len() - 2].to_string()
257 }
258
259 #[test]
260 fn test_json_compact_object_with_trailing_newline() {
261 let record = Record::new(Metadata::new(Level::Info, "app"), "hello", &[]);
262 assert_eq!(
263 fmt_one(JsonFormat::new(), &record),
264 "{\"level\":\"info\",\"target\":\"app\",\"message\":\"hello\"}\n"
265 );
266 }
267
268 #[test]
269 fn test_json_metadata_order_is_stable() {
270 let meta = Metadata::new(Level::Warn, "a")
271 .with_location("f.rs", 7)
272 .with_timestamp(5);
273 let record = Record::new(meta, "m", &[]);
274 assert_eq!(
275 fmt_one(JsonFormat::new(), &record),
276 "{\"timestamp\":5,\"level\":\"warn\",\"target\":\"a\",\"message\":\"m\",\
277 \"file\":\"f.rs\",\"line\":7}\n"
278 );
279 }
280
281 #[test]
282 fn test_json_context_before_fields() {
283 let ctx = [Field::new("trace_id", "abc")];
284 let fields = [Field::new("port", 80_u32)];
285 let record = Record::with_context(Metadata::new(Level::Info, "app"), "msg", &fields, &ctx);
286 let out = fmt_one(JsonFormat::new(), &record);
287 assert!(out.find("\"trace_id\"").unwrap() < out.find("\"port\"").unwrap());
288 }
289
290 #[test]
291 fn test_json_values_render_per_variant() {
292 assert_eq!(field_json(Value::Null), "null");
293 assert_eq!(field_json(Value::Bool(true)), "true");
294 assert_eq!(field_json(Value::Bool(false)), "false");
295 assert_eq!(field_json(Value::I64(-3)), "-3");
296 assert_eq!(field_json(Value::U64(3)), "3");
297 assert_eq!(field_json(Value::I128(i128::MIN)), i128::MIN.to_string());
298 assert_eq!(field_json(Value::U128(u128::MAX)), u128::MAX.to_string());
299 assert_eq!(field_json(Value::F64(1.0)), "1.0");
300 assert_eq!(field_json(Value::F64(1.5)), "1.5");
301 assert_eq!(field_json(Value::F64(1e300)), "1e300");
302 assert_eq!(field_json(Value::F64(f64::NAN)), "null");
303 assert_eq!(field_json(Value::F64(f64::NEG_INFINITY)), "null");
304 assert_eq!(field_json(Value::Str("a\"b\nc")), "\"a\\\"b\\nc\"");
305 assert_eq!(field_json(Value::Char('\n')), "\"\\n\"");
306 assert_eq!(field_json(Value::Char('"')), "\"\\\"\"");
307 assert_eq!(field_json(Value::Bytes(&[0xde, 0xad])), "\"dead\"");
308 assert_eq!(field_json(Value::Display(&"x\ty")), "\"x\\ty\"");
309 assert_eq!(field_json(Value::Debug(&"q")), "\"\\\"q\\\"\"");
310 let e = std::io::Error::other("disk \"full\"");
311 assert_eq!(field_json(Value::Error(&e)), "\"disk \\\"full\\\"\"");
312 }
313
314 #[test]
315 fn test_json_reserved_keys_are_prefixed() {
316 let fields = [
317 Field::new("level", "fake"),
318 Field::new("message", "spoofed"),
319 Field::new("timestamp", 0_u32),
320 Field::new("target", "x"),
321 Field::new("file", "y"),
322 Field::new("line", 1_u32),
323 Field::new("levels", "not reserved"),
324 ];
325 let record = Record::new(Metadata::new(Level::Error, "t"), "real", &fields);
326 let out = fmt_one(JsonFormat::new(), &record);
327 assert_eq!(out.matches("\"level\":").count(), 1, "{out}");
328 assert_eq!(out.matches("\"message\":").count(), 1, "{out}");
329 for k in ["level", "message", "timestamp", "target", "file", "line"] {
330 assert!(out.contains(&format!("\"fields.{k}\":")), "{k}: {out}");
331 }
332 assert!(out.contains("\"levels\":"), "{out}");
333 }
334
335 #[test]
336 fn test_json_keys_are_escaped() {
337 let fields = [Field::new("we\"ird\n", 1_u32)];
338 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
339 let out = fmt_one(JsonFormat::new(), &record);
340 assert!(out.contains("\"we\\\"ird\\n\":1"), "{out}");
341 }
342
343 #[test]
344 fn test_json_timestamp_forms() {
345 let meta = Metadata::new(Level::Info, "t").with_timestamp(1_700_000_000_000_000_000);
346 let record = Record::new(meta, "x", &[]);
347 assert!(fmt_one(JsonFormat::new(), &record).contains("\"timestamp\":1700000000000000000"));
348 assert!(
349 fmt_one(JsonFormat::new().timestamp_rfc3339(true), &record)
350 .contains("\"timestamp\":\"2023-11-14T22:13:20.000000000Z\"")
351 );
352 }
353
354 #[test]
355 fn test_json_pretty_indents_and_breaks_lines() {
356 let fields = [Field::new("port", 80_u32)];
357 let record = Record::new(Metadata::new(Level::Info, "t"), "x", &fields);
358 let out = fmt_one(JsonFormat::new().pretty(true), &record);
359 assert_eq!(
360 out,
361 "{\n \"level\": \"info\",\n \"target\": \"t\",\n \"message\": \"x\",\n \"port\": 80\n}\n"
362 );
363 }
364}