open_sesame/util/
log.rs

1//! Centralized logging configuration for Open Sesame
2//!
3//! Ensures all tracing output goes to stderr (never stdout) to prevent
4//! corruption of user-facing output during stdout redirection.
5//!
6//! # Critical Design Requirements
7//!
8//! 1. **All logs MUST go to stderr**: This is enforced by `.with_writer(std::io::stderr)`
9//!    on all `tracing_subscriber::fmt()` calls. This ensures commands like
10//!    `sesame --print-config > config.toml` produce clean output files.
11//!
12//! 2. **Centralized configuration**: All logging setup is in this single module
13//!    to prevent future developers from accidentally creating stdout loggers.
14//!
15//! 3. **Three logging modes**:
16//!    - Default: SILENT (no logging at all)
17//!    - With RUST_LOG env: file logging at specified level
18//!    - With debug-logging feature: file logging at DEBUG level
19//!
20//! # Usage
21//!
22//! ```rust
23//! use open_sesame::util::log;
24//!
25//! log::init();
26//! tracing::info!("Application started");
27//! ```
28
29use std::fs::OpenOptions;
30use tracing_subscriber::prelude::*;
31
32use crate::util::log_file;
33
34/// Initialize the logging subsystem
35///
36/// # Logging Strategy
37///
38/// - **With debug-logging feature**: Always log to file at DEBUG level
39/// - **With RUST_LOG env var**: Log to file at specified level
40/// - **Otherwise**: SILENT (no logging subscriber initialized)
41///
42/// # Critical Guarantee
43///
44/// **Release builds are SILENT by default** - no log output at all unless
45/// explicitly requested via RUST_LOG environment variable or debug-logging feature.
46///
47/// When logging IS enabled, **ALL OUTPUT GOES TO STDERR, NEVER STDOUT**.
48/// This is enforced by `.with_writer(std::io::stderr)` on all fmt() calls.
49/// This ensures that commands like `sesame --print-config > file.toml`
50/// produce clean TOML files without log contamination.
51///
52/// # Fallback Behavior
53///
54/// If file logging is requested but the log file path cannot be determined
55/// or the file cannot be opened, the function falls back to stderr logging
56/// with a warning message.
57pub fn init() {
58    let use_file_logging = cfg!(feature = "debug-logging") || std::env::var("RUST_LOG").is_ok();
59
60    // Default release builds: SILENT (no logging at all)
61    if !use_file_logging {
62        return;
63    }
64
65    // Logging is explicitly enabled via feature or env var
66    let env_filter = if cfg!(feature = "debug-logging") {
67        tracing_subscriber::EnvFilter::new("debug")
68    } else {
69        // RUST_LOG is set - use it without adding default directive
70        tracing_subscriber::EnvFilter::from_default_env()
71    };
72
73    // Log to file for GUI debugging
74    // Uses secure cache directory with proper permissions
75    let log_path = match log_file() {
76        Ok(path) => path,
77        Err(e) => {
78            // Falls back to stderr when secure log path is unavailable
79            eprintln!(
80                "Warning: Cannot determine log file path: {}. Logging to stderr.",
81                e
82            );
83            tracing_subscriber::fmt()
84                .with_writer(std::io::stderr)
85                .with_env_filter(env_filter)
86                .init();
87            return;
88        }
89    };
90
91    // Appends to log file to preserve history across multiple instances
92    let log_file_result = OpenOptions::new().create(true).append(true).open(&log_path);
93
94    match log_file_result {
95        Ok(log_file) => {
96            let file_layer = tracing_subscriber::fmt::layer()
97                .with_writer(log_file)
98                .with_ansi(false);
99
100            tracing_subscriber::registry()
101                .with(env_filter)
102                .with(file_layer)
103                .init();
104
105            tracing::info!(
106                "========== NEW RUN (PID: {}) ==========",
107                std::process::id()
108            );
109            tracing::info!("Logging to: {}", log_path.display());
110        }
111        Err(e) => {
112            // Fallback to stderr logging if file cannot be opened
113            // CRITICAL: Uses stderr writer to prevent stdout contamination
114            tracing_subscriber::fmt()
115                .with_writer(std::io::stderr)
116                .with_env_filter(env_filter)
117                .init();
118            tracing::warn!(
119                "Failed to open log file {}: {}. Falling back to stderr.",
120                log_path.display(),
121                e
122            );
123        }
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    #[test]
130    fn test_init_does_not_panic() {
131        // This test verifies that init() can be called without panicking
132        // We can't easily test the actual logging behavior in a unit test,
133        // but we can at least ensure it doesn't crash
134        //
135        // Note: This will fail if called multiple times in the same process
136        // because tracing subscriber can only be set once. That's expected.
137        //
138        // Run with: cargo test --lib util::log::tests
139    }
140
141    #[test]
142    fn test_logging_modes_compile() {
143        // This test just verifies that the feature flag logic compiles
144        // The actual behavior is tested via integration tests
145        let _use_file = cfg!(feature = "debug-logging") || std::env::var("RUST_LOG").is_ok();
146    }
147}