hyperi_rustlib/logger/
format.rs1use std::fmt;
22
23use owo_colors::{OwoColorize, Style};
24use tracing::Level;
25use tracing_subscriber::fmt::format::Writer;
26use tracing_subscriber::fmt::time::{FormatTime, UtcTime};
27use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
28use tracing_subscriber::registry::LookupSpan;
29
30#[derive(Debug, Clone)]
34#[allow(clippy::struct_excessive_bools)]
35pub struct ColouredFormatter {
36 enable_ansi: bool,
37 display_target: bool,
38 display_file: bool,
39 display_line_number: bool,
40}
41
42impl ColouredFormatter {
43 #[must_use]
45 pub fn new(enable_ansi: bool) -> Self {
46 Self {
47 enable_ansi,
48 display_target: true,
49 display_file: true,
50 display_line_number: true,
51 }
52 }
53
54 #[must_use]
56 pub fn with_file(mut self, display: bool) -> Self {
57 self.display_file = display;
58 self
59 }
60
61 #[must_use]
63 pub fn with_line_number(mut self, display: bool) -> Self {
64 self.display_line_number = display;
65 self
66 }
67}
68
69impl<S, N> FormatEvent<S, N> for ColouredFormatter
70where
71 S: tracing::Subscriber + for<'a> LookupSpan<'a>,
72 N: for<'a> FormatFields<'a> + 'static,
73{
74 fn format_event(
75 &self,
76 ctx: &FmtContext<'_, S, N>,
77 mut writer: Writer<'_>,
78 event: &tracing::Event<'_>,
79 ) -> fmt::Result {
80 let meta = event.metadata();
81 let ansi = self.enable_ansi && writer.has_ansi_escapes();
82
83 let timer = UtcTime::rfc_3339();
85 let mut ts_buf = String::new();
86 let _ = timer.format_time(&mut Writer::new(&mut ts_buf));
87 if ansi {
88 write!(writer, "{} ", ts_buf.style(dim_style()))?;
89 } else {
90 write!(writer, "{ts_buf} ")?;
91 }
92
93 let level = meta.level();
95 let level_str = format!("{level:>5}");
96 if ansi {
97 write!(writer, "{} ", level_str.style(level_style(*level)))?;
98 } else {
99 write!(writer, "{level_str} ")?;
100 }
101
102 if self.display_target {
104 let target = meta.target();
105 if ansi {
106 write!(writer, "{}:", target.style(target_style()))?;
107 } else {
108 write!(writer, "{target}:")?;
109 }
110 }
111
112 if let Some(scope) = ctx.event_scope() {
114 for span in scope.from_root() {
115 if ansi {
116 write!(writer, "{}", span.name().style(span_style()))?;
117 } else {
118 write!(writer, "{}", span.name())?;
119 }
120 let ext = span.extensions();
121 if let Some(fields) = ext.get::<tracing_subscriber::fmt::FormattedFields<N>>()
122 && !fields.is_empty()
123 {
124 write!(writer, "{{{fields}}}")?;
125 }
126 write!(writer, ":")?;
127 }
128 }
129
130 write!(writer, " ")?;
132
133 ctx.format_fields(writer.by_ref(), event)?;
135
136 if self.display_file || self.display_line_number {
138 let file = meta.file();
139 let line = meta.line();
140 match (self.display_file, self.display_line_number, file, line) {
141 (true, true, Some(f), Some(l)) => {
142 let loc = format!(" {f}:{l}");
143 if ansi {
144 write!(writer, "{}", loc.style(dim_style()))?;
145 } else {
146 write!(writer, "{loc}")?;
147 }
148 }
149 (true, _, Some(f), _) => {
150 let loc = format!(" {f}");
151 if ansi {
152 write!(writer, "{}", loc.style(dim_style()))?;
153 } else {
154 write!(writer, "{loc}")?;
155 }
156 }
157 (_, true, _, Some(l)) => {
158 let loc = format!(" :{l}");
159 if ansi {
160 write!(writer, "{}", loc.style(dim_style()))?;
161 } else {
162 write!(writer, "{loc}")?;
163 }
164 }
165 _ => {}
166 }
167 }
168
169 writeln!(writer)
170 }
171}
172
173fn level_style(level: Level) -> Style {
178 match level {
179 Level::ERROR => Style::new().red().bold(),
180 Level::WARN => Style::new().yellow(),
181 Level::INFO => Style::new().green(),
182 Level::DEBUG => Style::new().blue(),
183 Level::TRACE => Style::new().magenta().dimmed(),
184 }
185}
186
187fn dim_style() -> Style {
188 Style::new().dimmed()
189}
190
191fn target_style() -> Style {
192 Style::new().cyan().dimmed()
193}
194
195fn span_style() -> Style {
196 Style::new().bold()
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_level_style_returns_distinct_styles() {
205 let _ = level_style(Level::ERROR);
207 let _ = level_style(Level::WARN);
208 let _ = level_style(Level::INFO);
209 let _ = level_style(Level::DEBUG);
210 let _ = level_style(Level::TRACE);
211 }
212
213 #[test]
214 fn test_coloured_formatter_builder() {
215 let fmt = ColouredFormatter::new(true)
216 .with_file(false)
217 .with_line_number(false);
218
219 assert!(fmt.enable_ansi);
220 assert!(fmt.display_target);
221 assert!(!fmt.display_file);
222 assert!(!fmt.display_line_number);
223 }
224}