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_force_color() {
165 // Clear NO_COLOR first
166 unsafe {
167 std::env::remove_var("NO_COLOR");
168 }
169
170 // Set FORCE_COLOR and verify it enables colors
171 std::env::set_var("FORCE_COLOR", "1");
172 assert!(should_use_colors());
173 unsafe {
174 std::env::remove_var("FORCE_COLOR");
175 }
176 }
177
178 #[test]
179 fn test_env_filter_verbose() {
180 // Just verify we can create the filter without panicking
181 let _filter = EnvFilter::new("fob=debug,fob_bundler=debug,fob_config=debug,fob_cli=debug");
182 // The internal format of EnvFilter isn't guaranteed, so we just verify creation
183 }
184
185 #[test]
186 fn test_env_filter_quiet() {
187 // Just verify we can create the filter without panicking
188 let _filter = EnvFilter::new("fob=error");
189 // The internal format of EnvFilter isn't guaranteed, so we just verify creation
190 }
191
192 // Integration test example (would need to be in tests/ directory)
193 // This demonstrates how you'd test actual logging output
194 /*
195 #[test]
196 fn test_logger_output() {
197 use std::sync::Once;
198 static INIT: Once = Once::new();
199
200 INIT.call_once(|| {
201 init_logger(false, false, true); // no color for testing
202 });
203
204 // Would need to capture stdout/stderr to verify actual output
205 info!("test message");
206 }
207 */
208}