Skip to main content

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}