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}