1use crate::color::Color;
11use crate::console::{ConsoleOptions, RenderResult, Renderable};
12use crate::segment::Segment;
13use crate::style::Style;
14use crate::table::{Column, Table};
15
16#[derive(Debug, Clone)]
44pub struct LogRender {
45 show_time: bool,
47 show_level: bool,
49 show_path: bool,
51 time_format: String,
53 level_width: usize,
55 omit_repeated_times: bool,
57 last_time: Option<String>,
59}
60
61impl LogRender {
62 pub fn new() -> Self {
64 Self {
65 show_time: true,
66 show_level: true,
67 show_path: true,
68 time_format: "[%x %X]".to_string(),
69 level_width: 8,
70 omit_repeated_times: true,
71 last_time: None,
72 }
73 }
74
75 pub fn show_time(mut self, value: bool) -> Self {
77 self.show_time = value;
78 self
79 }
80
81 pub fn show_level(mut self, value: bool) -> Self {
83 self.show_level = value;
84 self
85 }
86
87 pub fn show_path(mut self, value: bool) -> Self {
89 self.show_path = value;
90 self
91 }
92
93 pub fn time_format(mut self, format: impl Into<String>) -> Self {
95 self.time_format = format.into();
96 self
97 }
98
99 pub fn level_width(mut self, width: usize) -> Self {
101 self.level_width = width;
102 self
103 }
104
105 pub fn omit_repeated_times(mut self, value: bool) -> Self {
107 self.omit_repeated_times = value;
108 self
109 }
110
111 pub fn get_level_style(level: &str) -> Style {
115 use crate::theme::default_theme;
116 let theme = default_theme();
117 let key = format!("logging.level.{}", level.to_lowercase());
118 theme
119 .get(&key)
120 .cloned()
121 .unwrap_or_else(|| match level.to_lowercase().as_str() {
122 "debug" => Style::new().color(
123 crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
124 ),
125 "info" => Style::new().color(
126 crate::color::Color::parse("bright_cyan").unwrap_or_else(|_| Color::default()),
127 ),
128 "warning" => Style::new().color(
129 crate::color::Color::parse("bright_yellow")
130 .unwrap_or_else(|_| Color::default()),
131 ),
132 "error" => Style::new()
133 .color(
134 crate::color::Color::parse("bright_red")
135 .unwrap_or_else(|_| Color::default()),
136 )
137 .bold(true),
138 "critical" => Style::new()
139 .color(crate::color::Color::parse("red").unwrap_or_else(|_| Color::default()))
140 .bold(true)
141 .reverse(true),
142 _ => Style::new(),
143 })
144 }
145
146 pub fn get_time_style() -> Style {
148 Style::new()
149 .color(crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()))
150 }
151
152 pub fn get_message_style() -> Style {
154 Style::new()
155 }
156
157 pub fn get_path_style() -> Style {
159 Style::new()
160 .color(crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()))
161 }
162
163 pub fn render_log(
172 &mut self,
173 time: Option<&str>,
174 level: &str,
175 message: &str,
176 path: Option<&str>,
177 line_no: Option<u32>,
178 ) -> LogRecord {
179 let time_str = if self.show_time {
181 let ts = time.unwrap_or("");
182 if self.omit_repeated_times {
183 if let Some(ref last) = self.last_time {
184 if last == ts {
185 "".to_string()
186 } else {
187 self.last_time = Some(ts.to_string());
188 ts.to_string()
189 }
190 } else {
191 self.last_time = Some(ts.to_string());
192 ts.to_string()
193 }
194 } else {
195 ts.to_string()
196 }
197 } else {
198 String::new()
199 };
200
201 let path_str = if self.show_path {
203 match (path, line_no) {
204 (Some(p), Some(l)) => format!("{p}:{l}"),
205 (Some(p), None) => p.to_string(),
206 (None, Some(l)) => format!("<unknown>:{l}"),
207 (None, None) => String::new(),
208 }
209 } else {
210 String::new()
211 };
212
213 let padded_level = if self.show_level {
215 format!("{level:>width$}", width = self.level_width)
216 } else {
217 String::new()
218 };
219
220 LogRecord {
221 time: time_str,
222 level: padded_level,
223 message: message.to_string(),
224 path: path_str,
225 show_time: self.show_time,
226 show_level: self.show_level,
227 show_path: self.show_path,
228 }
229 }
230
231 #[allow(clippy::type_complexity)]
233 pub fn render_batch(
234 &mut self,
235 records: &[(Option<&str>, &str, &str, Option<&str>, Option<u32>)],
236 ) -> LogTable {
237 let rendered: Vec<LogRecord> = records
238 .iter()
239 .map(|(time, level, msg, path, line)| self.render_log(*time, level, msg, *path, *line))
240 .collect();
241 LogTable { records: rendered }
242 }
243
244 pub fn reset_time_cache(&mut self) {
246 self.last_time = None;
247 }
248}
249
250impl Default for LogRender {
251 fn default() -> Self {
252 Self::new()
253 }
254}
255
256#[derive(Debug, Clone)]
262pub struct LogRecord {
263 time: String,
264 level: String,
265 message: String,
266 path: String,
267 show_time: bool,
268 show_level: bool,
269 show_path: bool,
270}
271
272impl Renderable for LogRecord {
273 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
274 let time_style = LogRender::get_time_style();
275 let level_style = LogRender::get_level_style(self.level.trim());
276 let msg_style = LogRender::get_message_style();
277 let path_style = LogRender::get_path_style();
278
279 let mut line: Vec<Segment> = Vec::new();
280
281 if self.show_time && !self.time.is_empty() {
282 line.push(Segment::styled(&self.time, time_style.clone()));
283 line.push(Segment::new(" "));
284 }
285
286 if self.show_level && !self.level.is_empty() {
287 line.push(Segment::styled(&self.level, level_style));
288 line.push(Segment::new(" "));
289 }
290
291 line.push(Segment::styled(&self.message, msg_style));
292
293 if self.show_path && !self.path.is_empty() {
294 line.push(Segment::new(" "));
295 line.push(Segment::styled(&self.path, path_style));
296 }
297
298 line.push(Segment::line());
299
300 RenderResult {
301 lines: vec![line],
302 items: Vec::new(),
303 }
304 }
305}
306
307#[derive(Debug, Clone)]
313pub struct LogTable {
314 records: Vec<LogRecord>,
315}
316
317impl Renderable for LogTable {
318 fn render(&self, options: &ConsoleOptions) -> RenderResult {
319 if self.records.is_empty() {
320 return RenderResult {
321 lines: Vec::new(),
322 items: Vec::new(),
323 };
324 }
325
326 let mut table = Table::new();
327 table.show_header = false;
328 table.show_edge = false;
329 table.show_lines = false;
330
331 let first = &self.records[0];
333 if first.show_time {
334 table.add_column(Column::new("Time"));
335 }
336 if first.show_level {
337 table.add_column(Column::new("Level"));
338 }
339 table.add_column(Column::new("Message"));
340 if first.show_path {
341 table.add_column(Column::new("Path"));
342 }
343
344 for record in &self.records {
345 let mut cells: Vec<crate::table::Cell> = Vec::new();
346
347 if record.show_time {
348 let time_str = if record.time.is_empty() {
349 String::new()
350 } else {
351 LogRender::get_time_style().render(&record.time)
352 };
353 cells.push(crate::table::Cell::new(time_str));
354 }
355
356 if record.show_level {
357 let level_str =
358 LogRender::get_level_style(record.level.trim()).render(&record.level);
359 cells.push(crate::table::Cell::new(level_str));
360 }
361
362 cells.push(crate::table::Cell::new(
363 LogRender::get_message_style().render(&record.message),
364 ));
365
366 if record.show_path && !record.path.is_empty() {
367 cells.push(crate::table::Cell::new(
368 LogRender::get_path_style().render(&record.path),
369 ));
370 }
371
372 table.add_row(cells);
373 }
374
375 table.render(options)
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use super::*;
382
383 #[test]
384 fn test_log_render_defaults() {
385 let lr = LogRender::new();
386 assert!(lr.show_time);
387 assert!(lr.show_level);
388 assert!(lr.show_path);
389 }
390
391 #[test]
392 fn test_log_render_builder() {
393 let lr = LogRender::new()
394 .show_time(false)
395 .show_level(false)
396 .show_path(false)
397 .level_width(10)
398 .omit_repeated_times(false);
399 assert!(!lr.show_time);
400 assert!(!lr.show_level);
401 assert!(!lr.show_path);
402 assert_eq!(lr.level_width, 10);
403 assert!(!lr.omit_repeated_times);
404 }
405
406 #[test]
407 fn test_log_render_single() {
408 let mut lr = LogRender::new();
409 let record = lr.render_log(
410 Some("2024-01-15 10:30:00"),
411 "INFO",
412 "Hello world",
413 Some("src/main.rs"),
414 Some(42),
415 );
416 let opts = ConsoleOptions::default();
417 let result = record.render(&opts);
418 let ansi = result.to_ansi();
419 assert!(ansi.contains("Hello world"));
420 }
421
422 #[test]
423 fn test_log_render_no_path() {
424 let mut lr = LogRender::new().show_path(false);
425 let record = lr.render_log(None, "DEBUG", "debug message", None, None);
426 let opts = ConsoleOptions::default();
427 let result = record.render(&opts);
428 let ansi = result.to_ansi();
429 assert!(ansi.contains("debug message"));
430 }
431
432 #[test]
433 fn test_log_render_level_styles() {
434 let debug_style = LogRender::get_level_style("DEBUG");
435 let info_style = LogRender::get_level_style("INFO");
436 let warn_style = LogRender::get_level_style("WARNING");
437 let error_style = LogRender::get_level_style("ERROR");
438 let critical_style = LogRender::get_level_style("CRITICAL");
439 assert!(!debug_style.is_null() || true);
441 assert!(!info_style.is_null() || true);
442 assert!(!warn_style.is_null() || true);
443 assert!(!error_style.is_null() || true);
444 assert!(!critical_style.is_null() || true);
445 }
446
447 #[test]
448 fn test_log_render_batch() {
449 let mut lr = LogRender::new().show_path(false).show_time(false);
450 let records = vec![
451 (None, "INFO", "first", None, None),
452 (None, "ERROR", "second", None, None),
453 ];
454 let table = lr.render_batch(&records);
455 let opts = ConsoleOptions::default();
456 let result = table.render(&opts);
457 let ansi = result.to_ansi();
458 assert!(ansi.contains("first"));
459 assert!(ansi.contains("second"));
460 }
461
462 #[test]
463 fn test_log_render_time_dedup() {
464 let mut lr = LogRender::new().show_path(false).show_level(false);
465 let r1 = lr.render_log(Some("2024-01-01"), "INFO", "msg1", None, None);
466 let r2 = lr.render_log(Some("2024-01-01"), "INFO", "msg2", None, None);
467 assert!(!r1.time.is_empty());
469 assert!(r2.time.is_empty());
470 }
471
472 #[test]
473 fn test_log_render_reset_cache() {
474 let mut lr = LogRender::new().show_path(false).show_level(false);
475 lr.render_log(Some("ts"), "INFO", "msg1", None, None);
476 lr.reset_time_cache();
477 let r = lr.render_log(Some("ts"), "INFO", "msg2", None, None);
478 assert!(!r.time.is_empty());
480 }
481}