sublime_cli_tools/output/logger.rs
1//! Logging system for CLI operations.
2//!
3//! This module provides structured logging using the tracing library, with
4//! strict separation between logs (stderr) and output (stdout).
5//!
6//! # What
7//!
8//! Provides:
9//! - Logging initialization based on `LogLevel`
10//! - Tracing subscriber configuration
11//! - Convenience macros for logging at different levels
12//! - RUST_LOG environment variable support
13//! - Proper stream separation (stderr for logs, stdout for output)
14//!
15//! # How
16//!
17//! Uses `tracing` and `tracing-subscriber` to create a global logging system
18//! that writes exclusively to stderr. The logging level is controlled by the
19//! `--log-level` CLI flag, which is completely independent of the `--format`
20//! flag that controls stdout output.
21//!
22//! # Why
23//!
24//! Separating logs (stderr) from output (stdout) enables:
25//! - Clean JSON output that's never mixed with logs
26//! - Reliable piping and parsing in automation
27//! - Debugging without contaminating command output
28//! - Independent control of verbosity and format
29//!
30//! # Examples
31//!
32//! ```rust
33//! use sublime_cli_tools::output::logger::init_logging;
34//! use sublime_cli_tools::cli::LogLevel;
35//!
36//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
37//! // Initialize logging at startup
38//! init_logging(LogLevel::Info, false)?;
39//!
40//! // Use logging macros in your code
41//! tracing::info!("Operation started");
42//! tracing::debug!("Processing item: {}", "example");
43//! tracing::warn!("Potential issue detected");
44//! # Ok(())
45//! # }
46//! ```
47
48use crate::cli::LogLevel;
49use crate::error::{CliError, Result};
50use tracing_subscriber::EnvFilter;
51use tracing_subscriber::fmt::format::FmtSpan;
52
53/// Initializes the global tracing subscriber for logging.
54///
55/// This function sets up structured logging that writes exclusively to stderr,
56/// never contaminating stdout. It respects both the CLI `--log-level` flag
57/// and the `RUST_LOG` environment variable.
58///
59/// # Stream Separation
60///
61/// **Critical:** All logs go to stderr, never stdout. This ensures:
62/// - JSON output on stdout is never mixed with logs
63/// - Output can be reliably piped and parsed
64/// - Logs can be independently redirected or suppressed
65///
66/// # Log Level Priority
67///
68/// 1. If `RUST_LOG` is set, it takes precedence (for debugging)
69/// 2. Otherwise, uses the provided `level` from `--log-level` flag
70/// 3. Silent mode (`LogLevel::Silent`) completely disables logging
71///
72/// # Arguments
73///
74/// * `level` - The log level from CLI arguments (--log-level)
75/// * `no_color` - Whether to disable ANSI colors (from --no-color or NO_COLOR)
76///
77/// # Errors
78///
79/// Returns `CliError::Execution` if:
80/// - The tracing subscriber is already initialized
81/// - Environment variable parsing fails
82/// - Subscriber configuration fails
83///
84/// # Examples
85///
86/// ```rust
87/// use sublime_cli_tools::output::logger::init_logging;
88/// use sublime_cli_tools::cli::LogLevel;
89///
90/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
91/// // Initialize with info level and colors enabled
92/// init_logging(LogLevel::Info, false)?;
93///
94/// // Now logging is active
95/// tracing::info!("Application started");
96/// tracing::debug!("This won't be shown (level is Info)");
97/// # Ok(())
98/// # }
99/// ```
100///
101/// # Silent Mode
102///
103/// ```rust
104/// use sublime_cli_tools::output::logger::init_logging;
105/// use sublime_cli_tools::cli::LogLevel;
106///
107/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
108/// // Silent mode - no logs at all
109/// init_logging(LogLevel::Silent, false)?;
110///
111/// // These produce no output
112/// tracing::error!("Even errors are suppressed");
113/// tracing::info!("Silent is silent");
114/// # Ok(())
115/// # }
116/// ```
117pub fn init_logging(level: LogLevel, no_color: bool) -> Result<()> {
118 // Silent mode means no logging at all
119 if level.is_silent() {
120 return Ok(());
121 }
122
123 // Build environment filter
124 // RUST_LOG takes precedence if set, otherwise use CLI level
125 let env_filter = if std::env::var("RUST_LOG").is_ok() {
126 EnvFilter::from_default_env()
127 } else {
128 // Create filter based on log level
129 let level_filter = level.to_tracing_level();
130
131 // Filter out noisy dependencies but allow our crates
132 EnvFilter::new(format!(
133 "sublime_cli_tools={level_filter},sublime_pkg_tools={level_filter},sublime_standard_tools={level_filter},sublime_git_tools={level_filter}"
134 ))
135 };
136
137 // Configure the subscriber
138 let subscriber = tracing_subscriber::fmt()
139 .with_env_filter(env_filter)
140 .with_writer(std::io::stderr) // CRITICAL: Always stderr, never stdout
141 .with_ansi(!no_color) // Respect NO_COLOR
142 .with_target(level.includes_debug()) // Show target in debug/trace
143 .with_line_number(level.includes_trace()) // Show line numbers in trace
144 .with_file(level.includes_trace()) // Show file names in trace
145 .with_span_events(if level.includes_trace() {
146 FmtSpan::ENTER | FmtSpan::EXIT
147 } else {
148 FmtSpan::NONE
149 })
150 .with_level(true) // Always show level
151 .with_thread_ids(level.includes_trace()) // Show thread IDs in trace
152 .with_thread_names(level.includes_debug()) // Show thread names in debug
153 .compact() // Use compact format
154 .finish();
155
156 // Set the global default subscriber
157 tracing::subscriber::set_global_default(subscriber)
158 .map_err(|e| CliError::execution(format!("Failed to initialize logging: {e}")))?;
159
160 Ok(())
161}
162
163/// Creates a tracing span for command execution.
164///
165/// Spans help organize log messages by context. This is particularly useful
166/// for tracing execution flow and debugging complex operations.
167///
168/// # Arguments
169///
170/// * `name` - The name of the span (typically the command name)
171///
172/// # Returns
173///
174/// A tracing span that will log entry/exit when trace level is enabled.
175///
176/// # Examples
177///
178/// ```rust
179/// use sublime_cli_tools::output::logger::{init_logging, command_span};
180/// use sublime_cli_tools::cli::LogLevel;
181///
182/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
183/// init_logging(LogLevel::Trace, false)?;
184///
185/// let _span = command_span("bump");
186/// tracing::info!("Executing bump command");
187/// // Span automatically closes when dropped
188/// # Ok(())
189/// # }
190/// ```
191#[must_use]
192pub fn command_span(name: &str) -> tracing::Span {
193 tracing::span!(tracing::Level::INFO, "command", name = name)
194}
195
196/// Creates a tracing span for a specific operation.
197///
198/// Use this for fine-grained tracking of operations within commands.
199///
200/// # Arguments
201///
202/// * `operation` - The name of the operation
203///
204/// # Returns
205///
206/// A tracing span that will log entry/exit when trace level is enabled.
207///
208/// # Examples
209///
210/// ```rust
211/// use sublime_cli_tools::output::logger::{init_logging, operation_span};
212/// use sublime_cli_tools::cli::LogLevel;
213///
214/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
215/// init_logging(LogLevel::Trace, false)?;
216///
217/// {
218/// let _span = operation_span("load_config");
219/// tracing::debug!("Loading configuration file");
220/// }
221/// // Span automatically closes here
222/// # Ok(())
223/// # }
224/// ```
225#[must_use]
226pub fn operation_span(operation: &str) -> tracing::Span {
227 tracing::span!(tracing::Level::DEBUG, "operation", name = operation)
228}
229
230/// Re-exports for convenience.
231///
232/// These re-exports allow command implementations to use logging macros
233/// without importing tracing directly.
234///
235/// # Examples
236///
237/// ```text
238/// Instead of: use tracing::{info, debug, warn, error, trace};
239/// Just use the macros directly after this module is in scope
240/// ```
241pub use tracing::{debug, error, info, trace, warn};