1use std::io::Write;
28use std::sync::{Arc, Mutex};
29use std::time::{Duration, SystemTime, UNIX_EPOCH};
30
31use rich::{
32 Console, ConsoleOptions, Highlighter, LogRender, ReprHighlighter, Segment, Table, Text,
33};
34
35use crate::event::{Message, Severity, SpanContext, SpanEvent, StructuredEvent};
36use crate::hyperlink::Hyperlinker;
37use crate::live::{LiveCoordinator, LiveError};
38
39pub const KEYWORDS: &[&str] = &[
41 "GET", "POST", "HEAD", "PUT", "DELETE", "OPTIONS", "TRACE", "PATCH",
42];
43
44type TimeFormat = Box<dyn Fn() -> String + Send + Sync>;
45type Output = Box<dyn Fn(&[Segment]) -> std::io::Result<()> + Send + Sync>;
46
47#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub enum SpanView {
50 #[default]
52 Inline,
53 Tree,
57 Hidden,
59}
60
61pub struct RichHandler {
63 console: Mutex<Console>,
64 render: Mutex<LogRender>,
65 highlighter: Option<Box<dyn Highlighter + Send + Sync>>,
66 markup: bool,
67 keywords: Vec<String>,
68 enable_link_path: bool,
69 time_format: TimeFormat,
70 span_view: SpanView,
71 hyperlinker: Option<Hyperlinker>,
72 output: Option<Output>,
73}
74
75impl RichHandler {
76 pub fn new(console: Console) -> Self {
80 RichHandler {
81 console: Mutex::new(console),
82 render: Mutex::new(LogRender::new().show_level(true)),
83 highlighter: Some(Box::new(ReprHighlighter::new())),
84 markup: false,
85 keywords: KEYWORDS.iter().map(|word| word.to_string()).collect(),
86 enable_link_path: true,
87 time_format: Box::new(utc_time),
88 span_view: SpanView::Inline,
89 hyperlinker: None,
90 output: None,
91 }
92 }
93
94 pub fn span_view(mut self, view: SpanView) -> Self {
96 self.span_view = view;
97 self
98 }
99
100 pub fn hyperlinker(mut self, hyperlinker: Hyperlinker) -> Self {
104 self.hyperlinker = Some(hyperlinker);
105 self
106 }
107
108 pub fn live<W: Write + Send + 'static>(mut self, live: Arc<Mutex<LiveCoordinator<W>>>) -> Self {
114 self.output = Some(Box::new(move |segments| {
115 let segments = neutralise(segments);
116 live.lock()
117 .unwrap_or_else(|e| e.into_inner())
118 .print(&segments)
119 .map_err(|error| match error {
120 LiveError::Io(error) => error,
121 other => std::io::Error::other(other.to_string()),
122 })
123 }));
124 self
125 }
126
127 fn map_render(self, f: impl FnOnce(LogRender) -> LogRender) -> Self {
128 let render = self.render.into_inner().unwrap_or_else(|e| e.into_inner());
129 RichHandler {
130 render: Mutex::new(f(render)),
131 ..self
132 }
133 }
134
135 pub fn show_time(self, show: bool) -> Self {
137 self.map_render(|render| render.show_time(show))
138 }
139
140 pub fn show_level(self, show: bool) -> Self {
142 self.map_render(|render| render.show_level(show))
143 }
144
145 pub fn show_path(self, show: bool) -> Self {
147 self.map_render(|render| render.show_path(show))
148 }
149
150 pub fn omit_repeated_times(self, omit: bool) -> Self {
153 self.map_render(|render| render.omit_repeated_times(omit))
154 }
155
156 pub fn level_width(self, width: Option<usize>) -> Self {
159 self.map_render(|render| render.level_width(width))
160 }
161
162 pub fn markup(mut self, markup: bool) -> Self {
164 self.markup = markup;
165 self
166 }
167
168 pub fn highlighter(mut self, highlighter: Option<Box<dyn Highlighter + Send + Sync>>) -> Self {
170 self.highlighter = highlighter;
171 self
172 }
173
174 pub fn keywords<I, S>(mut self, keywords: I) -> Self
176 where
177 I: IntoIterator<Item = S>,
178 S: Into<String>,
179 {
180 self.keywords = keywords.into_iter().map(Into::into).collect();
181 self
182 }
183
184 pub fn enable_link_path(mut self, enable: bool) -> Self {
186 self.enable_link_path = enable;
187 self
188 }
189
190 pub fn time_format(mut self, format: impl Fn() -> String + Send + Sync + 'static) -> Self {
193 self.time_format = Box::new(format);
194 self
195 }
196
197 pub fn level_text(severity: Severity) -> Text {
200 let name = match severity {
201 Severity::Trace => {
202 return Text::styled(format!("{:<8}", "TRACE"), "logging.level.notset")
203 }
204 Severity::Debug => "DEBUG",
205 Severity::Info => "INFO",
206 Severity::Warn => "WARNING",
207 Severity::Error => "ERROR",
208 Severity::Fatal => "CRITICAL",
209 };
210 rich::level_text(name)
211 }
212
213 pub fn render_message(&self, event: &StructuredEvent) -> Text {
216 let ascii = self
217 .console
218 .lock()
219 .unwrap_or_else(|e| e.into_inner())
220 .ascii_only();
221 self.message_text(event, ascii)
222 }
223
224 fn message_text(&self, event: &StructuredEvent, ascii: bool) -> Text {
227 let spans = event.span_context();
228 let mut text = match event.span_marker() {
229 Some(marker) => self.span_line(event, marker, ascii),
230 None => {
231 let mut text = self.prefix(spans, ascii);
232 text = text.append_text(&match &event.message {
233 Message::Literal(message) if !self.markup => Text::new(message.clone()),
234 Message::Literal(markup) | Message::Markup(markup) => {
235 Text::from_markup(markup).unwrap_or_else(|_| Text::new(markup.clone()))
236 }
237 });
238 for (key, value) in &event.fields {
239 text.append(&format!(" {key}={}", value.format(false, 0)), None);
240 }
241 text
242 }
243 };
244 if let Some(highlighter) = &self.highlighter {
245 highlighter.highlight(&mut text);
246 }
247 if !self.keywords.is_empty() {
248 let words: Vec<&str> = self.keywords.iter().map(String::as_str).collect();
249 let _ = text.highlight_words(&words, "logging.keyword", true);
250 }
251 text
252 }
253
254 fn prefix(&self, spans: &[SpanContext], ascii: bool) -> Text {
257 let mut text = Text::new("");
258 match self.span_view {
259 SpanView::Hidden => {}
260 SpanView::Tree => text.append(&guide(ascii).repeat(spans.len()), Some("dim".into())),
261 SpanView::Inline if spans.is_empty() => {}
262 SpanView::Inline => {
263 for (index, span) in spans.iter().enumerate() {
264 if index > 0 {
265 text.append(":", Some("dim".into()));
266 }
267 text = text.append_text(&span_label(span, "{", "}"));
268 }
269 text.append(": ", Some("dim".into()));
270 }
271 }
272 text
273 }
274
275 fn span_line(&self, event: &StructuredEvent, marker: SpanEvent, ascii: bool) -> Text {
278 let name = match &event.message {
279 Message::Literal(name) | Message::Markup(name) => name.clone(),
280 };
281 let span = SpanContext {
282 name,
283 fields: event.fields.clone(),
284 };
285 let parents = event.span_context();
286 let elapsed = match marker {
287 SpanEvent::Open => None,
288 SpanEvent::Close { elapsed } => Some(elapsed),
289 };
290 let mut text = Text::new("");
291 match self.span_view {
292 SpanView::Tree => {
293 text.append(&guide(ascii).repeat(parents.len()), Some("dim".into()));
294 let corner = match (elapsed.is_some(), ascii) {
295 (false, false) => "┌ ",
296 (true, false) => "└ ",
297 (false, true) => "+ ",
298 (true, true) => "` ",
299 };
300 text.append(corner, Some("dim".into()));
301 if elapsed.is_some() {
303 text = text.append_text(&Text::styled(span.name.clone(), "bold"));
304 } else {
305 text = text.append_text(&span_label(&span, " ", ""));
306 }
307 }
308 SpanView::Inline | SpanView::Hidden => {
309 if self.span_view == SpanView::Inline {
310 for parent in parents {
311 text = text.append_text(&span_label(parent, "{", "}"));
312 text.append(":", Some("dim".into()));
313 }
314 }
315 text = text.append_text(&span_label(&span, "{", "}"));
316 text.append(
317 if elapsed.is_some() {
318 " closed"
319 } else {
320 " opened"
321 },
322 Some("dim".into()),
323 );
324 }
325 }
326 if let Some(elapsed) = elapsed {
327 text.append(" ", None);
328 text.append(&format_elapsed(elapsed), Some("dim".into()));
329 }
330 text
331 }
332
333 pub fn render(&self, event: &StructuredEvent) -> Table {
335 let console = self.console.lock().unwrap_or_else(|e| e.into_inner());
336 self.render_with(&console, event)
337 }
338
339 fn render_with(&self, console: &Console, event: &StructuredEvent) -> Table {
340 let context = &event.context;
341 let time = context
342 .timestamp
343 .clone()
344 .unwrap_or_else(|| (self.time_format)());
345 let level = Self::level_text(context.severity.unwrap_or(Severity::Info));
346 let source = context.source.as_ref();
347 let full_path = source.map(|source| source.path.as_str());
348 let name = full_path.map(|path| path.rsplit(['/', '\\']).next().unwrap_or(path));
349 let render = self.render.lock().unwrap_or_else(|e| e.into_inner());
350 render.render(
351 console,
352 self.message_text(event, console.ascii_only()),
353 Some(Text::new(time)),
354 level,
355 name,
356 source.and_then(|source| u32::try_from(source.line).ok()),
357 full_path.filter(|_| self.enable_link_path),
358 )
359 }
360
361 pub fn emit_event(&self, event: &StructuredEvent) {
363 let _ = self.try_emit(event);
364 }
365
366 fn try_emit(&self, event: &StructuredEvent) -> std::io::Result<()> {
367 let console = self.console.lock().unwrap_or_else(|e| e.into_inner());
368 let table = self.render_with(&console, event);
369 if self.hyperlinker.is_none() && self.output.is_none() {
370 console.print(&table);
371 return Ok(());
372 }
373 let mut lines = console.render_lines(&table, &console.options(), false);
374 if let (Some(hyperlinker), Some(source)) = (&self.hyperlinker, &event.context.source) {
375 let line = Some(source.line).filter(|&line| line > 0);
376 let url = hyperlinker.file_url(&source.path, line, None);
377 relink(&mut lines, &source.path, url.as_deref());
378 }
379 match &self.output {
380 Some(output) => output(&join_lines(lines)),
381 None => {
382 console.print(&Lines(lines));
383 Ok(())
384 }
385 }
386 }
387}
388
389fn neutralise(segments: &[Segment]) -> Vec<Segment> {
394 segments
395 .iter()
396 .filter(|segment| !segment.control)
397 .map(|segment| {
398 let mut segment = segment.clone();
399 if segment
400 .text
401 .chars()
402 .any(|c| c.is_control() && c != '\n' && c != '\t')
403 {
404 segment.text = crate::sanitize_terminal_controls(&segment.text);
405 }
406 if let Some(style) = &segment.style {
407 if let Some(link) = style
408 .link()
409 .filter(|link| link.chars().any(char::is_control))
410 {
411 use std::fmt::Write as _;
412 let mut encoded = String::with_capacity(link.len());
413 for c in link.chars() {
414 if c.is_control() {
415 let mut bytes = [0; 4];
416 for byte in c.encode_utf8(&mut bytes).bytes() {
417 let _ = write!(encoded, "%{byte:02X}");
418 }
419 } else {
420 encoded.push(c);
421 }
422 }
423 segment.style = Some(style.update_link(Some(encoded)));
424 }
425 }
426 segment
427 })
428 .collect()
429}
430
431fn guide(ascii: bool) -> &'static str {
433 if ascii {
434 "| "
435 } else {
436 "│ "
437 }
438}
439
440fn span_label(span: &SpanContext, open: &str, close: &str) -> Text {
443 let mut text = Text::styled(span.name.clone(), "bold");
444 if !span.fields.is_empty() {
445 let fields: Vec<String> = span
446 .fields
447 .iter()
448 .map(|(key, value)| format!("{key}={}", value.format(false, 0)))
449 .collect();
450 text.append(open, Some("dim".into()));
451 text.append(&fields.join(" "), None);
452 text.append(close, Some("dim".into()));
453 }
454 text
455}
456
457fn format_elapsed(elapsed: Duration) -> String {
459 let micros = elapsed.as_secs_f64() * 1e6;
460 if micros < 1000.0 {
461 format!("{micros:.0}µs")
462 } else if micros < 1e6 {
463 format!("{:.2}ms", micros / 1e3)
464 } else {
465 format!("{:.2}s", micros / 1e6)
466 }
467}
468
469fn relink(lines: &mut [Vec<Segment>], path: &str, url: Option<&str>) {
472 let bare = format!("file://{path}");
473 for segment in lines.iter_mut().flatten() {
474 let Some(style) = &segment.style else {
475 continue;
476 };
477 let linked = style
478 .link()
479 .is_some_and(|link| link == bare || link.starts_with(&format!("{bare}#")));
480 if linked {
481 segment.style = Some(style.update_link(url.map(str::to_owned)));
482 }
483 }
484}
485
486fn join_lines(lines: Vec<Vec<Segment>>) -> Vec<Segment> {
487 let mut segments = Vec::new();
488 for (index, line) in lines.into_iter().enumerate() {
489 if index > 0 {
490 segments.push(Segment::line());
491 }
492 segments.extend(line);
493 }
494 segments
495}
496
497struct Lines(Vec<Vec<Segment>>);
499
500impl rich::Renderable for Lines {
501 fn rich_render(&self, _: &Console, _: &ConsoleOptions) -> Vec<Segment> {
502 join_lines(self.0.clone())
503 }
504}
505
506#[cfg(any(feature = "log", feature = "tracing"))]
507impl crate::adapters::EventSink for RichHandler {
508 fn emit(&self, event: StructuredEvent) -> std::io::Result<()> {
509 self.emit_event(&event);
510 Ok(())
511 }
512}
513
514fn utc_time() -> String {
516 let secs = SystemTime::now()
517 .duration_since(UNIX_EPOCH)
518 .map_or(0, |elapsed| elapsed.as_secs());
519 let day = secs % 86_400;
520 format!("[{:02}:{:02}:{:02}]", day / 3600, day / 60 % 60, day % 60)
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526 use crate::event::{EventContext, SourceLocation, Value};
527
528 fn handler() -> RichHandler {
529 let console = Console::builder()
530 .force_terminal(true)
531 .color_system(Some(rich::ColorSystem::Truecolor))
532 .width(60)
533 .build();
534 RichHandler::new(console).time_format(|| "[12:00:00]".into())
535 }
536
537 fn event(message: &str, severity: Severity) -> StructuredEvent {
538 StructuredEvent::new(Message::Literal(message.into())).context(EventContext {
539 severity: Some(severity),
540 source: Some(SourceLocation {
541 path: "src/server/main.rs".into(),
542 line: 42,
543 column: None,
544 }),
545 ..Default::default()
546 })
547 }
548
549 #[test]
550 fn renders_time_level_message_and_file_name() {
551 let handler = handler().enable_link_path(false);
552 let console = handler.console.lock().unwrap();
553 let plain = console
554 .render_to_string(&handler.render_with(&console, &event("GET /index", Severity::Warn)));
555 assert!(plain.contains("[12:00:00]"), "{plain:?}");
556 assert!(plain.contains("WARNING"), "{plain:?}");
557 assert!(plain.contains("main.rs:42"), "{plain:?}");
558 assert!(!plain.contains("src/server"), "{plain:?}");
559 assert!(plain.contains("\x1b[1;33mGET\x1b[0m"), "{plain:?}");
561 }
562
563 #[test]
564 fn links_the_full_path_and_appends_fields() {
565 let handler = handler();
566 let console = handler.console.lock().unwrap();
567 let event = event("ready", Severity::Info).field("count", Value::Integer(3));
568 let out = console.render_to_string(&handler.render_with(&console, &event));
569 assert!(out.contains("file://src/server/main.rs#42"), "{out:?}");
570 assert!(out.contains("\x1b[33mcount\x1b[0m=\x1b[1;36m3"), "{out:?}");
571 }
572
573 #[test]
574 fn fatal_is_critical_and_trace_is_notset() {
575 assert_eq!(RichHandler::level_text(Severity::Fatal).plain(), "CRITICAL");
576 let trace = RichHandler::level_text(Severity::Trace);
577 assert_eq!(trace.plain(), "TRACE ");
578 }
579}