Skip to main content

ferridriver_test/
logging.rs

1//! Centralized tracing/logging initialization.
2//!
3//! Call `ferridriver_test::logging::init()` once at startup. It's safe to call
4//! multiple times — subsequent calls are no-ops.
5//!
6//! Respects (in priority order):
7//! 1. `RUST_LOG` — standard tracing env filter
8//! 2. `FERRIDRIVER_DEBUG` — category-based filter
9//! 3. `verbose` parameter — 0=warn, 1=debug, 2+=trace
10//!
11//! ## `FERRIDRIVER_DEBUG` categories
12//!
13//! | Value | Tracing target |
14//! |-------|---------------|
15//! | `*` / `all` | `ferridriver=trace` |
16//! | `cdp` | `ferridriver::cdp=trace` |
17//! | `step` / `steps` | `ferridriver::bdd::step=trace` |
18//! | `hook` / `hooks` | `ferridriver::bdd::hook=trace` |
19//! | `worker` | `ferridriver::worker=trace` |
20//! | `fixture` | `ferridriver::fixture=trace` |
21//! | `reporter` | `ferridriver::reporter=trace` |
22//! | `action` | `ferridriver::action=trace` |
23//! | `runner` | `ferridriver::runner=trace` |
24//!
25//! ## Profiling modes (`FERRIDRIVER_PROFILE` env var)
26//!
27//! | Value | Feature flag | Output |
28//! |-------|-------------|--------|
29//! | `chrome` | `--features profiling` | `trace-{pid}.json` (Chrome DevTools / Perfetto) |
30//! | `console` | `--features tokio-console` | Live tokio-console dashboard |
31
32use std::sync::Once;
33use tracing_subscriber::EnvFilter;
34
35static INIT: Once = Once::new();
36
37/// Initialize the tracing subscriber. Safe to call multiple times.
38///
39/// `verbose`: 0 = warn (default), 1 = debug, 2+ = trace.
40/// Overridden by `RUST_LOG` or `FERRIDRIVER_DEBUG` env vars.
41pub fn init(verbose: u8) {
42  INIT.call_once(|| {
43    // tokio-console mode: sole subscriber, mutually exclusive with everything else.
44    #[cfg(feature = "tokio-console")]
45    if std::env::var("FERRIDRIVER_PROFILE").as_deref() == Ok("console") {
46      console_subscriber::init();
47      return;
48    }
49
50    let filter = build_filter(verbose);
51    let use_ansi = std::io::IsTerminal::is_terminal(&std::io::stderr());
52
53    // Chrome trace mode: layer on top of the fmt subscriber.
54    #[cfg(feature = "profiling")]
55    if std::env::var("FERRIDRIVER_PROFILE").as_deref() == Ok("chrome") {
56      use tracing_subscriber::prelude::*;
57
58      let trace_file =
59        std::env::var("FERRIDRIVER_TRACE_FILE").unwrap_or_else(|_| format!("trace-{}.json", std::process::id()));
60      let (chrome_layer, guard) = tracing_chrome::ChromeLayerBuilder::new()
61        .file(trace_file)
62        .include_args(true)
63        .build();
64
65      // Leak the guard so it flushes on process exit.
66      std::mem::forget(guard);
67
68      let fmt_layer = tracing_subscriber::fmt::layer()
69        .with_writer(std::io::stderr)
70        .with_ansi(use_ansi);
71
72      let _ = tracing_subscriber::registry()
73        .with(filter)
74        .with(fmt_layer)
75        .with(chrome_layer)
76        .try_init();
77      return;
78    }
79
80    // Default: fmt subscriber only.
81    let _ = tracing_subscriber::fmt()
82      .with_env_filter(filter)
83      .with_writer(std::io::stderr)
84      .with_ansi(use_ansi)
85      .try_init();
86  });
87}
88
89/// Initialize with env-var-only detection (no verbose flag).
90/// Used by standalone harnesses and NAPI where there's no CLI verbose flag.
91pub fn init_from_env() {
92  if std::env::var("FERRIDRIVER_DEBUG").is_ok()
93    || std::env::var("RUST_LOG").is_ok()
94    || std::env::var("FERRIDRIVER_PROFILE").is_ok()
95  {
96    init(0);
97  }
98}
99
100/// Build a tracing `EnvFilter` from verbose level and env vars.
101fn build_filter(verbose: u8) -> EnvFilter {
102  // RUST_LOG takes top priority.
103  if std::env::var("RUST_LOG").is_ok() {
104    return EnvFilter::from_default_env();
105  }
106
107  // FERRIDRIVER_DEBUG category-based filter.
108  if let Ok(debug_val) = std::env::var("FERRIDRIVER_DEBUG") {
109    return parse_debug_categories(&debug_val);
110  }
111
112  // --verbose flag.
113  match verbose {
114    0 => EnvFilter::new("warn"),
115    1 => EnvFilter::new("warn,ferridriver=debug"),
116    _ => EnvFilter::new("warn,ferridriver=trace"),
117  }
118}
119
120/// Parse `FERRIDRIVER_DEBUG` value into an `EnvFilter`.
121fn parse_debug_categories(debug_val: &str) -> EnvFilter {
122  let mut filter = EnvFilter::new("warn");
123  for category in debug_val.split(',').map(str::trim) {
124    let directive = match category {
125      "*" | "all" => "ferridriver=trace",
126      "cdp" => "ferridriver::cdp=trace",
127      "step" | "steps" => "ferridriver::bdd::step=trace",
128      "hook" | "hooks" => "ferridriver::bdd::hook=trace",
129      "worker" => "ferridriver::worker=trace",
130      "fixture" => "ferridriver::fixture=trace",
131      "reporter" => "ferridriver::reporter=trace",
132      "action" => "ferridriver::action=trace",
133      "runner" => "ferridriver::runner=trace",
134      "napi" => "napi=trace",
135      other => {
136        // Allow arbitrary target names.
137        let owned = format!("{other}=trace");
138        filter = filter.add_directive(
139          owned
140            .parse()
141            .unwrap_or_else(|_| "warn".parse().expect("'warn' is a valid directive")),
142        );
143        continue;
144      },
145    };
146    filter = filter.add_directive(
147      directive
148        .parse()
149        .unwrap_or_else(|_| "warn".parse().expect("'warn' is a valid directive")),
150    );
151  }
152  filter
153}