1use std::cell::RefCell;
16use std::sync::Arc;
17
18use crate::console::{Console, ConsoleOptions, Overflow};
19use crate::containers::Renderables;
20use crate::measure::Measurement;
21use crate::protocol::Renderable;
22use crate::segment::Segment;
23use crate::style::{Style, StyleType};
24use crate::table::{Cell, Table};
25use crate::text::Text;
26
27pub struct LogRender {
29 show_time: bool,
30 show_level: bool,
31 show_path: bool,
32 omit_repeated_times: bool,
33 level_width: Option<usize>,
34 last_time: RefCell<Option<Text>>,
35}
36
37impl Default for LogRender {
38 fn default() -> Self {
39 LogRender {
40 show_time: true,
41 show_level: false,
42 show_path: true,
43 omit_repeated_times: true,
44 level_width: Some(8),
45 last_time: RefCell::new(None),
46 }
47 }
48}
49
50impl LogRender {
51 pub fn new() -> Self {
54 LogRender::default()
55 }
56
57 pub fn show_time(mut self, show: bool) -> Self {
59 self.show_time = show;
60 self
61 }
62
63 pub fn show_level(mut self, show: bool) -> Self {
65 self.show_level = show;
66 self
67 }
68
69 pub fn show_path(mut self, show: bool) -> Self {
71 self.show_path = show;
72 self
73 }
74
75 pub fn omit_repeated_times(mut self, omit: bool) -> Self {
78 self.omit_repeated_times = omit;
79 self
80 }
81
82 pub fn level_width(mut self, width: Option<usize>) -> Self {
85 self.level_width = width;
86 self
87 }
88
89 #[allow(clippy::too_many_arguments)]
96 pub fn render(
97 &self,
98 console: &Console,
99 message: Text,
100 time: Option<Text>,
101 level: Text,
102 path: Option<&str>,
103 line_no: Option<u32>,
104 link_path: Option<&str>,
105 ) -> Table {
106 self.render_cell(
107 console,
108 Cell::Text(message),
109 time,
110 level,
111 path,
112 line_no,
113 link_path,
114 )
115 }
116
117 #[allow(clippy::too_many_arguments)]
123 pub fn render_renderables(
124 &self,
125 console: &Console,
126 renderables: Vec<Arc<dyn Renderable + Send + Sync>>,
127 time: Option<Text>,
128 level: Text,
129 path: Option<&str>,
130 line_no: Option<u32>,
131 link_path: Option<&str>,
132 ) -> Table {
133 self.render_cell(
134 console,
135 Cell::Renderable(Arc::new(Renderables::new(renderables))),
136 time,
137 level,
138 path,
139 line_no,
140 link_path,
141 )
142 }
143
144 #[allow(clippy::too_many_arguments)]
145 fn render_cell(
146 &self,
147 console: &Console,
148 message: Cell,
149 time: Option<Text>,
150 level: Text,
151 path: Option<&str>,
152 line_no: Option<u32>,
153 link_path: Option<&str>,
154 ) -> Table {
155 let style = |name: &str| {
156 console
157 .get_style(&StyleType::from(name))
158 .unwrap_or_default()
159 };
160 let mut output = Table::grid().padding(0, 1, 0, 1).expand(true);
161 if self.show_time {
162 output.add_column("").column_style(style("log.time"));
163 }
164 if self.show_level {
165 output.add_column("").column_style(style("log.level"));
166 if let Some(width) = self.level_width {
167 output.column_width(width);
168 }
169 }
170 output
171 .add_column("")
172 .column_ratio(1)
173 .column_style(style("log.message"))
174 .column_overflow(Overflow::Fold);
175 let path = path
176 .filter(|_| self.show_path)
177 .filter(|path| !path.is_empty());
178 if path.is_some() {
179 output.add_column("").column_style(style("log.path"));
180 }
181
182 let mut row: Vec<Cell> = Vec::new();
183 if self.show_time {
184 let display = time.unwrap_or_default();
185 let mut last = self.last_time.borrow_mut();
186 let repeated = last.as_ref().is_some_and(|last| same_text(last, &display));
187 if repeated && self.omit_repeated_times {
188 row.push(Cell::Text(Text::new(
189 " ".repeat(display.plain().chars().count()),
190 )));
191 } else {
192 row.push(Cell::Text(display.clone()));
193 *last = Some(display);
194 }
195 }
196 if self.show_level {
197 row.push(Cell::Text(level));
198 }
199 row.push(message);
200 if let Some(path) = path {
201 let link = |target: String| Style::new().with_link(target);
202 let mut path_text = Text::new("");
203 path_text.append(
204 path,
205 link_path.map(|link_path| link(format!("file://{link_path}")).into()),
206 );
207 if let Some(line_no) = line_no.filter(|line| *line > 0) {
208 path_text.append(":", None);
209 path_text.append(
210 &line_no.to_string(),
211 link_path.map(|link_path| link(format!("file://{link_path}#{line_no}")).into()),
212 );
213 }
214 row.push(Cell::Text(path_text));
215 }
216 output.add_row_cells(row);
217 output
218 }
219}
220
221fn same_text(a: &Text, b: &Text) -> bool {
223 a.plain() == b.plain() && a.spans() == b.spans()
224}
225
226pub fn level_text(name: &str) -> Text {
229 Text::styled(
230 format!("{name:<8}"),
231 format!("logging.level.{}", name.to_lowercase()),
232 )
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum LogLevel {
238 Trace,
239 Debug,
240 Info,
241 Warn,
242 Error,
243}
244
245impl LogLevel {
246 pub fn name(self) -> &'static str {
249 match self {
250 LogLevel::Trace => "TRACE",
251 LogLevel::Debug => "DEBUG",
252 LogLevel::Info => "INFO",
253 LogLevel::Warn => "WARNING",
254 LogLevel::Error => "ERROR",
255 }
256 }
257
258 pub fn text(self) -> Text {
260 match self {
261 LogLevel::Trace => Text::styled(format!("{:<8}", "TRACE"), "logging.level.notset"),
262 level => level_text(level.name()),
263 }
264 }
265}
266
267pub struct LogRecord {
271 level: LogLevel,
272 message: String,
273 time: Option<String>,
274 path: Option<String>,
275 line_no: Option<u32>,
276}
277
278impl LogRecord {
279 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
281 LogRecord {
282 level,
283 message: message.into(),
284 time: None,
285 path: None,
286 line_no: None,
287 }
288 }
289
290 pub fn time(mut self, time: impl Into<String>) -> Self {
292 self.time = Some(time.into());
293 self
294 }
295
296 pub fn path(mut self, path: impl Into<String>) -> Self {
298 self.path = Some(path.into());
299 self
300 }
301
302 pub fn line(mut self, line: u32) -> Self {
304 self.line_no = Some(line);
305 self
306 }
307
308 fn table(&self, console: &Console) -> Table {
309 LogRender::new()
310 .show_level(true)
311 .show_time(self.time.is_some())
312 .render(
313 console,
314 Text::new(self.message.clone()),
315 self.time.as_deref().map(Text::new),
316 self.level.text(),
317 self.path.as_deref(),
318 self.line_no,
319 None,
320 )
321 }
322}
323
324impl Renderable for LogRecord {
325 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
326 self.table(console).rich_render(console, options)
327 }
328
329 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
330 self.table(console).measure(console, options)
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use crate::color::ColorSystem;
338
339 fn console() -> Console {
340 Console::builder()
341 .force_terminal(true)
342 .color_system(Some(ColorSystem::Truecolor))
343 .width(40)
344 .highlight(false)
345 .build()
346 }
347
348 #[test]
349 fn a_repeated_time_is_blanked() {
350 let console = console();
351 let render = LogRender::new();
352 let first = console.render_to_string(&render.render(
353 &console,
354 Text::new("one"),
355 Some(Text::new("[12:00]")),
356 Text::new(""),
357 None,
358 None,
359 None,
360 ));
361 let second = console.render_to_string(&render.render(
362 &console,
363 Text::new("two"),
364 Some(Text::new("[12:00]")),
365 Text::new(""),
366 None,
367 None,
368 None,
369 ));
370 assert!(first.contains("[12:00]"), "{first:?}");
371 assert!(!second.contains("[12:00]"), "{second:?}");
372 assert!(second.contains("two"));
373 }
374
375 #[test]
376 fn warn_uses_pythons_level_name() {
377 let out = console().render_to_string(&LogRecord::new(LogLevel::Warn, "low disk"));
378 assert!(out.contains("\x1b[33mWARNING \x1b[0m"), "{out:?}");
379 assert!(out.contains("low disk"));
380 }
381}