rosace_trace/subscribers/log_console.rs
1//! Colored terminal sink for log records — streams `info!`/`warn!`/… to stdout
2//! with a level-colored, timestamped line. This is the default visible logging
3//! output for `rsc dev`/`rsc run`; other sinks (DevTools panel, a browser-tools
4//! socket) subscribe to the same `Log` events off the bus.
5
6use std::io::{IsTerminal, Write};
7
8use crate::bus::TraceSubscriber;
9use crate::event::RosaceTrace;
10
11/// Prints `RosaceTrace::Log` events to stdout, one colored line each. Ignores
12/// every other (structured) event — those have their own sinks. Colors are
13/// auto-disabled when stdout isn't a TTY (piped/redirected), so log files stay
14/// clean.
15pub struct LogConsoleSubscriber {
16 color: bool,
17 start: web_time::Instant,
18}
19
20impl LogConsoleSubscriber {
21 /// Auto-detects color (on only when stdout is a terminal).
22 pub fn new() -> Self {
23 Self {
24 color: std::io::stdout().is_terminal(),
25 start: web_time::Instant::now(),
26 }
27 }
28
29 /// Force color on/off (e.g. off for a log file, on for a color-capable pipe).
30 pub fn with_color(mut self, on: bool) -> Self {
31 self.color = on;
32 self
33 }
34}
35
36impl Default for LogConsoleSubscriber {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl TraceSubscriber for LogConsoleSubscriber {
43 fn on_trace(&self, event: &RosaceTrace) {
44 let RosaceTrace::Log { level, target, message, timestamp } = event else {
45 return; // not a log record — a structured trace; other sinks handle it
46 };
47 // Seconds since the sink started — a cheap, monotonic, timezone-free
48 // stamp that reads well in a dev terminal.
49 let secs = timestamp.saturating_duration_since(self.start).as_secs_f64();
50 let line = if self.color {
51 format!(
52 "\x1b[2m{secs:8.3}\x1b[0m {}{}\x1b[0m \x1b[2m{target}\x1b[0m {message}\n",
53 level.ansi(),
54 level.label(),
55 )
56 } else {
57 format!("{secs:8.3} {} {target} {message}\n", level.label())
58 };
59 // One locked write so lines from multiple threads don't interleave.
60 let out = std::io::stdout();
61 let _ = out.lock().write_all(line.as_bytes());
62 }
63}