mermaid_cli/utils/
logger.rs1use std::fs::OpenOptions;
2use std::path::{Path, PathBuf};
3use tracing::{debug, error, info, warn};
4use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
5
6const MAX_LOG_SIZE: u64 = 10 * 1024 * 1024; fn get_log_file_path() -> Option<PathBuf> {
13 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
21fn 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
34pub fn init_logger(verbose: bool) {
36 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 if let Some(log_path) = get_log_file_path() {
47 if let Some(parent) = log_path.parent() {
49 let _ = std::fs::create_dir_all(parent);
50 }
51
52 rotate_if_large(&log_path);
54
55 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) .compact();
64
65 tracing_subscriber::registry()
66 .with(filter)
67 .with(fmt_layer)
68 .init();
69 return;
70 }
71 }
72
73 tracing_subscriber::registry().with(filter).init();
75}
76
77pub fn log_info(category: &str, message: impl std::fmt::Display) {
79 info!(category = %category, "{}", message);
80}
81
82pub fn log_warn(category: &str, message: impl std::fmt::Display) {
84 warn!(category = %category, "{}", message);
85}
86
87pub fn log_error(category: &str, message: impl std::fmt::Display) {
89 error!(category = %category, "{}", message);
90}
91
92pub fn log_debug(message: impl std::fmt::Display) {
94 debug!("{}", message);
95}
96
97pub 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 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}