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.get(&key).cloned().unwrap_or_else(|| match level.to_lowercase().as_str() {
119 "debug" => Style::new().color(
120 crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
121 ),
122 "info" => Style::new().color(
123 crate::color::Color::parse("bright_cyan").unwrap_or_else(|_| Color::default()),
124 ),
125 "warning" => Style::new().color(
126 crate::color::Color::parse("bright_yellow").unwrap_or_else(|_| Color::default()),
127 ),
128 "error" => Style::new().color(
129 crate::color::Color::parse("bright_red").unwrap_or_else(|_| Color::default()),
130 ).bold(true),
131 "critical" => Style::new().color(
132 crate::color::Color::parse("red").unwrap_or_else(|_| Color::default()),
133 ).bold(true).reverse(true),
134 _ => Style::new(),
135 })
136 }
137
138 pub fn get_time_style() -> Style {
140 Style::new().color(
141 crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
142 )
143 }
144
145 pub fn get_message_style() -> Style {
147 Style::new()
148 }
149
150 pub fn get_path_style() -> Style {
152 Style::new().color(
153 crate::color::Color::parse("bright_black").unwrap_or_else(|_| Color::default()),
154 )
155 }
156
157 pub fn render_log(
166 &mut self,
167 time: Option<&str>,
168 level: &str,
169 message: &str,
170 path: Option<&str>,
171 line_no: Option<u32>,
172 ) -> LogRecord {
173 let time_str = if self.show_time {
175 let ts = time.unwrap_or("");
176 if self.omit_repeated_times {
177 if let Some(ref last) = self.last_time {
178 if last == ts {
179 "".to_string()
180 } else {
181 self.last_time = Some(ts.to_string());
182 ts.to_string()
183 }
184 } else {
185 self.last_time = Some(ts.to_string());
186 ts.to_string()
187 }
188 } else {
189 ts.to_string()
190 }
191 } else {
192 String::new()
193 };
194
195 let path_str = if self.show_path {
197 match (path, line_no) {
198 (Some(p), Some(l)) => format!("{p}:{l}"),
199 (Some(p), None) => p.to_string(),
200 (None, Some(l)) => format!("<unknown>:{l}"),
201 (None, None) => String::new(),
202 }
203 } else {
204 String::new()
205 };
206
207 let padded_level = if self.show_level {
209 format!("{level:>width$}", width = self.level_width)
210 } else {
211 String::new()
212 };
213
214 LogRecord {
215 time: time_str,
216 level: padded_level,
217 message: message.to_string(),
218 path: path_str,
219 show_time: self.show_time,
220 show_level: self.show_level,
221 show_path: self.show_path,
222 }
223 }
224
225 pub fn render_batch(
227 &mut self,
228 records: &[(Option<&str>, &str, &str, Option<&str>, Option<u32>)],
229 ) -> LogTable {
230 let rendered: Vec<LogRecord> = records
231 .iter()
232 .map(|(time, level, msg, path, line)| {
233 self.render_log(*time, level, msg, *path, *line)
234 })
235 .collect();
236 LogTable { records: rendered }
237 }
238
239 pub fn reset_time_cache(&mut self) {
241 self.last_time = None;
242 }
243}
244
245impl Default for LogRender {
246 fn default() -> Self {
247 Self::new()
248 }
249}
250
251#[derive(Debug, Clone)]
257pub struct LogRecord {
258 time: String,
259 level: String,
260 message: String,
261 path: String,
262 show_time: bool,
263 show_level: bool,
264 show_path: bool,
265}
266
267impl Renderable for LogRecord {
268 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
269 let time_style = LogRender::get_time_style();
270 let level_style = LogRender::get_level_style(&self.level.trim());
271 let msg_style = LogRender::get_message_style();
272 let path_style = LogRender::get_path_style();
273
274 let mut line: Vec<Segment> = Vec::new();
275
276 if self.show_time && !self.time.is_empty() {
277 line.push(Segment::styled(&self.time, time_style.clone()));
278 line.push(Segment::new(" "));
279 }
280
281 if self.show_level && !self.level.is_empty() {
282 line.push(Segment::styled(&self.level, level_style));
283 line.push(Segment::new(" "));
284 }
285
286 line.push(Segment::styled(&self.message, msg_style));
287
288 if self.show_path && !self.path.is_empty() {
289 line.push(Segment::new(" "));
290 line.push(Segment::styled(&self.path, path_style));
291 }
292
293 line.push(Segment::line());
294
295 RenderResult {
296 lines: vec![line],
297 items: Vec::new(),
298 }
299 }
300}
301
302#[derive(Debug, Clone)]
308pub struct LogTable {
309 records: Vec<LogRecord>,
310}
311
312impl Renderable for LogTable {
313 fn render(&self, options: &ConsoleOptions) -> RenderResult {
314 if self.records.is_empty() {
315 return RenderResult {
316 lines: Vec::new(),
317 items: Vec::new(),
318 };
319 }
320
321 let mut table = Table::new();
322 table.show_header = false;
323 table.show_edge = false;
324 table.show_lines = false;
325
326 let first = &self.records[0];
328 if first.show_time {
329 table.add_column(Column::new("Time"));
330 }
331 if first.show_level {
332 table.add_column(Column::new("Level"));
333 }
334 table.add_column(Column::new("Message"));
335 if first.show_path {
336 table.add_column(Column::new("Path"));
337 }
338
339 for record in &self.records {
340 let mut cells: Vec<crate::table::Cell> = Vec::new();
341
342 if record.show_time {
343 let time_str = if record.time.is_empty() {
344 String::new()
345 } else {
346 LogRender::get_time_style().render(&record.time)
347 };
348 cells.push(crate::table::Cell::new(time_str));
349 }
350
351 if record.show_level {
352 let level_str = LogRender::get_level_style(record.level.trim()).render(&record.level);
353 cells.push(crate::table::Cell::new(level_str));
354 }
355
356 cells.push(crate::table::Cell::new(
357 LogRender::get_message_style().render(&record.message),
358 ));
359
360 if record.show_path && !record.path.is_empty() {
361 cells.push(crate::table::Cell::new(
362 LogRender::get_path_style().render(&record.path),
363 ));
364 }
365
366 table.add_row(cells);
367 }
368
369 table.render(options)
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_log_render_defaults() {
379 let lr = LogRender::new();
380 assert!(lr.show_time);
381 assert!(lr.show_level);
382 assert!(lr.show_path);
383 }
384
385 #[test]
386 fn test_log_render_builder() {
387 let lr = LogRender::new()
388 .show_time(false)
389 .show_level(false)
390 .show_path(false)
391 .level_width(10)
392 .omit_repeated_times(false);
393 assert!(!lr.show_time);
394 assert!(!lr.show_level);
395 assert!(!lr.show_path);
396 assert_eq!(lr.level_width, 10);
397 assert!(!lr.omit_repeated_times);
398 }
399
400 #[test]
401 fn test_log_render_single() {
402 let mut lr = LogRender::new();
403 let record = lr.render_log(
404 Some("2024-01-15 10:30:00"),
405 "INFO",
406 "Hello world",
407 Some("src/main.rs"),
408 Some(42),
409 );
410 let opts = ConsoleOptions::default();
411 let result = record.render(&opts);
412 let ansi = result.to_ansi();
413 assert!(ansi.contains("Hello world"));
414 }
415
416 #[test]
417 fn test_log_render_no_path() {
418 let mut lr = LogRender::new().show_path(false);
419 let record = lr.render_log(None, "DEBUG", "debug message", None, None);
420 let opts = ConsoleOptions::default();
421 let result = record.render(&opts);
422 let ansi = result.to_ansi();
423 assert!(ansi.contains("debug message"));
424 }
425
426 #[test]
427 fn test_log_render_level_styles() {
428 let debug_style = LogRender::get_level_style("DEBUG");
429 let info_style = LogRender::get_level_style("INFO");
430 let warn_style = LogRender::get_level_style("WARNING");
431 let error_style = LogRender::get_level_style("ERROR");
432 let critical_style = LogRender::get_level_style("CRITICAL");
433 assert!(!debug_style.is_null() || true);
435 assert!(!info_style.is_null() || true);
436 assert!(!warn_style.is_null() || true);
437 assert!(!error_style.is_null() || true);
438 assert!(!critical_style.is_null() || true);
439 }
440
441 #[test]
442 fn test_log_render_batch() {
443 let mut lr = LogRender::new().show_path(false).show_time(false);
444 let records = vec![
445 (None, "INFO", "first", None, None),
446 (None, "ERROR", "second", None, None),
447 ];
448 let table = lr.render_batch(&records);
449 let opts = ConsoleOptions::default();
450 let result = table.render(&opts);
451 let ansi = result.to_ansi();
452 assert!(ansi.contains("first"));
453 assert!(ansi.contains("second"));
454 }
455
456 #[test]
457 fn test_log_render_time_dedup() {
458 let mut lr = LogRender::new().show_path(false).show_level(false);
459 let r1 = lr.render_log(Some("2024-01-01"), "INFO", "msg1", None, None);
460 let r2 = lr.render_log(Some("2024-01-01"), "INFO", "msg2", None, None);
461 assert!(!r1.time.is_empty());
463 assert!(r2.time.is_empty());
464 }
465
466 #[test]
467 fn test_log_render_reset_cache() {
468 let mut lr = LogRender::new().show_path(false).show_level(false);
469 lr.render_log(Some("ts"), "INFO", "msg1", None, None);
470 lr.reset_time_cache();
471 let r = lr.render_log(Some("ts"), "INFO", "msg2", None, None);
472 assert!(!r.time.is_empty());
474 }
475}