Skip to main content

mermaid_cli/utils/
logger.rs

1use std::fs::OpenOptions;
2use std::path::{Path, PathBuf};
3use tracing::{debug, error, info, warn};
4use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
5
6/// Rotate the log file when it reaches this size. Bounded: at most two
7/// log files (`mermaid.log` current + `mermaid.log.old` previous), so
8/// worst-case disk use is ~2x this value between restarts.
9const MAX_LOG_SIZE: u64 = 10 * 1024 * 1024; // 10 MB
10
11/// Get the log file path (~/.mermaid/mermaid.log)
12fn get_log_file_path() -> Option<PathBuf> {
13    // Fall back to USERPROFILE on Windows where HOME is not conventionally
14    // set; mirrors the pattern used in app::config::get_config_dir.
15    std::env::var("HOME")
16        .or_else(|_| std::env::var("USERPROFILE"))
17        .ok()
18        .map(|home| PathBuf::from(home).join(".mermaid").join("mermaid.log"))
19}
20
21/// If the log file exceeds MAX_LOG_SIZE, rename it to `.log.old`
22/// (overwriting any prior `.log.old`). Best-effort — rotation failures
23/// are silent because logging is non-critical. Runs once per startup.
24fn rotate_if_large(path: &Path) {
25    let Ok(meta) = std::fs::metadata(path) else {
26        return;
27    };
28    if meta.len() >= MAX_LOG_SIZE {
29        let rotated = path.with_extension("log.old");
30        let _ = std::fs::rename(path, rotated);
31    }
32}
33
34/// Initialize the logging system with tracing
35pub fn init_logger(verbose: bool) {
36    // If --verbose flag is set, override to debug level
37    // Otherwise use RUST_LOG environment variable, default to warn level (quieter)
38    let filter = if verbose {
39        EnvFilter::new("debug,mermaid=debug")
40    } else {
41        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn,mermaid=info"))
42    };
43
44    // Try to write logs to a file to avoid corrupting the TUI
45    // Falls back to no logging if file creation fails (TUI takes priority)
46    if let Some(log_path) = get_log_file_path() {
47        // Ensure parent directory exists
48        if let Some(parent) = log_path.parent() {
49            let _ = std::fs::create_dir_all(parent);
50        }
51
52        // Rotate at startup if the previous session left a large file.
53        rotate_if_large(&log_path);
54
55        // Open log file for appending
56        if let Ok(file) = OpenOptions::new().create(true).append(true).open(&log_path) {
57            let fmt_layer = tracing_subscriber::fmt::layer()
58                .with_writer(file)
59                .with_target(false)
60                .with_thread_ids(false)
61                .with_thread_names(false)
62                .with_ansi(false) // No ANSI colors in file
63                .compact();
64
65            tracing_subscriber::registry()
66                .with(filter)
67                .with(fmt_layer)
68                .init();
69            return;
70        }
71    }
72
73    // Fallback: no logging if file creation fails (don't corrupt TUI)
74    tracing_subscriber::registry().with(filter).init();
75}
76
77/// Log an info message with category prefix (backward compatible)
78pub fn log_info(category: &str, message: impl std::fmt::Display) {
79    info!(category = %category, "{}", message);
80}
81
82/// Log a warning message with category prefix (backward compatible)
83pub fn log_warn(category: &str, message: impl std::fmt::Display) {
84    warn!(category = %category, "{}", message);
85}
86
87/// Log an error message with category prefix (backward compatible)
88pub fn log_error(category: &str, message: impl std::fmt::Display) {
89    error!(category = %category, "{}", message);
90}
91
92/// Log a debug message (backward compatible)
93pub fn log_debug(message: impl std::fmt::Display) {
94    debug!("{}", message);
95}
96
97/// Progress indicator for startup sequence
98pub fn log_progress(step: usize, total: usize, message: impl std::fmt::Display) {
99    info!(step = step, total = total, "{}", message);
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn rotate_small_file_is_noop() {
108        let tmp = std::env::temp_dir().join("mermaid_logger_small.log");
109        let _ = std::fs::remove_file(&tmp);
110        let _ = std::fs::remove_file(tmp.with_extension("log.old"));
111        std::fs::write(&tmp, b"hello world").unwrap();
112
113        rotate_if_large(&tmp);
114
115        assert!(tmp.exists(), "small file should NOT be rotated");
116        assert!(
117            !tmp.with_extension("log.old").exists(),
118            "no .log.old should be created for small files"
119        );
120
121        let _ = std::fs::remove_file(&tmp);
122    }
123
124    #[test]
125    fn rotate_large_file_renames_to_old() {
126        let tmp = std::env::temp_dir().join("mermaid_logger_large.log");
127        let _ = std::fs::remove_file(&tmp);
128        let old = tmp.with_extension("log.old");
129        let _ = std::fs::remove_file(&old);
130
131        let file = std::fs::File::create(&tmp).unwrap();
132        file.set_len(MAX_LOG_SIZE + 1).unwrap();
133        drop(file);
134
135        rotate_if_large(&tmp);
136
137        assert!(!tmp.exists(), "oversized file should be rotated away");
138        assert!(old.exists(), ".log.old should now exist");
139
140        let _ = std::fs::remove_file(&old);
141    }
142
143    #[test]
144    fn rotate_overwrites_prior_old() {
145        let tmp = std::env::temp_dir().join("mermaid_logger_overwrite.log");
146        let _ = std::fs::remove_file(&tmp);
147        let old = tmp.with_extension("log.old");
148        std::fs::write(&old, b"stale previous rotation").unwrap();
149
150        let file = std::fs::File::create(&tmp).unwrap();
151        file.set_len(MAX_LOG_SIZE + 1).unwrap();
152        drop(file);
153
154        rotate_if_large(&tmp);
155
156        // Previous .old should have been replaced by the freshly rotated file.
157        let rotated_size = std::fs::metadata(&old).unwrap().len();
158        assert!(
159            rotated_size >= MAX_LOG_SIZE,
160            "the rotated file should be the large one, not the stale old"
161        );
162
163        let _ = std::fs::remove_file(&old);
164    }
165}