forc_tracing/
lib.rs

1//! Utility items shared between forc crates.
2
3use ansiterm::Colour;
4use std::str;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::{env, io};
7use tracing::{Level, Metadata};
8pub use tracing_subscriber::{
9    self,
10    filter::{filter_fn, EnvFilter, LevelFilter},
11    fmt::{format::FmtSpan, MakeWriter},
12    layer::SubscriberExt,
13};
14
15const ACTION_COLUMN_WIDTH: usize = 12;
16
17// Global flag to track if JSON output mode is active
18static JSON_MODE_ACTIVE: AtomicBool = AtomicBool::new(false);
19
20/// Check if JSON mode is currently active
21fn is_json_mode_active() -> bool {
22    JSON_MODE_ACTIVE.load(Ordering::SeqCst)
23}
24
25/// Returns the indentation for the action prefix relative to [ACTION_COLUMN_WIDTH].
26fn get_action_indentation(action: &str) -> String {
27    if action.len() < ACTION_COLUMN_WIDTH {
28        " ".repeat(ACTION_COLUMN_WIDTH - action.len())
29    } else {
30        String::new()
31    }
32}
33
34enum TextStyle {
35    Plain,
36    Bold,
37    Label(String),
38    Action(String),
39}
40
41enum LogLevel {
42    #[allow(dead_code)]
43    Trace,
44    Debug,
45    Info,
46    Warn,
47    Error,
48}
49
50/// Common function to handle all kinds of output with color and styling
51fn print_message(text: &str, color: Colour, style: TextStyle, level: LogLevel) {
52    let log_msg = match (is_json_mode_active(), style) {
53        // JSON mode formatting (no colors)
54        (true, TextStyle::Plain | TextStyle::Bold) => text.to_string(),
55        (true, TextStyle::Label(label)) => format!("{label}: {text}"),
56        (true, TextStyle::Action(action)) => {
57            let indent = get_action_indentation(&action);
58            format!("{indent}{action} {text}")
59        }
60
61        // Normal mode formatting (with colors)
62        (false, TextStyle::Plain) => format!("{}", color.paint(text)),
63        (false, TextStyle::Bold) => format!("{}", color.bold().paint(text)),
64        (false, TextStyle::Label(label)) => format!("{} {}", color.bold().paint(label), text),
65        (false, TextStyle::Action(action)) => {
66            let indent = get_action_indentation(&action);
67            format!("{}{} {}", indent, color.bold().paint(action), text)
68        }
69    };
70
71    match level {
72        LogLevel::Trace => tracing::trace!("{}", log_msg),
73        LogLevel::Debug => tracing::debug!("{}", log_msg),
74        LogLevel::Info => tracing::info!("{}", log_msg),
75        LogLevel::Warn => tracing::warn!("{}", log_msg),
76        LogLevel::Error => tracing::error!("{}", log_msg),
77    }
78}
79
80/// Prints a label with a green-bold label prefix like "Compiling ".
81pub fn println_label_green(label: &str, txt: &str) {
82    print_message(
83        txt,
84        Colour::Green,
85        TextStyle::Label(label.to_string()),
86        LogLevel::Info,
87    );
88}
89
90/// Prints an action message with a green-bold prefix like "   Compiling ".
91pub fn println_action_green(action: &str, txt: &str) {
92    println_action(action, txt, Colour::Green);
93}
94
95/// Prints a label with a red-bold label prefix like "error: ".
96pub fn println_label_red(label: &str, txt: &str) {
97    print_message(
98        txt,
99        Colour::Red,
100        TextStyle::Label(label.to_string()),
101        LogLevel::Info,
102    );
103}
104
105/// Prints an action message with a red-bold prefix like "   Removing ".
106pub fn println_action_red(action: &str, txt: &str) {
107    println_action(action, txt, Colour::Red);
108}
109
110/// Prints an action message with a yellow-bold prefix like "   Finished ".
111pub fn println_action_yellow(action: &str, txt: &str) {
112    println_action(action, txt, Colour::Yellow);
113}
114
115fn println_action(action: &str, txt: &str, color: Colour) {
116    print_message(
117        txt,
118        color,
119        TextStyle::Action(action.to_string()),
120        LogLevel::Info,
121    );
122}
123
124/// Prints a warning message to stdout with the yellow prefix "warning: ".
125pub fn println_warning(txt: &str) {
126    print_message(
127        txt,
128        Colour::Yellow,
129        TextStyle::Label("warning:".to_string()),
130        LogLevel::Warn,
131    );
132}
133
134/// Prints a warning message to stdout with the yellow prefix "warning: " only in verbose mode.
135pub fn println_warning_verbose(txt: &str) {
136    print_message(
137        txt,
138        Colour::Yellow,
139        TextStyle::Label("warning:".to_string()),
140        LogLevel::Debug,
141    );
142}
143
144/// Prints a warning message to stderr with the red prefix "error: ".
145pub fn println_error(txt: &str) {
146    print_message(
147        txt,
148        Colour::Red,
149        TextStyle::Label("error:".to_string()),
150        LogLevel::Error,
151    );
152}
153
154pub fn println_red(txt: &str) {
155    print_message(txt, Colour::Red, TextStyle::Plain, LogLevel::Info);
156}
157
158pub fn println_green(txt: &str) {
159    print_message(txt, Colour::Green, TextStyle::Plain, LogLevel::Info);
160}
161
162pub fn println_yellow(txt: &str) {
163    print_message(txt, Colour::Yellow, TextStyle::Plain, LogLevel::Info);
164}
165
166pub fn println_green_bold(txt: &str) {
167    print_message(txt, Colour::Green, TextStyle::Bold, LogLevel::Info);
168}
169
170pub fn println_yellow_bold(txt: &str) {
171    print_message(txt, Colour::Yellow, TextStyle::Bold, LogLevel::Info);
172}
173
174pub fn println_yellow_err(txt: &str) {
175    print_message(txt, Colour::Yellow, TextStyle::Plain, LogLevel::Error);
176}
177
178pub fn println_red_err(txt: &str) {
179    print_message(txt, Colour::Red, TextStyle::Plain, LogLevel::Error);
180}
181
182const LOG_FILTER: &str = "RUST_LOG";
183
184#[derive(PartialEq, Eq)]
185pub enum TracingWriter {
186    /// Write ERROR and WARN to stderr and everything else to stdout.
187    Stdio,
188    /// Write everything to stdout.
189    Stdout,
190    /// Write everything to stderr.
191    Stderr,
192    /// Write everything as structured JSON to stdout.
193    Json,
194}
195
196#[derive(Default)]
197pub struct TracingSubscriberOptions {
198    pub verbosity: Option<u8>,
199    pub silent: Option<bool>,
200    pub log_level: Option<LevelFilter>,
201    pub writer_mode: Option<TracingWriter>,
202    pub regex_filter: Option<String>,
203}
204
205// This allows us to write ERROR and WARN level logs to stderr and everything else to stdout.
206// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/trait.MakeWriter.html
207impl<'a> MakeWriter<'a> for TracingWriter {
208    type Writer = Box<dyn io::Write>;
209
210    fn make_writer(&'a self) -> Self::Writer {
211        match self {
212            TracingWriter::Stderr => Box::new(io::stderr()),
213            // We must have an implementation of `make_writer` that makes
214            // a "default" writer without any configuring metadata. Let's
215            // just return stdout in that case.
216            _ => Box::new(io::stdout()),
217        }
218    }
219
220    fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
221        // Here's where we can implement our special behavior. We'll
222        // check if the metadata's verbosity level is WARN or ERROR,
223        // and return stderr in that case.
224        if *self == TracingWriter::Stderr
225            || (*self == TracingWriter::Stdio && meta.level() <= &Level::WARN)
226        {
227            return Box::new(io::stderr());
228        }
229
230        // Otherwise, we'll return stdout.
231        Box::new(io::stdout())
232    }
233}
234
235/// A subscriber built from default `tracing_subscriber::fmt::SubscriberBuilder` such that it would match directly using `println!` throughout the repo.
236///
237/// `RUST_LOG` environment variable can be used to set different minimum level for the subscriber, default is `INFO`.
238pub fn init_tracing_subscriber(options: TracingSubscriberOptions) {
239    let env_filter = match env::var_os(LOG_FILTER) {
240        Some(_) => EnvFilter::try_from_default_env().expect("Invalid `RUST_LOG` provided"),
241        None => EnvFilter::new("info"),
242    };
243
244    let level_filter = options
245        .log_level
246        .or_else(|| {
247            options.verbosity.and_then(|verbosity| match verbosity {
248                1 => Some(LevelFilter::DEBUG), // matches --verbose or -v
249                2 => Some(LevelFilter::TRACE), // matches -vv
250                _ => None,
251            })
252        })
253        .or_else(|| {
254            options
255                .silent
256                .and_then(|silent| silent.then_some(LevelFilter::OFF))
257        });
258
259    let writer_mode = match options.writer_mode {
260        Some(TracingWriter::Json) => {
261            JSON_MODE_ACTIVE.store(true, Ordering::SeqCst);
262            TracingWriter::Json
263        }
264        Some(TracingWriter::Stderr) => TracingWriter::Stderr,
265        _ => TracingWriter::Stdio,
266    };
267
268    let builder = tracing_subscriber::fmt::Subscriber::builder()
269        .with_env_filter(env_filter)
270        .with_ansi(true)
271        .with_level(false)
272        .with_file(false)
273        .with_line_number(false)
274        .without_time()
275        .with_target(false)
276        .with_writer(writer_mode);
277
278    // Use regex to filter logs - if provided; otherwise allow all logs
279    let filter = filter_fn(move |metadata| {
280        if let Some(ref regex_filter) = options.regex_filter {
281            let regex = regex::Regex::new(regex_filter).unwrap();
282            regex.is_match(metadata.target())
283        } else {
284            true
285        }
286    });
287
288    match (is_json_mode_active(), level_filter) {
289        (true, Some(level)) => {
290            let subscriber = builder.json().with_max_level(level).finish().with(filter);
291            tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
292        }
293        (true, None) => {
294            let subscriber = builder.json().finish().with(filter);
295            tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
296        }
297        (false, Some(level)) => {
298            let subscriber = builder.with_max_level(level).finish().with(filter);
299            tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
300        }
301        (false, None) => {
302            let subscriber = builder.finish().with(filter);
303            tracing::subscriber::set_global_default(subscriber).expect("setting subscriber failed");
304        }
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use tracing_test::traced_test;
312
313    // Helper function to set up each test with consistent JSON mode state
314    fn setup_test() {
315        JSON_MODE_ACTIVE.store(false, Ordering::SeqCst);
316    }
317
318    #[traced_test]
319    #[test]
320    fn test_println_label_green() {
321        setup_test();
322
323        let txt = "main.sw";
324        println_label_green("Compiling", txt);
325
326        let expected_action = "\x1b[1;32mCompiling\x1b[0m";
327        assert!(logs_contain(&format!("{expected_action} {txt}")));
328    }
329
330    #[traced_test]
331    #[test]
332    fn test_println_label_red() {
333        setup_test();
334
335        let txt = "main.sw";
336        println_label_red("Error", txt);
337
338        let expected_action = "\x1b[1;31mError\x1b[0m";
339        assert!(logs_contain(&format!("{expected_action} {txt}")));
340    }
341
342    #[traced_test]
343    #[test]
344    fn test_println_action_green() {
345        setup_test();
346
347        let txt = "main.sw";
348        println_action_green("Compiling", txt);
349
350        let expected_action = "\x1b[1;32mCompiling\x1b[0m";
351        assert!(logs_contain(&format!("    {expected_action} {txt}")));
352    }
353
354    #[traced_test]
355    #[test]
356    fn test_println_action_green_long() {
357        setup_test();
358
359        let txt = "main.sw";
360        println_action_green("Supercalifragilistic", txt);
361
362        let expected_action = "\x1b[1;32mSupercalifragilistic\x1b[0m";
363        assert!(logs_contain(&format!("{expected_action} {txt}")));
364    }
365
366    #[traced_test]
367    #[test]
368    fn test_println_action_red() {
369        setup_test();
370
371        let txt = "main";
372        println_action_red("Removing", txt);
373
374        let expected_action = "\x1b[1;31mRemoving\x1b[0m";
375        assert!(logs_contain(&format!("     {expected_action} {txt}")));
376    }
377
378    #[traced_test]
379    #[test]
380    fn test_json_mode_println_functions() {
381        setup_test();
382
383        JSON_MODE_ACTIVE.store(true, Ordering::SeqCst);
384
385        // Call various print functions and capture the output
386        println_label_green("Label", "Value");
387        assert!(logs_contain("Label: Value"));
388
389        println_action_green("Action", "Target");
390        assert!(logs_contain("Action"));
391        assert!(logs_contain("Target"));
392
393        println_green("Green text");
394        assert!(logs_contain("Green text"));
395
396        println_warning("This is a warning");
397        assert!(logs_contain("This is a warning"));
398
399        println_error("This is an error");
400        assert!(logs_contain("This is an error"));
401
402        JSON_MODE_ACTIVE.store(false, Ordering::SeqCst);
403    }
404}