1use crate::console::{Console, ConsoleOptions};
14use crate::measure::Measurement;
15use crate::protocol::Renderable;
16use crate::segment::Segment;
17use crate::style::Style;
18use crate::text::Text;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LogLevel {
23 Trace,
24 Debug,
25 Info,
26 Warn,
27 Error,
28}
29
30impl LogLevel {
31 fn styled(self) -> (&'static str, &'static str) {
33 match self {
34 LogLevel::Trace => ("TRACE", "dim"),
35 LogLevel::Debug => ("DEBUG", "green"),
36 LogLevel::Info => ("INFO", "blue"),
37 LogLevel::Warn => ("WARN", "yellow"),
38 LogLevel::Error => ("ERROR", "bold red"),
39 }
40 }
41}
42
43pub struct LogRender {
46 time: Option<String>,
47 level: LogLevel,
48 message: String,
49 path: Option<String>,
50}
51
52impl LogRender {
53 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
55 LogRender {
56 time: None,
57 level,
58 message: message.into(),
59 path: None,
60 }
61 }
62
63 pub fn time(mut self, time: impl Into<String>) -> Self {
65 self.time = Some(time.into());
66 self
67 }
68
69 pub fn path(mut self, path: impl Into<String>) -> Self {
71 self.path = Some(path.into());
72 self
73 }
74
75 fn content(&self) -> Text {
76 let mut text = Text::new("");
77 if let Some(time) = &self.time {
78 text.append(time, Style::parse("dim cyan").ok().map(Into::into));
79 text.append(" ", None);
80 }
81 let (name, spec) = self.level.styled();
82 text.append(
84 &format!("{name:<5}"),
85 Style::parse(spec).ok().map(Into::into),
86 );
87 text.append(" ", None);
88 text.append(&self.message, None);
89 if let Some(path) = &self.path {
90 text.append(
91 &format!(" {path}"),
92 Style::parse("dim").ok().map(Into::into),
93 );
94 }
95 text
96 }
97}
98
99impl Renderable for LogRender {
100 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
101 self.content().rich_render(console, options)
102 }
103
104 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
105 self.content().measure(console, options)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::color::ColorSystem;
113
114 fn render(record: &LogRender) -> String {
115 Console::builder()
116 .force_terminal(true)
117 .color_system(Some(ColorSystem::Truecolor))
118 .width(80)
119 .no_color(false)
120 .build()
121 .render_to_string(record)
122 }
123
124 #[test]
125 fn info_record_is_blue_level() {
126 let out = render(&LogRender::new(LogLevel::Info, "server started"));
127 assert!(out.contains("\x1b[34mINFO \x1b[0m"), "got {out:?}");
129 assert!(out.contains("server started"));
130 }
131
132 #[test]
133 fn error_level_is_bold_red() {
134 let out = render(&LogRender::new(LogLevel::Error, "boom"));
135 assert!(out.contains("\x1b[1;31mERROR\x1b[0m"), "got {out:?}");
136 }
137
138 #[test]
139 fn time_and_path_columns() {
140 let out = render(
141 &LogRender::new(LogLevel::Warn, "low disk")
142 .time("12:00:00")
143 .path("main.rs:42"),
144 );
145 assert!(out.contains("\x1b[2;36m12:00:00\x1b[0m"), "got {out:?}");
147 assert!(out.contains("main.rs:42"));
148 assert!(out.contains("\x1b[33mWARN \x1b[0m"));
149 }
150}