1use crate::{Level, LogRecord};
10use std::io::{self, IsTerminal, Write};
11
12const TAG_WIDTH: usize = 24;
14
15pub fn format_console(record: &LogRecord) -> String {
19 let (month, day, hour, min, sec, ms) = decompose_ts(record.ts);
20 let level_code = record.level.code();
21 let tag_display = if record.tag.len() > TAG_WIDTH {
23 format!("{}…", &record.tag[..TAG_WIDTH - 1])
24 } else {
25 format!("{: <width$}", record.tag, width = TAG_WIDTH)
26 };
27 format!(
28 "{:02}-{:02} {:02}:{:02}:{:02}.{:03} {} {} {}\n",
29 month, day, hour, min, sec, ms, level_code, tag_display, record.msg
30 )
31}
32
33pub fn write_console(record: &LogRecord) -> io::Result<()> {
39 let line = format_console(record);
40 let mut stderr = io::stderr().lock();
41 let use_color = stderr.is_terminal() && std::env::var_os("NO_COLOR").is_none();
42 let color = match record.level {
43 Level::Warn => Some("\x1b[33m"),
44 Level::Error => Some("\x1b[31m"),
45 _ => None,
46 };
47 if use_color && let Some(color) = color {
48 stderr.write_all(color.as_bytes())?;
49 stderr.write_all(line.as_bytes())?;
50 stderr.write_all(b"\x1b[0m")
51 } else {
52 stderr.write_all(line.as_bytes())
53 }
54}
55
56fn decompose_ts(ts_ns: u64) -> (u32, u32, u32, u32, u32, u32) {
59 let secs = (ts_ns / 1_000_000_000) as i64;
61 let nanos_rem = (ts_ns % 1_000_000_000) as u32;
62 let ms = nanos_rem / 1_000_000;
63
64 #[cfg(unix)]
67 {
68 let local = localtime(secs);
69 let (year, month, day, hour, min, sec) = local;
70 let _ = year;
72 (month + 1, day, hour, min, sec, ms)
73 }
74 #[cfg(not(unix))]
75 {
76 let total_secs = secs;
78 let day_secs = total_secs % 86400;
79 let hour = (day_secs / 3600) as u32;
80 let min = ((day_secs % 3600) / 60) as u32;
81 let sec = (day_secs % 60) as u32;
82 let month = 1;
83 let day = 1;
84 (month, day, hour, min, sec, ms)
85 }
86}
87
88#[cfg(unix)]
89fn localtime(secs: i64) -> (i32, u32, u32, u32, u32, u32) {
90 use std::mem::MaybeUninit;
91 let ts = libc::time_t::try_from(secs).unwrap_or(0);
92 let mut tm: MaybeUninit<libc::tm> = MaybeUninit::uninit();
93 let tm_ptr = unsafe {
95 libc::localtime_r(&ts, tm.as_mut_ptr());
96 tm.assume_init()
97 };
98 (
99 tm_ptr.tm_year,
100 tm_ptr.tm_mon as u32,
101 tm_ptr.tm_mday as u32,
102 tm_ptr.tm_hour as u32,
103 tm_ptr.tm_min as u32,
104 tm_ptr.tm_sec as u32,
105 )
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111 use crate::Level;
112
113 #[test]
114 fn format_includes_level_code_and_tag() {
115 let rec = LogRecord {
116 ts: 0,
117 level: Level::Warn,
118 tag: "atlas".into(),
119 msg: "test message".into(),
120 };
121 let line = format_console(&rec);
122 assert!(line.contains(" W "), "expected W level code, got: {line}");
123 assert!(line.contains("atlas"), "expected tag, got: {line}");
124 assert!(line.contains("test message"), "expected msg, got: {line}");
125 assert!(line.ends_with('\n'), "should end with newline");
126 }
127
128 #[test]
129 fn tag_padded_to_fixed_width() {
130 let rec_short = LogRecord {
131 ts: 0,
132 level: Level::Info,
133 tag: "a".into(),
134 msg: "msg".into(),
135 };
136 let rec_long = LogRecord {
137 ts: 0,
138 level: Level::Info,
139 tag: "this_tag_is_way_too_long_for_display".into(),
140 msg: "msg".into(),
141 };
142 let short = format_console(&rec_short);
143 let long = format_console(&rec_long);
144 let after_level: Vec<&str> = short.splitn(2, " I ").collect();
147 assert_eq!(after_level.len(), 2);
148 let rest = after_level[1]; assert!(
150 rest.starts_with(&format!("{: <width$} ", "a", width = TAG_WIDTH)),
151 "tag 'a' should be padded to {TAG_WIDTH} chars, got rest: {rest:?}"
152 );
153 assert!(
155 long.contains('…'),
156 "long tag should be truncated with '…', got: {long}"
157 );
158 }
159
160 #[test]
161 fn level_codes_in_output() {
162 for level in [Level::Debug, Level::Info, Level::Warn, Level::Error] {
163 let rec = LogRecord {
164 ts: 0,
165 level,
166 tag: "test".into(),
167 msg: String::new(),
168 };
169 let line = format_console(&rec);
170 let expected_code = format!(" {} ", level.code());
171 assert!(
172 line.contains(&expected_code),
173 "level {level:?}: expected '{expected_code}' in '{line}'"
174 );
175 }
176 }
177
178 #[test]
179 fn write_console_does_not_panic() {
180 let rec = LogRecord {
181 ts: 1765432100123456789,
182 level: Level::Info,
183 tag: "test".into(),
184 msg: "hello".into(),
185 };
186 write_console(&rec).unwrap();
188 }
189}