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 = 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
82/// Initialize tracing with file-only output (no stderr).
83///
84/// Used by the TUI which owns the terminal and cannot have tracing
85/// output on stderr.
86pub 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            // Register a no-op subscriber so tracing macros are silently
105            // discarded rather than leaving the global subscriber unset.
106            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
124fn 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
134fn make_filter(filter_str: &str, default_level: &str) -> EnvFilter {
135    EnvFilter::try_new(filter_str).unwrap_or_else(|_| EnvFilter::new(default_level))
136}
137
138/// Delete log files older than `max_days` from the log directory.
139fn 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        // Match both old (no .log) and new (.log suffix) patterns
160        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}