Skip to main content

par_term/
debug.rs

1/// Comprehensive debugging infrastructure for par-term
2///
3/// Two logging systems are unified into a single log file:
4///
5/// 1. **Custom debug macros** (`crate::debug_info!()`, etc.)
6///    - Controlled by `DEBUG_LEVEL` environment variable (0-4)
7///    - Best for high-frequency rendering/input logging with category tags
8///
9/// 2. **Standard `log` crate** (`log::info!()`, etc.)
10///    - Controlled by `RUST_LOG` environment variable
11///    - Used by most application code and third-party crates
12///
13/// Both write to `/tmp/par_term_debug.log` (Unix/macOS) or `%TEMP%\par_term_debug.log` (Windows).
14/// The log file is always created so that errors are captured even in GUI-only contexts
15/// (macOS app bundles, Windows GUI apps) where stderr is invisible.
16///
17/// When `RUST_LOG` is set, `log` crate output is also mirrored to stderr for terminal debugging.
18use parking_lot::Mutex;
19use std::fmt;
20use std::fs::OpenOptions;
21use std::io::Write;
22use std::sync::OnceLock;
23use std::time::{SystemTime, UNIX_EPOCH};
24
25/// Debug level configuration for custom debug macros
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub enum DebugLevel {
28    Off = 0,
29    Error = 1,
30    Info = 2,
31    Debug = 3,
32    Trace = 4,
33}
34
35impl DebugLevel {
36    fn from_env() -> Self {
37        match std::env::var("DEBUG_LEVEL") {
38            Ok(val) => match val.trim().parse::<u8>() {
39                Ok(0) => DebugLevel::Off,
40                Ok(1) => DebugLevel::Error,
41                Ok(2) => DebugLevel::Info,
42                Ok(3) => DebugLevel::Debug,
43                Ok(4) => DebugLevel::Trace,
44                _ => DebugLevel::Off,
45            },
46            Err(_) => DebugLevel::Off,
47        }
48    }
49}
50
51/// Global debug logger that handles both custom debug macros and `log` crate output.
52struct DebugLogger {
53    /// Level for custom debug macros (controlled by DEBUG_LEVEL)
54    level: DebugLevel,
55    /// Log file handle (always opened)
56    file: Option<std::fs::File>,
57}
58
59impl DebugLogger {
60    fn new() -> Self {
61        let level = DebugLevel::from_env();
62
63        #[cfg(unix)]
64        let log_path = std::path::PathBuf::from("/tmp/par_term_debug.log");
65        #[cfg(windows)]
66        let log_path = std::env::temp_dir().join("par_term_debug.log");
67
68        let file = OpenOptions::new()
69            .write(true)
70            .truncate(true)
71            .create(true)
72            .open(&log_path)
73            .ok();
74
75        let mut logger = DebugLogger { level, file };
76        logger.write_raw(&format!(
77            "\n{}\npar-term log session started at {} (debug_level={:?}, rust_log={})\n{}\n",
78            "=".repeat(80),
79            get_timestamp(),
80            level,
81            std::env::var("RUST_LOG").unwrap_or_else(|_| "unset".to_string()),
82            "=".repeat(80)
83        ));
84        logger
85    }
86
87    fn write_raw(&mut self, msg: &str) {
88        if let Some(ref mut file) = self.file {
89            let _ = file.write_all(msg.as_bytes());
90            let _ = file.flush();
91        }
92    }
93
94    /// Write a custom debug macro message (respects DEBUG_LEVEL)
95    fn log(&mut self, level: DebugLevel, category: &str, msg: &str) {
96        if level <= self.level {
97            let timestamp = get_timestamp();
98            let level_str = match level {
99                DebugLevel::Error => "ERROR",
100                DebugLevel::Info => "INFO ",
101                DebugLevel::Debug => "DEBUG",
102                DebugLevel::Trace => "TRACE",
103                DebugLevel::Off => return,
104            };
105            self.write_raw(&format!(
106                "[{}] [{}] [{}] {}\n",
107                timestamp, level_str, category, msg
108            ));
109        }
110    }
111
112    /// Write a `log` crate record (always writes to file)
113    fn log_record(&mut self, record: &log::Record) {
114        let timestamp = get_timestamp();
115        let level_str = match record.level() {
116            log::Level::Error => "ERROR",
117            log::Level::Warn => "WARN ",
118            log::Level::Info => "INFO ",
119            log::Level::Debug => "DEBUG",
120            log::Level::Trace => "TRACE",
121        };
122        self.write_raw(&format!(
123            "[{}] [{}] [{}] {}\n",
124            timestamp,
125            level_str,
126            record.target(),
127            record.args()
128        ));
129    }
130}
131
132static LOGGER: OnceLock<Mutex<DebugLogger>> = OnceLock::new();
133
134fn get_logger() -> &'static Mutex<DebugLogger> {
135    LOGGER.get_or_init(|| Mutex::new(DebugLogger::new()))
136}
137
138fn get_timestamp() -> String {
139    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
140    format!("{}.{:06}", now.as_secs(), now.subsec_micros())
141}
142
143/// Get the path to the debug log file.
144pub fn log_path() -> std::path::PathBuf {
145    #[cfg(unix)]
146    {
147        std::path::PathBuf::from("/tmp/par_term_debug.log")
148    }
149    #[cfg(windows)]
150    {
151        std::env::temp_dir().join("par_term_debug.log")
152    }
153}
154
155/// Check if debugging is enabled at given level (for custom debug macros)
156pub fn is_enabled(level: DebugLevel) -> bool {
157    let logger = get_logger().lock();
158    level <= logger.level
159}
160
161/// Log a message at specified level (for custom debug macros)
162pub fn log(level: DebugLevel, category: &str, msg: &str) {
163    let mut logger = get_logger().lock();
164    logger.log(level, category, msg);
165}
166
167/// Log formatted message (for custom debug macros)
168pub fn logf(level: DebugLevel, category: &str, args: fmt::Arguments) {
169    if is_enabled(level) {
170        log(level, category, &format!("{}", args));
171    }
172}
173
174// ============================================================================
175// log crate bridge — routes log::info!() etc. to the debug log file
176// ============================================================================
177
178/// Bridge that implements the `log` crate's `Log` trait, routing all log
179/// output to the par-term debug log file. Optionally mirrors to stderr
180/// when `RUST_LOG` is set (for terminal debugging).
181struct LogCrateBridge {
182    /// Maximum level to accept (parsed from RUST_LOG, default: Info)
183    max_level: log::LevelFilter,
184    /// Whether to also write to stderr (true when RUST_LOG is explicitly set)
185    mirror_stderr: bool,
186    /// Module-level filters (module_prefix, max_level) for noisy crates
187    module_filters: Vec<(&'static str, log::LevelFilter)>,
188}
189
190impl LogCrateBridge {
191    fn new() -> Self {
192        let rust_log_set = std::env::var("RUST_LOG").is_ok();
193        let max_level = if rust_log_set {
194            // Parse RUST_LOG for the default level (simplified: just use the first token)
195            match std::env::var("RUST_LOG")
196                .unwrap_or_default()
197                .to_lowercase()
198                .as_str()
199            {
200                "trace" => log::LevelFilter::Trace,
201                "debug" => log::LevelFilter::Debug,
202                "info" => log::LevelFilter::Info,
203                "warn" => log::LevelFilter::Warn,
204                "error" => log::LevelFilter::Error,
205                "off" => log::LevelFilter::Off,
206                _ => log::LevelFilter::Info, // default if RUST_LOG has module-specific syntax
207            }
208        } else {
209            // No RUST_LOG: capture info and above to the log file
210            log::LevelFilter::Info
211        };
212
213        LogCrateBridge {
214            max_level,
215            mirror_stderr: rust_log_set,
216            module_filters: vec![
217                ("wgpu_core", log::LevelFilter::Warn),
218                ("wgpu_hal", log::LevelFilter::Warn),
219                ("naga", log::LevelFilter::Warn),
220                ("rodio", log::LevelFilter::Error),
221                ("cpal", log::LevelFilter::Error),
222            ],
223        }
224    }
225
226    fn level_for_module(&self, target: &str) -> log::LevelFilter {
227        for (prefix, filter) in &self.module_filters {
228            if target.starts_with(prefix) {
229                return *filter;
230            }
231        }
232        self.max_level
233    }
234}
235
236impl log::Log for LogCrateBridge {
237    fn enabled(&self, metadata: &log::Metadata) -> bool {
238        metadata.level() <= self.level_for_module(metadata.target())
239    }
240
241    fn log(&self, record: &log::Record) {
242        if !self.enabled(record.metadata()) {
243            return;
244        }
245
246        // Write to the debug log file
247        let mut logger = get_logger().lock();
248        logger.log_record(record);
249        drop(logger);
250
251        // Mirror to stderr when RUST_LOG is set (for terminal debugging)
252        if self.mirror_stderr {
253            eprintln!(
254                "[{}] {}: {}",
255                record.level(),
256                record.target(),
257                record.args()
258            );
259        }
260    }
261
262    fn flush(&self) {}
263}
264
265/// Initialize the `log` crate bridge. Call this once from main() instead of env_logger::init().
266/// Routes all `log::info!()` etc. calls to the par-term debug log file.
267/// When `RUST_LOG` is set, also mirrors to stderr for terminal debugging.
268///
269/// `level_override` allows CLI or config to set the level. If `None`, uses
270/// `RUST_LOG` env var (or defaults to `Info`).
271pub fn init_log_bridge(level_override: Option<log::LevelFilter>) {
272    // Force logger initialization (opens the log file)
273    let _ = get_logger();
274
275    let bridge = LogCrateBridge::new();
276    // CLI/config override takes precedence, then RUST_LOG, then default
277    let max_level = level_override.unwrap_or(bridge.max_level);
278
279    // Install as the global logger
280    if log::set_boxed_logger(Box::new(bridge)).is_ok() {
281        log::set_max_level(max_level);
282    }
283}
284
285/// Update the log level at runtime (e.g., from settings UI).
286/// This only changes `log::max_level()` — the bridge itself always writes
287/// whatever passes the filter.
288pub fn set_log_level(level: log::LevelFilter) {
289    log::set_max_level(level);
290}
291
292// ============================================================================
293// Custom debug macros (unchanged, controlled by DEBUG_LEVEL)
294// ============================================================================
295
296#[macro_export]
297macro_rules! debug_error {
298    ($category:expr, $($arg:tt)*) => {
299        $crate::debug::logf($crate::debug::DebugLevel::Error, $category, format_args!($($arg)*))
300    };
301}
302
303#[macro_export]
304macro_rules! debug_info {
305    ($category:expr, $($arg:tt)*) => {
306        $crate::debug::logf($crate::debug::DebugLevel::Info, $category, format_args!($($arg)*))
307    };
308}
309
310#[macro_export]
311macro_rules! debug_log {
312    ($category:expr, $($arg:tt)*) => {
313        $crate::debug::logf($crate::debug::DebugLevel::Debug, $category, format_args!($($arg)*))
314    };
315}
316
317#[macro_export]
318macro_rules! debug_trace {
319    ($category:expr, $($arg:tt)*) => {
320        $crate::debug::logf($crate::debug::DebugLevel::Trace, $category, format_args!($($arg)*))
321    };
322}