Skip to main content

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