Skip to main content

mold_server/
logging.rs

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
8/// Holds the non-blocking file writer guard. Must live for the duration of the program.
9pub struct LogGuard {
10    _file_guard: Option<WorkerGuard>,
11}
12
13/// Initialize tracing with optional file logging and stderr output.
14pub 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 = 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            .expect("failed to create log file appender");
35        let (non_blocking, guard) = tracing_appender::non_blocking(appender);
36        file_guard = Some(guard);
37
38        let stderr_filter = make_filter(&filter_str, default_level);
39        let file_filter = make_filter(&filter_str, default_level);
40
41        let stderr_layer = tracing_subscriber::fmt::layer().with_writer(std::io::stderr);
42        let file_layer = tracing_subscriber::fmt::layer()
43            .with_writer(non_blocking)
44            .with_ansi(false);
45
46        tracing_subscriber::registry()
47            .with(stderr_layer.with_filter(stderr_filter))
48            .with(file_layer.with_filter(file_filter))
49            .init();
50    } else if json {
51        let filter = make_filter(&filter_str, default_level);
52        tracing_subscriber::fmt()
53            .with_env_filter(filter)
54            .with_writer(std::io::stderr)
55            .json()
56            .init();
57    } else {
58        let filter = make_filter(&filter_str, default_level);
59        tracing_subscriber::fmt()
60            .with_env_filter(filter)
61            .with_writer(std::io::stderr)
62            .init();
63    }
64
65    LogGuard {
66        _file_guard: file_guard,
67    }
68}
69
70/// Initialize tracing with file-only output (no stderr).
71///
72/// Used by the TUI which owns the terminal and cannot have tracing
73/// output on stderr.
74pub fn init_tracing_file_only(
75    config: &LoggingConfig,
76    default_level: &str,
77    log_dir: PathBuf,
78) -> LogGuard {
79    let filter_str = resolve_filter(config, default_level);
80
81    let _ = std::fs::create_dir_all(&log_dir);
82    cleanup_old_logs(&log_dir, config.max_days);
83    let appender = tracing_appender::rolling::RollingFileAppender::builder()
84        .rotation(tracing_appender::rolling::Rotation::DAILY)
85        .filename_prefix("mold-tui")
86        .filename_suffix("log")
87        .build(&log_dir)
88        .expect("failed to create log file appender");
89    let (non_blocking, guard) = tracing_appender::non_blocking(appender);
90
91    let filter = make_filter(&filter_str, default_level);
92    tracing_subscriber::fmt()
93        .with_env_filter(filter)
94        .with_writer(non_blocking)
95        .with_ansi(false)
96        .init();
97
98    LogGuard {
99        _file_guard: Some(guard),
100    }
101}
102
103fn resolve_filter(config: &LoggingConfig, default_level: &str) -> String {
104    std::env::var("MOLD_LOG").unwrap_or_else(|_| {
105        if config.level.is_empty() {
106            default_level.to_string()
107        } else {
108            config.level.clone()
109        }
110    })
111}
112
113fn make_filter(filter_str: &str, default_level: &str) -> EnvFilter {
114    EnvFilter::try_new(filter_str).unwrap_or_else(|_| EnvFilter::new(default_level))
115}
116
117/// Delete log files older than `max_days` from the log directory.
118fn cleanup_old_logs(log_dir: &Path, max_days: u32) {
119    let now = std::time::SystemTime::now();
120    let max_age = std::time::Duration::from_secs(max_days as u64 * 86400);
121
122    let entries = match std::fs::read_dir(log_dir) {
123        Ok(e) => e,
124        Err(_) => return,
125    };
126
127    for entry in entries.filter_map(|e| e.ok()) {
128        let path = entry.path();
129        if !path.is_file() {
130            continue;
131        }
132
133        let filename = path
134            .file_name()
135            .and_then(|f| f.to_str())
136            .unwrap_or_default();
137
138        // Match both old (no .log) and new (.log suffix) patterns
139        let is_mold_log = filename.starts_with("mold-server.") || filename.starts_with("mold-tui.");
140        if !is_mold_log {
141            continue;
142        }
143
144        if let Ok(metadata) = path.metadata() {
145            if let Ok(modified) = metadata.modified() {
146                if let Ok(age) = now.duration_since(modified) {
147                    if age > max_age {
148                        let _ = std::fs::remove_file(&path);
149                    }
150                }
151            }
152        }
153    }
154}