1use std::path::{Path, PathBuf};
2
3use mold_core::LoggingConfig;
4use tracing_appender::non_blocking::WorkerGuard;
5use tracing_subscriber::prelude::*;
6use tracing_subscriber::EnvFilter;
7
8pub struct LogGuard {
10 _file_guard: Option<WorkerGuard>,
11}
12
13pub fn init_tracing(
15 log_file: bool,
16 json: bool,
17 config: &LoggingConfig,
18 default_level: &str,
19 log_dir: PathBuf,
20) -> LogGuard {
21 let filter_str = resolve_filter(config, default_level);
22 let file_enabled = log_file || config.file;
23
24 let mut file_guard: Option<WorkerGuard> = None;
25
26 if file_enabled {
27 let _ = std::fs::create_dir_all(&log_dir);
28 cleanup_old_logs(&log_dir, config.max_days);
29 let appender = match tracing_appender::rolling::RollingFileAppender::builder()
30 .rotation(tracing_appender::rolling::Rotation::DAILY)
31 .filename_prefix("mold-server")
32 .filename_suffix("log")
33 .build(&log_dir)
34 {
35 Ok(a) => a,
36 Err(e) => {
37 eprintln!("warning: failed to create log file appender: {e}");
38 eprintln!("warning: file logging disabled, continuing with stderr only");
39 let filter = make_filter(&filter_str, default_level);
40 tracing_subscriber::fmt()
41 .with_env_filter(filter)
42 .with_writer(std::io::stderr)
43 .init();
44 return LogGuard { _file_guard: None };
45 }
46 };
47 let (non_blocking, guard) = tracing_appender::non_blocking(appender);
48 file_guard = Some(guard);
49
50 let stderr_filter = make_filter(&filter_str, default_level);
51 let file_filter = make_filter(&filter_str, default_level);
52
53 let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
54 let file_layer = tracing_subscriber::fmt::layer()
55 .with_writer(non_blocking)
56 .with_ansi(false);
57
58 tracing_subscriber::registry()
59 .with(stderr_layer.with_filter(stderr_filter))
60 .with(file_layer.with_filter(file_filter))
61 .init();
62 } else if json {
63 let filter = make_filter(&filter_str, default_level);
64 tracing_subscriber::fmt()
65 .with_env_filter(filter)
66 .with_writer(std::io::stderr)
67 .json()
68 .init();
69 } else {
70 let filter = make_filter(&filter_str, default_level);
71 tracing_subscriber::fmt()
72 .with_env_filter(filter)
73 .with_writer(std::io::stderr)
74 .init();
75 }
76
77 LogGuard {
78 _file_guard: file_guard,
79 }
80}
81
82pub fn init_tracing_file_only(
87 config: &LoggingConfig,
88 default_level: &str,
89 log_dir: PathBuf,
90) -> LogGuard {
91 let filter_str = resolve_filter(config, default_level);
92
93 let _ = std::fs::create_dir_all(&log_dir);
94 cleanup_old_logs(&log_dir, config.max_days);
95 let appender = match tracing_appender::rolling::RollingFileAppender::builder()
96 .rotation(tracing_appender::rolling::Rotation::DAILY)
97 .filename_prefix("mold-tui")
98 .filename_suffix("log")
99 .build(&log_dir)
100 {
101 Ok(a) => a,
102 Err(e) => {
103 eprintln!("warning: failed to create TUI log file appender: {e}");
104 let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
107 return LogGuard { _file_guard: None };
108 }
109 };
110 let (non_blocking, guard) = tracing_appender::non_blocking(appender);
111
112 let filter = make_filter(&filter_str, default_level);
113 tracing_subscriber::fmt()
114 .with_env_filter(filter)
115 .with_writer(non_blocking)
116 .with_ansi(false)
117 .init();
118
119 LogGuard {
120 _file_guard: Some(guard),
121 }
122}
123
124pub(crate) fn resolve_filter(config: &LoggingConfig, default_level: &str) -> String {
125 std::env::var("MOLD_LOG").unwrap_or_else(|_| {
126 if config.level.is_empty() {
127 default_level.to_string()
128 } else {
129 config.level.clone()
130 }
131 })
132}
133
134pub(crate) fn make_filter(filter_str: &str, default_level: &str) -> EnvFilter {
135 EnvFilter::try_new(filter_str).unwrap_or_else(|_| EnvFilter::new(default_level))
136}
137
138pub(crate) fn cleanup_old_logs(log_dir: &Path, max_days: u32) {
140 let now = std::time::SystemTime::now();
141 let max_age = std::time::Duration::from_secs(max_days as u64 * 86400);
142
143 let entries = match std::fs::read_dir(log_dir) {
144 Ok(e) => e,
145 Err(_) => return,
146 };
147
148 for entry in entries.filter_map(|e| e.ok()) {
149 let path = entry.path();
150 if !path.is_file() {
151 continue;
152 }
153
154 let filename = path
155 .file_name()
156 .and_then(|f| f.to_str())
157 .unwrap_or_default();
158
159 let is_mold_log = filename.starts_with("mold-server.") || filename.starts_with("mold-tui.");
161 if !is_mold_log {
162 continue;
163 }
164
165 if let Ok(metadata) = path.metadata() {
166 if let Ok(modified) = metadata.modified() {
167 if let Ok(age) = now.duration_since(modified) {
168 if age > max_age {
169 let _ = std::fs::remove_file(&path);
170 }
171 }
172 }
173 }
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use std::fs;
181 use std::time::{Duration, SystemTime};
182
183 #[test]
189 fn resolve_filter_precedence_chain() {
190 let prev = std::env::var("MOLD_LOG").ok();
191
192 std::env::set_var("MOLD_LOG", "trace");
194 let cfg = LoggingConfig {
195 level: "warn".to_string(),
196 file: false,
197 dir: None,
198 max_days: 7,
199 };
200 assert_eq!(resolve_filter(&cfg, "info"), "trace");
201
202 std::env::remove_var("MOLD_LOG");
204 let cfg = LoggingConfig {
205 level: "debug".to_string(),
206 file: false,
207 dir: None,
208 max_days: 7,
209 };
210 assert_eq!(resolve_filter(&cfg, "info"), "debug");
211
212 let cfg = LoggingConfig {
214 level: String::new(),
215 file: false,
216 dir: None,
217 max_days: 7,
218 };
219 assert_eq!(resolve_filter(&cfg, "info"), "info");
220
221 match prev {
222 Some(v) => std::env::set_var("MOLD_LOG", v),
223 None => std::env::remove_var("MOLD_LOG"),
224 }
225 }
226
227 #[test]
228 fn make_filter_accepts_both_valid_and_invalid_input_without_panicking() {
229 let _ = make_filter("mold=trace,hyper=warn", "info");
231 let _ = make_filter("@@@@@@", "warn");
234 let _ = make_filter("invalid=garbage=syntax", "info");
235 }
236
237 #[test]
238 fn cleanup_old_logs_deletes_aged_mold_files_and_keeps_fresh_ones() {
239 let dir = tempfile::tempdir().expect("tempdir");
240 let dir_path = dir.path();
241
242 let aged = dir_path.join("mold-server.2020-01-01");
243 let aged_with_suffix = dir_path.join("mold-tui.2020-01-01.log");
244 let fresh = dir_path.join("mold-server.fresh.log");
245 let unrelated = dir_path.join("not-our-business.log");
246
247 for path in [&aged, &aged_with_suffix, &fresh, &unrelated] {
248 fs::write(path, b"x").expect("write fixture");
249 }
250
251 let ancient = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 30); for path in [&aged, &aged_with_suffix] {
254 let f = fs::File::open(path).expect("open fixture");
255 f.set_modified(ancient).expect("backdate fixture");
256 }
257
258 cleanup_old_logs(dir_path, 7);
259
260 assert!(!aged.exists(), "30-day-old mold-server log must be removed");
261 assert!(
262 !aged_with_suffix.exists(),
263 "30-day-old mold-tui log must be removed",
264 );
265 assert!(fresh.exists(), "fresh mold-server log must be retained");
266 assert!(
267 unrelated.exists(),
268 "unrelated *.log file must be left untouched",
269 );
270 }
271
272 #[test]
273 fn cleanup_old_logs_returns_silently_for_missing_dir() {
274 let dir = tempfile::tempdir().expect("tempdir");
275 let missing = dir.path().join("does-not-exist");
276 cleanup_old_logs(&missing, 1);
278 }
279
280 #[test]
281 fn cleanup_old_logs_skips_subdirectories() {
282 let dir = tempfile::tempdir().expect("tempdir");
283 let dir_path = dir.path();
284 let subdir = dir_path.join("mold-server.subdir");
285 fs::create_dir(&subdir).expect("create subdir");
286 let ancient = SystemTime::now() - Duration::from_secs(60 * 60 * 24 * 30);
288 let f = fs::File::open(&subdir).expect("open subdir");
289 let _ = f.set_modified(ancient);
291
292 cleanup_old_logs(dir_path, 7);
293 assert!(
294 subdir.exists(),
295 "subdirectories must be skipped even when they pattern-match",
296 );
297 }
298}