1use core::fmt;
11use core::fmt::Write as _;
12
13use super::Format;
14use super::escape::{
15 JsonEscaper, QuoteScan, RESERVED_PREFIX, is_reserved_key, 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 if QuoteScan::needs_quotes(&lazy) {
149 w.write_char('"')?;
150 write!(JsonEscaper(&mut *w), "{lazy}")?;
151 w.write_char('"')
152 } else {
153 write!(w, "{lazy}")
154 }
155 }
156 }
157}
158
159#[cfg(all(test, feature = "std"))]
160mod tests {
161 use super::*;
162 use crate::Level;
163 use crate::record::{Field, Metadata};
164
165 fn fmt_one(fmt: LogfmtFormat, record: &Record<'_>) -> String {
166 let mut s = String::new();
167 fmt.write_record_generic(record, &mut s).unwrap();
168 let mut d = String::new();
169 fmt.write_record(record, &mut d).unwrap();
170 assert_eq!(s, d);
171 s
172 }
173
174 fn field_logfmt(v: Value<'_>) -> String {
175 let fields = [Field::new("v", v)];
176 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
177 let out = fmt_one(LogfmtFormat::new(), &record);
178 let start = out.find(" v=").unwrap() + 3;
179 out[start..out.len() - 1].to_string()
180 }
181
182 #[test]
183 fn test_logfmt_simple_record() {
184 let record = Record::new(Metadata::new(Level::Info, "app"), "hello", &[]);
185 assert_eq!(
186 fmt_one(LogfmtFormat::new(), &record),
187 "level=info target=app message=hello\n"
188 );
189 }
190
191 #[test]
192 fn test_logfmt_metadata_and_timestamps() {
193 let meta = Metadata::new(Level::Debug, "a")
194 .with_location("src/x.rs", 9)
195 .with_timestamp(1_700_000_000_000_000_000);
196 let record = Record::new(meta, "m", &[]);
197 assert_eq!(
198 fmt_one(LogfmtFormat::new(), &record),
199 "timestamp=1700000000000000000 level=debug target=a message=m file=src/x.rs line=9\n"
200 );
201 assert!(
202 fmt_one(LogfmtFormat::new().timestamp_rfc3339(true), &record)
203 .starts_with("timestamp=2023-11-14T22:13:20.000000000Z level=debug")
204 );
205 }
206
207 #[test]
208 fn test_logfmt_values_render_per_variant() {
209 assert_eq!(field_logfmt(Value::Null), "");
210 assert_eq!(field_logfmt(Value::Bool(true)), "true");
211 assert_eq!(field_logfmt(Value::Bool(false)), "false");
212 assert_eq!(field_logfmt(Value::I64(-1)), "-1");
213 assert_eq!(field_logfmt(Value::U64(1)), "1");
214 assert_eq!(field_logfmt(Value::I128(-2)), "-2");
215 assert_eq!(field_logfmt(Value::U128(2)), "2");
216 assert_eq!(field_logfmt(Value::F64(1.0)), "1.0");
217 assert_eq!(field_logfmt(Value::F64(f64::NAN)), "NaN");
218 assert_eq!(field_logfmt(Value::F64(f64::INFINITY)), "inf");
219 assert_eq!(field_logfmt(Value::Str("")), "\"\"");
220 assert_eq!(field_logfmt(Value::Str("plain")), "plain");
221 assert_eq!(field_logfmt(Value::Str("a b")), "\"a b\"");
222 assert_eq!(field_logfmt(Value::Str("a\"b")), "\"a\\\"b\"");
223 assert_eq!(field_logfmt(Value::Str("a=b")), "\"a=b\"");
224 assert_eq!(field_logfmt(Value::Str("a\\b")), "\"a\\\\b\"");
225 assert_eq!(field_logfmt(Value::Char('\n')), "\"\\n\"");
226 assert_eq!(field_logfmt(Value::Char('x')), "x");
227 assert_eq!(field_logfmt(Value::Bytes(&[])), "\"\"");
228 assert_eq!(field_logfmt(Value::Bytes(&[1, 255])), "01ff");
229 assert_eq!(field_logfmt(Value::Display(&42)), "42");
230 assert_eq!(field_logfmt(Value::Display(&"two words")), "\"two words\"");
231 assert_eq!(field_logfmt(Value::Debug(&"q")), "\"\\\"q\\\"\"");
232 let e = std::io::Error::other("boom");
233 assert_eq!(field_logfmt(Value::Error(&e)), "boom");
234 }
235
236 #[test]
237 fn test_logfmt_control_characters_are_escaped_and_quoted() {
238 assert_eq!(field_logfmt(Value::Str("\u{1b}[31m")), "\"\\u001b[31m\"");
239 assert_eq!(field_logfmt(Value::Str("x\u{9b}31m")), "\"x\\u009b31m\"");
240 assert_eq!(field_logfmt(Value::Str("a\u{7f}")), "\"a\\u007f\"");
241 assert_eq!(field_logfmt(Value::Str("a\u{2028}b")), "\"a\\u2028b\"");
242 assert_eq!(field_logfmt(Value::Str("tab\there")), "\"tab\\there\"");
243 }
244
245 #[test]
246 fn test_logfmt_keys_are_sanitized_and_reserved_keys_prefixed() {
247 let fields = [
248 Field::new("the key", 1_u32),
249 Field::new("k\u{1b}[0m", 2_u32),
250 Field::new("", 3_u32),
251 Field::new("a=b", 4_u32),
252 Field::new("level", "fake"),
253 ];
254 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
255 let out = fmt_one(LogfmtFormat::new(), &record);
256 assert_eq!(
257 out,
258 "level=info target=t message=m the_key=1 k_[0m=2 _=3 a_b=4 fields.level=fake\n"
259 );
260 }
261
262 #[test]
263 fn test_logfmt_message_and_target_are_quoted_when_needed() {
264 let record = Record::new(Metadata::new(Level::Info, "my app"), "hi there", &[]);
265 assert_eq!(
266 fmt_one(LogfmtFormat::new(), &record),
267 "level=info target=\"my app\" message=\"hi there\"\n"
268 );
269 let record = Record::new(Metadata::new(Level::Info, ""), "", &[]);
270 assert_eq!(
271 fmt_one(LogfmtFormat::new(), &record),
272 "level=info target=\"\" message=\"\"\n"
273 );
274 }
275}