1use std::cell::RefCell;
16
17use crate::console::{Console, ConsoleOptions, Overflow};
18use crate::measure::Measurement;
19use crate::protocol::Renderable;
20use crate::segment::Segment;
21use crate::style::{Style, StyleType};
22use crate::table::Table;
23use crate::text::Text;
24
25pub struct LogRender {
27 show_time: bool,
28 show_level: bool,
29 show_path: bool,
30 omit_repeated_times: bool,
31 level_width: Option<usize>,
32 last_time: RefCell<Option<Text>>,
33}
34
35impl Default for LogRender {
36 fn default() -> Self {
37 LogRender {
38 show_time: true,
39 show_level: false,
40 show_path: true,
41 omit_repeated_times: true,
42 level_width: Some(8),
43 last_time: RefCell::new(None),
44 }
45 }
46}
47
48impl LogRender {
49 pub fn new() -> Self {
52 LogRender::default()
53 }
54
55 pub fn show_time(mut self, show: bool) -> Self {
57 self.show_time = show;
58 self
59 }
60
61 pub fn show_level(mut self, show: bool) -> Self {
63 self.show_level = show;
64 self
65 }
66
67 pub fn show_path(mut self, show: bool) -> Self {
69 self.show_path = show;
70 self
71 }
72
73 pub fn omit_repeated_times(mut self, omit: bool) -> Self {
76 self.omit_repeated_times = omit;
77 self
78 }
79
80 pub fn level_width(mut self, width: Option<usize>) -> Self {
83 self.level_width = width;
84 self
85 }
86
87 #[allow(clippy::too_many_arguments)]
91 pub fn render(
92 &self,
93 console: &Console,
94 message: Text,
95 time: Option<Text>,
96 level: Text,
97 path: Option<&str>,
98 line_no: Option<u32>,
99 link_path: Option<&str>,
100 ) -> Table {
101 let style = |name: &str| {
102 console
103 .get_style(&StyleType::from(name))
104 .unwrap_or_default()
105 };
106 let mut output = Table::grid().padding(0, 1, 0, 1).expand(true);
107 if self.show_time {
108 output.add_column("").column_style(style("log.time"));
109 }
110 if self.show_level {
111 output.add_column("").column_style(style("log.level"));
112 if let Some(width) = self.level_width {
113 output.column_width(width);
114 }
115 }
116 output
117 .add_column("")
118 .column_ratio(1)
119 .column_style(style("log.message"))
120 .column_overflow(Overflow::Fold);
121 let path = path
122 .filter(|_| self.show_path)
123 .filter(|path| !path.is_empty());
124 if path.is_some() {
125 output.add_column("").column_style(style("log.path"));
126 }
127
128 let mut row = Vec::new();
129 if self.show_time {
130 let display = time.unwrap_or_default();
131 let mut last = self.last_time.borrow_mut();
132 let repeated = last.as_ref().is_some_and(|last| same_text(last, &display));
133 if repeated && self.omit_repeated_times {
134 row.push(Text::new(" ".repeat(display.plain().chars().count())));
135 } else {
136 row.push(display.clone());
137 *last = Some(display);
138 }
139 }
140 if self.show_level {
141 row.push(level);
142 }
143 row.push(message);
144 if let Some(path) = path {
145 let link = |target: String| Style::new().with_link(target);
146 let mut path_text = Text::new("");
147 path_text.append(
148 path,
149 link_path.map(|link_path| link(format!("file://{link_path}")).into()),
150 );
151 if let Some(line_no) = line_no.filter(|line| *line > 0) {
152 path_text.append(":", None);
153 path_text.append(
154 &line_no.to_string(),
155 link_path.map(|link_path| link(format!("file://{link_path}#{line_no}")).into()),
156 );
157 }
158 row.push(path_text);
159 }
160 output.add_row_text(row);
161 output
162 }
163}
164
165fn same_text(a: &Text, b: &Text) -> bool {
167 a.plain() == b.plain() && a.spans() == b.spans()
168}
169
170pub fn level_text(name: &str) -> Text {
173 Text::styled(
174 format!("{name:<8}"),
175 format!("logging.level.{}", name.to_lowercase()),
176 )
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum LogLevel {
182 Trace,
183 Debug,
184 Info,
185 Warn,
186 Error,
187}
188
189impl LogLevel {
190 pub fn name(self) -> &'static str {
193 match self {
194 LogLevel::Trace => "TRACE",
195 LogLevel::Debug => "DEBUG",
196 LogLevel::Info => "INFO",
197 LogLevel::Warn => "WARNING",
198 LogLevel::Error => "ERROR",
199 }
200 }
201
202 pub fn text(self) -> Text {
204 match self {
205 LogLevel::Trace => Text::styled(format!("{:<8}", "TRACE"), "logging.level.notset"),
206 level => level_text(level.name()),
207 }
208 }
209}
210
211pub struct LogRecord {
215 level: LogLevel,
216 message: String,
217 time: Option<String>,
218 path: Option<String>,
219 line_no: Option<u32>,
220}
221
222impl LogRecord {
223 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
225 LogRecord {
226 level,
227 message: message.into(),
228 time: None,
229 path: None,
230 line_no: None,
231 }
232 }
233
234 pub fn time(mut self, time: impl Into<String>) -> Self {
236 self.time = Some(time.into());
237 self
238 }
239
240 pub fn path(mut self, path: impl Into<String>) -> Self {
242 self.path = Some(path.into());
243 self
244 }
245
246 pub fn line(mut self, line: u32) -> Self {
248 self.line_no = Some(line);
249 self
250 }
251
252 fn table(&self, console: &Console) -> Table {
253 LogRender::new()
254 .show_level(true)
255 .show_time(self.time.is_some())
256 .render(
257 console,
258 Text::new(self.message.clone()),
259 self.time.as_deref().map(Text::new),
260 self.level.text(),
261 self.path.as_deref(),
262 self.line_no,
263 None,
264 )
265 }
266}
267
268impl Renderable for LogRecord {
269 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
270 self.table(console).rich_render(console, options)
271 }
272
273 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
274 self.table(console).measure(console, options)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::color::ColorSystem;
282
283 fn console() -> Console {
284 Console::builder()
285 .force_terminal(true)
286 .color_system(Some(ColorSystem::Truecolor))
287 .width(40)
288 .highlight(false)
289 .build()
290 }
291
292 #[test]
293 fn a_repeated_time_is_blanked() {
294 let console = console();
295 let render = LogRender::new();
296 let first = console.render_to_string(&render.render(
297 &console,
298 Text::new("one"),
299 Some(Text::new("[12:00]")),
300 Text::new(""),
301 None,
302 None,
303 None,
304 ));
305 let second = console.render_to_string(&render.render(
306 &console,
307 Text::new("two"),
308 Some(Text::new("[12:00]")),
309 Text::new(""),
310 None,
311 None,
312 None,
313 ));
314 assert!(first.contains("[12:00]"), "{first:?}");
315 assert!(!second.contains("[12:00]"), "{second:?}");
316 assert!(second.contains("two"));
317 }
318
319 #[test]
320 fn warn_uses_pythons_level_name() {
321 let out = console().render_to_string(&LogRecord::new(LogLevel::Warn, "low disk"));
322 assert!(out.contains("\x1b[33mWARNING \x1b[0m"), "{out:?}");
323 assert!(out.contains("low disk"));
324 }
325}