forc_tracing/
lib.rs

1//! Utility items shared between forc crates.
2
3use ansiterm::Colour;
4use std::str;
5use std::{env, io};
6use tracing::{Level, Metadata};
7pub use tracing_subscriber::{
8    self,
9    filter::{EnvFilter, LevelFilter},
10    fmt::{format::FmtSpan, MakeWriter},
11};
12
13const ACTION_COLUMN_WIDTH: usize = 12;
14
15/// Returns the indentation for the action prefix relative to [ACTION_COLUMN_WIDTH].
16fn get_action_indentation(action: &str) -> String {
17    if action.len() < ACTION_COLUMN_WIDTH {
18        " ".repeat(ACTION_COLUMN_WIDTH - action.len())
19    } else {
20        "".to_string()
21    }
22}
23
24/// Prints a label with a green-bold label prefix like "Compiling ".
25pub fn println_label_green(label: &str, txt: &str) {
26    println_label(label, txt, Colour::Green);
27}
28
29/// Prints an action message with a green-bold prefix like "   Compiling ".
30pub fn println_action_green(action: &str, txt: &str) {
31    println_action(action, txt, Colour::Green);
32}
33
34/// Prints a label with a red-bold label prefix like "error: ".
35pub fn println_label_red(label: &str, txt: &str) {
36    println_action(label, txt, Colour::Red);
37}
38
39fn println_label(label: &str, txt: &str, color: Colour) {
40    tracing::info!("{} {}", color.bold().paint(label), txt);
41}
42
43/// Prints an action message with a red-bold prefix like "   Removing ".
44pub fn println_action_red(action: &str, txt: &str) {
45    println_action(action, txt, Colour::Red);
46}
47
48/// Prints an action message with a yellow-bold prefix like "   Finished ".
49pub fn println_action_yellow(action: &str, txt: &str) {
50    println_action(action, txt, Colour::Yellow);
51}
52
53fn println_action(action: &str, txt: &str, color: Colour) {
54    tracing::info!(
55        "{}{} {}",
56        get_action_indentation(action),
57        color.bold().paint(action),
58        txt
59    );
60}
61
62/// Prints a warning message to stdout with the yellow prefix "warning: ".
63pub fn println_warning(txt: &str) {
64    tracing::warn!("{}: {}", Colour::Yellow.paint("warning"), txt);
65}
66
67/// Prints a warning message to stdout with the yellow prefix "warning: " only in verbose mode.
68pub fn println_warning_verbose(txt: &str) {
69    tracing::debug!("{}: {}", Colour::Yellow.paint("warning"), txt);
70}
71
72/// Prints a warning message to stderr with the red prefix "error: ".
73pub fn println_error(txt: &str) {
74    tracing::warn!("{}: {}", Colour::Red.paint("error"), txt);
75}
76
77pub fn println_red(txt: &str) {
78    println_std_out(txt, Colour::Red);
79}
80
81pub fn println_green(txt: &str) {
82    println_std_out(txt, Colour::Green);
83}
84
85pub fn println_yellow(txt: &str) {
86    println_std_out(txt, Colour::Yellow);
87}
88
89pub fn println_green_bold(txt: &str) {
90    tracing::info!("{}", Colour::Green.bold().paint(txt));
91}
92
93pub fn println_yellow_bold(txt: &str) {
94    tracing::info!("{}", Colour::Yellow.bold().paint(txt));
95}
96
97pub fn println_yellow_err(txt: &str) {
98    println_std_err(txt, Colour::Yellow);
99}
100
101pub fn println_red_err(txt: &str) {
102    println_std_err(txt, Colour::Red);
103}
104
105fn println_std_out(txt: &str, color: Colour) {
106    tracing::info!("{}", color.paint(txt));
107}
108
109fn println_std_err(txt: &str, color: Colour) {
110    tracing::error!("{}", color.paint(txt));
111}
112
113const LOG_FILTER: &str = "RUST_LOG";
114
115// This allows us to write ERROR and WARN level logs to stderr and everything else to stdout.
116// https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/trait.MakeWriter.html
117pub struct StdioTracingWriter {
118    pub writer_mode: TracingWriterMode,
119}
120
121impl<'a> MakeWriter<'a> for StdioTracingWriter {
122    type Writer = Box<dyn io::Write>;
123
124    fn make_writer(&'a self) -> Self::Writer {
125        if self.writer_mode == TracingWriterMode::Stderr {
126            Box::new(io::stderr())
127        } else {
128            // We must have an implementation of `make_writer` that makes
129            // a "default" writer without any configuring metadata. Let's
130            // just return stdout in that case.
131            Box::new(io::stdout())
132        }
133    }
134
135    fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
136        // Here's where we can implement our special behavior. We'll
137        // check if the metadata's verbosity level is WARN or ERROR,
138        // and return stderr in that case.
139        if self.writer_mode == TracingWriterMode::Stderr
140            || (self.writer_mode == TracingWriterMode::Stdio && meta.level() <= &Level::WARN)
141        {
142            return Box::new(io::stderr());
143        }
144
145        // Otherwise, we'll return stdout.
146        Box::new(io::stdout())
147    }
148}
149
150#[derive(PartialEq, Eq)]
151pub enum TracingWriterMode {
152    /// Write ERROR and WARN to stderr and everything else to stdout.
153    Stdio,
154    /// Write everything to stdout.
155    Stdout,
156    /// Write everything to stderr.
157    Stderr,
158}
159
160#[derive(Default)]
161pub struct TracingSubscriberOptions {
162    pub verbosity: Option<u8>,
163    pub silent: Option<bool>,
164    pub log_level: Option<LevelFilter>,
165    pub writer_mode: Option<TracingWriterMode>,
166}
167
168/// A subscriber built from default `tracing_subscriber::fmt::SubscriberBuilder` such that it would match directly using `println!` throughout the repo.
169///
170/// `RUST_LOG` environment variable can be used to set different minimum level for the subscriber, default is `INFO`.
171pub fn init_tracing_subscriber(options: TracingSubscriberOptions) {
172    let env_filter = match env::var_os(LOG_FILTER) {
173        Some(_) => EnvFilter::try_from_default_env().expect("Invalid `RUST_LOG` provided"),
174        None => EnvFilter::new("info"),
175    };
176    let level_filter = options
177        .log_level
178        .or_else(|| {
179            options.verbosity.and_then(|verbosity| {
180                match verbosity {
181                    1 => Some(LevelFilter::DEBUG), // matches --verbose or -v
182                    2 => Some(LevelFilter::TRACE), // matches -vv
183                    _ => None,
184                }
185            })
186        })
187        .or_else(|| {
188            options
189                .silent
190                .and_then(|silent| if silent { Some(LevelFilter::OFF) } else { None })
191        });
192
193    let builder = tracing_subscriber::fmt::Subscriber::builder()
194        .with_env_filter(env_filter)
195        .with_ansi(true)
196        .with_level(false)
197        .with_file(false)
198        .with_line_number(false)
199        .without_time()
200        .with_target(false)
201        .with_writer(StdioTracingWriter {
202            writer_mode: options.writer_mode.unwrap_or(TracingWriterMode::Stdio),
203        });
204
205    // If log level, verbosity, or silent mode is set, it overrides the RUST_LOG setting
206    if let Some(level_filter) = level_filter {
207        builder.with_max_level(level_filter).init();
208    } else {
209        builder.init();
210    }
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use tracing_test::traced_test;
217
218    #[traced_test]
219    #[test]
220    fn test_println_label_green() {
221        let txt = "main.sw";
222        println_label_green("Compiling", txt);
223
224        let expected_action = "\x1b[1;32mCompiling\x1b[0m";
225        assert!(logs_contain(&format!("{} {}", expected_action, txt)));
226    }
227
228    #[traced_test]
229    #[test]
230    fn test_println_label_red() {
231        let txt = "main.sw";
232        println_label_red("Error", txt);
233
234        let expected_action = "\x1b[1;31mError\x1b[0m";
235        assert!(logs_contain(&format!("{} {}", expected_action, txt)));
236    }
237
238    #[traced_test]
239    #[test]
240    fn test_println_action_green() {
241        let txt = "main.sw";
242        println_action_green("Compiling", txt);
243
244        let expected_action = "\x1b[1;32mCompiling\x1b[0m";
245        assert!(logs_contain(&format!("    {} {}", expected_action, txt)));
246    }
247
248    #[traced_test]
249    #[test]
250    fn test_println_action_green_long() {
251        let txt = "main.sw";
252        println_action_green("Supercalifragilistic", txt);
253
254        let expected_action = "\x1b[1;32mSupercalifragilistic\x1b[0m";
255        assert!(logs_contain(&format!("{} {}", expected_action, txt)));
256    }
257
258    #[traced_test]
259    #[test]
260    fn test_println_action_red() {
261        let txt = "main";
262        println_action_red("Removing", txt);
263
264        let expected_action = "\x1b[1;31mRemoving\x1b[0m";
265        assert!(logs_contain(&format!("     {} {}", expected_action, txt)));
266    }
267}