1use core::fmt;
14
15use super::Format;
16use crate::record::Record;
17use crate::value::Value;
18
19#[derive(Debug, Clone, Copy)]
38pub struct HumanFormat {
39 show_target: bool,
40 show_source_location: bool,
41}
42
43impl Default for HumanFormat {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl HumanFormat {
50 pub const fn new() -> Self {
53 Self {
54 show_target: true,
55 show_source_location: false,
56 }
57 }
58
59 pub const fn show_target(mut self, enabled: bool) -> Self {
61 self.show_target = enabled;
62 self
63 }
64
65 pub const fn show_source_location(mut self, enabled: bool) -> Self {
68 self.show_source_location = enabled;
69 self
70 }
71}
72
73impl Format for HumanFormat {
74 fn write_record<W: fmt::Write + ?Sized>(
75 &self,
76 record: &Record<'_>,
77 writer: &mut W,
78 ) -> fmt::Result {
79 if let Some(ts) = record.metadata.timestamp_unix_nanos {
80 crate::format::rfc3339::write_rfc3339_nanos(ts, writer)?;
81 writer.write_char(' ')?;
82 }
83 write!(writer, "{:<5} ", record.metadata.level.as_str_upper())?;
85 if self.show_target {
86 writer.write_char('[')?;
87 write_line_safe(writer, record.metadata.target)?;
88 writer.write_str("] ")?;
89 }
90 write_line_safe(writer, record.message)?;
91 for field in record.all_fields() {
92 writer.write_char(' ')?;
93 write_line_safe(writer, field.key)?;
94 writer.write_char('=')?;
95 write_value(writer, field.value)?;
96 }
97 if self.show_source_location {
98 if let (Some(file), Some(line)) = (record.metadata.file, record.metadata.line) {
99 write!(writer, " ({file}:{line})")?;
100 }
101 }
102 writer.write_char('\n')
103 }
104}
105
106fn write_line_safe<W: fmt::Write + ?Sized>(w: &mut W, s: &str) -> fmt::Result {
110 for c in s.chars() {
111 match c {
112 '\n' => w.write_str("\\n")?,
113 '\r' => w.write_str("\\r")?,
114 '\t' => w.write_str("\\t")?,
115 c => w.write_char(c)?,
116 }
117 }
118 Ok(())
119}
120
121fn write_value<W: fmt::Write + ?Sized>(w: &mut W, v: Value<'_>) -> fmt::Result {
122 match v {
123 Value::Null => w.write_str("<null>"),
124 Value::Bool(b) => write!(w, "{b}"),
125 Value::I64(n) => write!(w, "{n}"),
126 Value::U64(n) => write!(w, "{n}"),
127 Value::F64(n) => write!(w, "{n}"),
128 Value::Str(s) => {
129 let needs_quote = s
130 .chars()
131 .any(|c| c.is_whitespace() || c == '"' || (c as u32) < 0x20);
132 if needs_quote {
133 w.write_char('"')?;
134 for c in s.chars() {
135 match c {
136 '"' => w.write_str("\\\"")?,
137 '\\' => w.write_str("\\\\")?,
138 '\n' => w.write_str("\\n")?,
139 '\r' => w.write_str("\\r")?,
140 '\t' => w.write_str("\\t")?,
141 c if (c as u32) < 0x20 => write!(w, "\\x{:02x}", c as u32)?,
142 c => w.write_char(c)?,
143 }
144 }
145 w.write_char('"')
146 } else {
147 w.write_str(s)
148 }
149 }
150 Value::Char(c) => write!(w, "{c}"),
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::record::{Field, Metadata};
158 use crate::Level;
159
160 fn fmt_one(fmt: HumanFormat, record: &Record<'_>) -> String {
161 let mut s = String::new();
162 fmt.write_record(record, &mut s).unwrap();
163 s
164 }
165
166 #[test]
167 fn level_is_uppercase_padded() {
168 let record = Record::new(Metadata::new(Level::Info, "app"), "hi", &[]);
169 let out = fmt_one(HumanFormat::new(), &record);
170 assert!(out.starts_with("INFO ["));
171 }
172
173 #[test]
174 fn target_can_be_hidden() {
175 let record = Record::new(Metadata::new(Level::Warn, "app"), "hi", &[]);
176 let out = fmt_one(HumanFormat::new().show_target(false), &record);
177 assert!(!out.contains('['));
178 assert!(out.contains("WARN hi"));
179 }
180
181 #[test]
182 fn fields_appended_to_line() {
183 let fields = [Field::new("port", Value::U64(80))];
184 let record = Record::new(Metadata::new(Level::Info, "app"), "ok", &fields);
185 let out = fmt_one(HumanFormat::new(), &record);
186 assert!(out.contains("ok port=80"));
187 }
188
189 #[test]
190 fn source_location_optional() {
191 let metadata = Metadata::new(Level::Info, "app").with_location("src/lib.rs", 42);
192 let record = Record::new(metadata, "ok", &[]);
193 let with = fmt_one(HumanFormat::new().show_source_location(true), &record);
194 let without = fmt_one(HumanFormat::new(), &record);
195 assert!(with.contains("(src/lib.rs:42)"), "{with}");
196 assert!(!without.contains("src/lib.rs"));
197 }
198
199 #[test]
200 fn timestamp_prepended_when_set() {
201 let metadata = Metadata::new(Level::Info, "app").with_timestamp(1_700_000_000_000_000_000);
202 let record = Record::new(metadata, "ok", &[]);
203 let out = fmt_one(HumanFormat::new(), &record);
204 assert!(out.starts_with("2023-11-14T"), "{out}");
205 }
206
207 #[test]
208 fn null_value_renders_as_placeholder() {
209 let fields = [Field::new("x", Value::Null)];
210 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
211 let out = fmt_one(HumanFormat::new(), &record);
212 assert!(out.contains("x=<null>"));
213 }
214}