1use core::fmt;
11use core::fmt::Write as _;
12
13use super::Format;
14use super::escape::{
15 JsonEscaper, RESERVED_PREFIX, is_reserved_key, render_once, value_needs_quotes,
16 write_json_escaped, write_sanitized_key,
17};
18use crate::record::Record;
19use crate::value::{Value, write_f64, write_hex};
20
21#[cfg_attr(all(feature = "logfmt"), doc = "```")]
29#[cfg_attr(not(all(feature = "logfmt")), doc = "```ignore")]
30#[derive(Debug, Clone, Copy, Default)]
42pub struct LogfmtFormat {
43 timestamp_rfc3339: bool,
44}
45
46impl LogfmtFormat {
47 #[must_use]
49 pub const fn new() -> Self {
50 Self {
51 timestamp_rfc3339: false,
52 }
53 }
54
55 #[must_use]
58 pub const fn timestamp_rfc3339(mut self, enabled: bool) -> Self {
59 self.timestamp_rfc3339 = enabled;
60 self
61 }
62
63 #[inline(never)]
67 fn write_impl<W: fmt::Write + ?Sized>(&self, record: &Record<'_>, w: &mut W) -> fmt::Result {
68 if let Some(ts) = record.metadata.timestamp_unix_nanos {
69 w.write_str("timestamp=")?;
70 if self.timestamp_rfc3339 {
71 super::rfc3339::write_rfc3339_nanos(ts, w)?;
72 } else {
73 write!(w, "{ts}")?;
74 }
75 w.write_char(' ')?;
76 }
77 w.write_str("level=")?;
78 w.write_str(record.metadata.level.as_str())?;
79 w.write_str(" target=")?;
80 write_str_value(w, record.metadata.target)?;
81 w.write_str(" message=")?;
82 write_str_value(w, record.message)?;
83 if let Some(file) = record.metadata.file {
84 w.write_str(" file=")?;
85 write_str_value(w, file)?;
86 }
87 if let Some(line) = record.metadata.line {
88 write!(w, " line={line}")?;
89 }
90 for field in record.all_fields() {
91 w.write_char(' ')?;
92 if is_reserved_key(field.key) {
93 w.write_str(RESERVED_PREFIX)?;
94 }
95 write_sanitized_key(w, field.key)?;
96 w.write_char('=')?;
97 write_value(w, field.value)?;
98 }
99 w.write_char('\n')
100 }
101}
102
103impl Format for LogfmtFormat {
104 fn write_record(&self, record: &Record<'_>, writer: &mut dyn fmt::Write) -> fmt::Result {
105 self.write_impl(record, writer)
106 }
107
108 #[inline]
109 fn write_record_generic<W: fmt::Write>(
110 &self,
111 record: &Record<'_>,
112 writer: &mut W,
113 ) -> fmt::Result {
114 self.write_impl(record, writer)
115 }
116}
117
118#[inline]
119fn write_str_value<W: fmt::Write + ?Sized>(w: &mut W, s: &str) -> fmt::Result {
120 if value_needs_quotes(s) {
121 w.write_char('"')?;
122 write_json_escaped(w, s)?;
123 w.write_char('"')
124 } else {
125 w.write_str(s)
126 }
127}
128
129fn write_value<W: fmt::Write + ?Sized>(w: &mut W, v: Value<'_>) -> fmt::Result {
130 match v {
131 Value::Null => Ok(()),
132 Value::Bool(true) => w.write_str("true"),
133 Value::Bool(false) => w.write_str("false"),
134 Value::I64(n) => write!(w, "{n}"),
135 Value::U64(n) => write!(w, "{n}"),
136 Value::I128(n) => write!(w, "{n}"),
137 Value::U128(n) => write!(w, "{n}"),
138 Value::F64(n) => write_f64(w, n),
139 Value::Str(s) => write_str_value(w, s),
140 Value::Char(c) => {
141 let mut buf = [0_u8; 4];
142 write_str_value(w, c.encode_utf8(&mut buf))
143 }
144 Value::Bytes([]) => w.write_str("\"\""),
145 Value::Bytes(b) => write_hex(w, b),
146 lazy @ (Value::Display(_) | Value::Debug(_) | Value::Error(_)) => {
147 render_once(&lazy, |text| match text {
151 Some(text) => write_str_value(w, text),
152 None => {
155 w.write_char('"')?;
156 write!(JsonEscaper(&mut *w), "{lazy}")?;
157 w.write_char('"')
158 }
159 })
160 }
161 }
162}
163
164#[cfg(all(test, feature = "std"))]
165mod tests {
166 use super::*;
167 use crate::Level;
168 use crate::record::{Field, Metadata};
169
170 fn fmt_one(fmt: LogfmtFormat, record: &Record<'_>) -> String {
171 let mut s = String::new();
172 fmt.write_record_generic(record, &mut s).unwrap();
173 let mut d = String::new();
174 fmt.write_record(record, &mut d).unwrap();
175 assert_eq!(s, d);
176 s
177 }
178
179 fn field_logfmt(v: Value<'_>) -> String {
180 let fields = [Field::new("v", v)];
181 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
182 let out = fmt_one(LogfmtFormat::new(), &record);
183 let start = out.find(" v=").unwrap() + 3;
184 out[start..out.len() - 1].to_string()
185 }
186
187 struct Changing(core::cell::Cell<u32>);
189
190 impl core::fmt::Display for Changing {
191 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
192 let n = self.0.get();
193 self.0.set(n + 1);
194 if n == 0 {
195 f.write_str("clean")
196 } else {
197 f.write_str("clean\nlevel=error message=forged\u{1b}[2J")
198 }
199 }
200 }
201
202 #[test]
207 fn test_logfmt_lazy_value_is_rendered_once() {
208 fn once(value: &Changing) -> String {
209 let fields = [Field::new("v", Value::Display(value))];
210 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
211 let mut out = String::new();
212 LogfmtFormat::new()
213 .write_record_generic(&record, &mut out)
214 .unwrap();
215 let start = out.find(" v=").unwrap() + 3;
216 out[start..out.len() - 1].to_string()
217 }
218 let value = Changing(core::cell::Cell::new(0));
219 let out = once(&value);
220 assert_eq!(out, "clean");
221 assert_eq!(value.0.get(), 1, "rendered more than once");
222 let out = once(&value);
225 assert!(!out.contains('\n') && !out.contains('\u{1b}'), "{out:?}");
226 assert!(out.starts_with('"') && out.ends_with('"'), "{out:?}");
227 }
228
229 #[test]
230 fn test_logfmt_simple_record() {
231 let record = Record::new(Metadata::new(Level::Info, "app"), "hello", &[]);
232 assert_eq!(
233 fmt_one(LogfmtFormat::new(), &record),
234 "level=info target=app message=hello\n"
235 );
236 }
237
238 #[test]
239 fn test_logfmt_metadata_and_timestamps() {
240 let meta = Metadata::new(Level::Debug, "a")
241 .with_location("src/x.rs", 9)
242 .with_timestamp(1_700_000_000_000_000_000);
243 let record = Record::new(meta, "m", &[]);
244 assert_eq!(
245 fmt_one(LogfmtFormat::new(), &record),
246 "timestamp=1700000000000000000 level=debug target=a message=m file=src/x.rs line=9\n"
247 );
248 assert!(
249 fmt_one(LogfmtFormat::new().timestamp_rfc3339(true), &record)
250 .starts_with("timestamp=2023-11-14T22:13:20.000000000Z level=debug")
251 );
252 }
253
254 #[test]
255 fn test_logfmt_values_render_per_variant() {
256 assert_eq!(field_logfmt(Value::Null), "");
257 assert_eq!(field_logfmt(Value::Bool(true)), "true");
258 assert_eq!(field_logfmt(Value::Bool(false)), "false");
259 assert_eq!(field_logfmt(Value::I64(-1)), "-1");
260 assert_eq!(field_logfmt(Value::U64(1)), "1");
261 assert_eq!(field_logfmt(Value::I128(-2)), "-2");
262 assert_eq!(field_logfmt(Value::U128(2)), "2");
263 assert_eq!(field_logfmt(Value::F64(1.0)), "1.0");
264 assert_eq!(field_logfmt(Value::F64(f64::NAN)), "NaN");
265 assert_eq!(field_logfmt(Value::F64(f64::INFINITY)), "inf");
266 assert_eq!(field_logfmt(Value::Str("")), "\"\"");
267 assert_eq!(field_logfmt(Value::Str("plain")), "plain");
268 assert_eq!(field_logfmt(Value::Str("a b")), "\"a b\"");
269 assert_eq!(field_logfmt(Value::Str("a\"b")), "\"a\\\"b\"");
270 assert_eq!(field_logfmt(Value::Str("a=b")), "\"a=b\"");
271 assert_eq!(field_logfmt(Value::Str("a\\b")), "\"a\\\\b\"");
272 assert_eq!(field_logfmt(Value::Char('\n')), "\"\\n\"");
273 assert_eq!(field_logfmt(Value::Char('x')), "x");
274 assert_eq!(field_logfmt(Value::Bytes(&[])), "\"\"");
275 assert_eq!(field_logfmt(Value::Bytes(&[1, 255])), "01ff");
276 assert_eq!(field_logfmt(Value::Display(&42)), "42");
277 assert_eq!(field_logfmt(Value::Display(&"two words")), "\"two words\"");
278 assert_eq!(field_logfmt(Value::Debug(&"q")), "\"\\\"q\\\"\"");
279 let e = std::io::Error::other("boom");
280 assert_eq!(field_logfmt(Value::Error(&e)), "boom");
281 }
282
283 #[test]
284 fn test_logfmt_control_characters_are_escaped_and_quoted() {
285 assert_eq!(field_logfmt(Value::Str("\u{1b}[31m")), "\"\\u001b[31m\"");
286 assert_eq!(field_logfmt(Value::Str("x\u{9b}31m")), "\"x\\u009b31m\"");
287 assert_eq!(field_logfmt(Value::Str("a\u{7f}")), "\"a\\u007f\"");
288 assert_eq!(field_logfmt(Value::Str("a\u{2028}b")), "\"a\\u2028b\"");
289 assert_eq!(field_logfmt(Value::Str("tab\there")), "\"tab\\there\"");
290 }
291
292 #[test]
293 fn test_logfmt_keys_are_sanitized_and_reserved_keys_prefixed() {
294 let fields = [
295 Field::new("the key", 1_u32),
296 Field::new("k\u{1b}[0m", 2_u32),
297 Field::new("", 3_u32),
298 Field::new("a=b", 4_u32),
299 Field::new("level", "fake"),
300 ];
301 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
302 let out = fmt_one(LogfmtFormat::new(), &record);
303 assert_eq!(
304 out,
305 "level=info target=t message=m the_key=1 k_[0m=2 _=3 a_b=4 fields.level=fake\n"
306 );
307 }
308
309 #[test]
310 fn test_logfmt_message_and_target_are_quoted_when_needed() {
311 let record = Record::new(Metadata::new(Level::Info, "my app"), "hi there", &[]);
312 assert_eq!(
313 fmt_one(LogfmtFormat::new(), &record),
314 "level=info target=\"my app\" message=\"hi there\"\n"
315 );
316 let record = Record::new(Metadata::new(Level::Info, ""), "", &[]);
317 assert_eq!(
318 fmt_one(LogfmtFormat::new(), &record),
319 "level=info target=\"\" message=\"\"\n"
320 );
321 }
322}