fob_cli/
logger.rs

1//! Logging infrastructure for the Joy CLI.
2//!
3//! This module provides a structured logging setup using the `tracing` ecosystem.
4//! It supports multiple verbosity levels, colored output, and environment-based
5//! configuration for debugging.
6//!
7//! # Features
8//!
9//! - **Verbosity control**: `--verbose` for debug, `--quiet` for errors only
10//! - **Color support**: Automatic detection with `--no-color` override
11//! - **Environment filters**: Override via `RUST_LOG` environment variable
12//! - **Structured logging**: Use tracing spans for context
13//!
14//! # Example
15//!
16//! ```rust,no_run
17//! use fob_cli::logger::init_logger;
18//! use tracing::{info, debug, error};
19//!
20//! init_logger(false, false, false);
21//!
22//! info!("Starting build");
23//! debug!("Processing module: {}", "index.ts");
24//! ```
25
26use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
27
28/// Initialize the tracing subscriber with the specified options.
29///
30/// This function sets up structured logging for the CLI. It should be called
31/// once at the start of the program, before any logging occurs.
32///
33/// # Arguments
34///
35/// * `verbose` - Enable debug-level logging (overrides `quiet`)
36/// * `quiet` - Only show error-level logs
37/// * `no_color` - Disable colored output
38///
39/// # Verbosity Levels
40///
41/// The logging level is determined in this order:
42/// 1. `--verbose` flag: Sets level to DEBUG for fob crates
43/// 2. `--quiet` flag: Sets level to ERROR only
44/// 3. `RUST_LOG` environment variable: Custom filter
45/// 4. Default: INFO level for fob crates
46///
47/// # Examples
48///
49/// ```rust,no_run
50/// use fob_cli::logger::init_logger;
51///
52/// // Default logging (INFO level)
53/// init_logger(false, false, false);
54///
55/// // Debug logging
56/// init_logger(true, false, false);
57///
58/// // Quiet mode (errors only)
59/// init_logger(false, true, false);
60///
61/// // No colors (for CI/piped output)
62/// init_logger(false, false, true);
63/// ```
64pub fn init_logger(verbose: bool, quiet: bool, no_color: bool) {
65    // Determine the filter level based on flags and environment
66    let filter = if verbose {
67        // Verbose mode: debug level for fob crates, info for dependencies
68        EnvFilter::new("fob=debug,fob_bundler=debug,fob_config=debug,fob_cli=debug")
69    } else if quiet {
70        // Quiet mode: only errors
71        EnvFilter::new("fob=error")
72    } else {
73        // Try to read from RUST_LOG env var, fallback to info level
74        EnvFilter::try_from_default_env()
75            .unwrap_or_else(|_| EnvFilter::new("fob=info,fob_bundler=info,fob_config=info"))
76    };
77
78    // Configure the formatter
79    let fmt_layer = fmt::layer()
80        .with_target(false) // Don't show the module path (keeps output clean)
81        .with_level(true) // Show log level (INFO, DEBUG, etc.)
82        .with_ansi(!no_color) // Enable colors unless disabled
83        .compact(); // Use compact formatting for better readability
84
85    // Initialize the global subscriber
86    tracing_subscriber::registry()
87        .with(filter)
88        .with(fmt_layer)
89        .init();
90}
91
92/// Initialize logger with custom environment filter.
93///
94/// This is useful for testing or advanced scenarios where you need precise
95/// control over log filtering.
96///
97/// # Example
98///
99/// ```rust,no_run
100/// use fob_cli::logger::init_logger_with_filter;
101/// use tracing_subscriber::EnvFilter;
102///
103/// let filter = EnvFilter::new("fob=trace,hyper=off");
104/// init_logger_with_filter(filter, false);
105/// ```
106pub fn init_logger_with_filter(filter: EnvFilter, no_color: bool) {
107    let fmt_layer = fmt::layer()
108        .with_target(false)
109        .with_level(true)
110        .with_ansi(!no_color)
111        .compact();
112
113    tracing_subscriber::registry()
114        .with(filter)
115        .with(fmt_layer)
116        .init();
117}
118
119/// Check if colored output should be enabled.
120///
121/// This checks terminal capabilities and environment variables to determine
122/// if colors should be used. Useful for determining color support before
123/// initializing the logger.
124///
125/// # Returns
126///
127/// `true` if colors should be enabled, `false` otherwise
128///
129/// # Environment Variables
130///
131/// - `NO_COLOR`: If set, disables colors
132/// - `FORCE_COLOR`: If set, forces colors even in non-TTY
133pub fn should_use_colors() -> bool {
134    // Check NO_COLOR environment variable (standard convention)
135    if std::env::var("NO_COLOR").is_ok() {
136        return false;
137    }
138
139    // Check FORCE_COLOR environment variable
140    if std::env::var("FORCE_COLOR").is_ok() {
141        return true;
142    }
143
144    // Use console crate to detect terminal capabilities
145    // It handles cross-platform TTY detection for us
146    console::Term::stdout().features().colors_supported()
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    // Note: These tests verify the API but don't test actual output
154    // since tracing is global and can only be initialized once per process.
155
156    #[test]
157    fn test_logger_initialization() {
158        // This test just verifies the function doesn't panic
159        // We can't actually test the output without complex mocking
160        // In a real scenario, you'd use separate binaries for integration tests
161    }
162
163    #[test]
164    fn test_should_use_colors_respects_no_color() {
165        // Set NO_COLOR and verify it disables colors
166        std::env::set_var("NO_COLOR", "1");
167        assert!(!should_use_colors());
168        unsafe {
169            std::env::remove_var("NO_COLOR");
170        }
171    }
172
173    #[test]
174    fn test_should_use_colors_respects_force_color() {
175        // Clear NO_COLOR first
176        unsafe {
177            std::env::remove_var("NO_COLOR");
178        }
179
180        // Set FORCE_COLOR and verify it enables colors
181        std::env::set_var("FORCE_COLOR", "1");
182        assert!(should_use_colors());
183        unsafe {
184            std::env::remove_var("FORCE_COLOR");
185        }
186    }
187
188    #[test]
189    fn test_env_filter_verbose() {
190        // Just verify we can create the filter without panicking
191        let _filter = EnvFilter::new("fob=debug,fob_bundler=debug,fob_config=debug,fob_cli=debug");
192        // The internal format of EnvFilter isn't guaranteed, so we just verify creation
193    }
194
195    #[test]
196    fn test_env_filter_quiet() {
197        // Just verify we can create the filter without panicking
198        let _filter = EnvFilter::new("fob=error");
199        // The internal format of EnvFilter isn't guaranteed, so we just verify creation
200    }
201
202    // Integration test example (would need to be in tests/ directory)
203    // This demonstrates how you'd test actual logging output
204    /*
205    #[test]
206    fn test_logger_output() {
207        use std::sync::Once;
208        static INIT: Once = Once::new();
209
210        INIT.call_once(|| {
211            init_logger(false, false, true); // no color for testing
212        });
213
214        // Would need to capture stdout/stderr to verify actual output
215        info!("test message");
216    }
217    */
218}