Skip to main content

base_console/
console.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4use crate::{OutputFormat, console_fields::ConsoleFields, console_format::ConsoleFormat};
5use std::{
6    io::{self, IsTerminal, Write},
7    sync::Mutex,
8    time::Instant,
9};
10use tracing::{Subscriber, subscriber::SetGlobalDefaultError};
11use tracing_error::ErrorLayer;
12use tracing_subscriber::{
13    EnvFilter, Layer, Registry, filter::FromEnvError, layer::SubscriberExt, registry::LookupSpan,
14};
15
16/// Configure console output without selecting a runtime or installing global state.
17///
18/// The default filter is `info`. Use [`Self::with_env_filter`] to read `RUST_LOG`, or [`Self::with_filter`] for explicit configuration. Writing is synchronous; complete records are serialized through the writer's mutex.
19pub struct Console<W = io::Stderr> {
20    writer: W,
21    format: OutputFormat,
22    terminal: bool,
23    ansi: Option<bool>,
24    filter: EnvFilter,
25}
26
27impl Default for Console {
28    fn default() -> Self {
29        let writer = io::stderr();
30        let terminal = writer.is_terminal();
31        Self {
32            writer,
33            format: OutputFormat::Auto,
34            terminal,
35            ansi: None,
36            filter: EnvFilter::new("info"),
37        }
38    }
39}
40
41impl<W> Console<W> {
42    /// Select text, JSON, or automatic output.
43    pub fn with_format(mut self, format: OutputFormat) -> Self {
44        self.format = format;
45        self
46    }
47
48    /// Enable or disable ANSI colour in text output. JSON never includes styling.
49    pub fn with_ansi(mut self, enabled: bool) -> Self {
50        self.ansi = Some(enabled);
51        self
52    }
53
54    /// Replace the destination. Automatic output treats custom writers as redirected streams, selecting JSON without colour. Use [`Self::with_format`] to request text.
55    pub fn with_writer<T>(self, writer: T) -> Console<T> {
56        Console {
57            writer,
58            format: self.format,
59            terminal: false,
60            ansi: self.ansi,
61            filter: self.filter,
62        }
63    }
64
65    /// Set the console's filter without affecting other subscriber layers.
66    pub fn with_filter(mut self, filter: EnvFilter) -> Self {
67        self.filter = filter;
68        self
69    }
70
71    /// Read `RUST_LOG`, defaulting to `info` if absent. Invalid values are errors.
72    pub fn with_env_filter(mut self) -> Result<Self, FromEnvError> {
73        self.filter = EnvFilter::builder()
74            .with_default_directive(tracing::level_filters::LevelFilter::INFO.into())
75            .from_env()?;
76        Ok(self)
77    }
78}
79
80impl<W: Write + Send + 'static> Console<W> {
81    /// Build a composable output layer, including support for captured span traces.
82    ///
83    /// This does not install a subscriber or a `log` bridge. Its filter applies only to this layer. Attach it to a registry, then use a scoped subscriber or install the application's combined subscriber.
84    pub fn layer<S>(self) -> impl Layer<S>
85    where
86        S: Subscriber + for<'lookup> LookupSpan<'lookup>,
87    {
88        let filter = self.filter.clone();
89        self.output_layer().with_filter(filter)
90    }
91
92    fn output_layer<S>(self) -> impl Layer<S>
93    where
94        S: Subscriber + for<'lookup> LookupSpan<'lookup>,
95    {
96        let format = self.format.resolve(self.terminal);
97        let ansi = self.ansi.unwrap_or(self.terminal) && format == OutputFormat::Text;
98        let formatter = ConsoleFormat {
99            format,
100            started: Instant::now(),
101            ansi,
102        };
103        let output = tracing_subscriber::fmt::layer()
104            .fmt_fields(ConsoleFields)
105            .event_format(formatter)
106            .with_ansi(ansi)
107            .with_writer(Mutex::new(RecordWriter(self.writer)));
108        ErrorLayer::new(ConsoleFields).and_then(output)
109    }
110
111    /// Build an isolated subscriber; no global state is installed.
112    pub fn subscriber(self) -> impl Subscriber + Send + Sync {
113        let filter = self.filter.clone();
114        Registry::default().with(filter).with(self.output_layer())
115    }
116
117    /// Install this console globally. Use [`Self::try_init`] to handle setup errors.
118    ///
119    /// This leaves the process's `log` logger unchanged.
120    ///
121    /// # Panics
122    ///
123    /// Panics if another global subscriber is already installed.
124    #[track_caller]
125    pub fn init(self) {
126        self.try_init().expect("could not initialize the console");
127    }
128
129    /// Try to install this console globally. Returns an error if another subscriber is already installed.
130    ///
131    /// This leaves the process's `log` logger unchanged. Applications that want to route `log` records into tracing can install `tracing_log::LogTracer` separately.
132    pub fn try_init(self) -> Result<(), SetGlobalDefaultError> {
133        tracing::subscriber::set_global_default(self.subscriber())
134    }
135}
136
137/// A global subscriber is never dropped, so buffered destinations must flush while the record is being written.
138struct RecordWriter<W>(W);
139
140impl<W: Write> Write for RecordWriter<W> {
141    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
142        let written = self.0.write(bytes)?;
143        self.0.flush()?;
144        Ok(written)
145    }
146
147    fn flush(&mut self) -> io::Result<()> {
148        self.0.flush()
149    }
150}
151
152#[cfg(test)]
153mod tests;