forc_tracing/
lib.rs

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