1use anyhow::Result;
2use std::path::Path;
3use std::time::SystemTime;
4use tracing_appender::non_blocking::WorkerGuard;
5use tracing_subscriber::layer::SubscriberExt;
6use tracing_subscriber::util::SubscriberInitExt;
7use tracing_subscriber::{EnvFilter, Layer};
8
9pub fn setup(log_file: &Path, level: &str) -> Result<WorkerGuard> {
14 if let Some(parent) = log_file.parent() {
15 std::fs::create_dir_all(parent)?;
16 }
17
18 let file_appender = tracing_appender::rolling::daily(
19 log_file.parent().unwrap_or(Path::new(".")),
20 log_file.file_name().unwrap_or(std::ffi::OsStr::new("proxy.log")),
21 );
22 let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
23
24 let filter = EnvFilter::try_from_default_env()
25 .unwrap_or_else(|_| EnvFilter::new(level));
26
27 let file_layer = tracing_subscriber::fmt::layer()
28 .json()
29 .with_writer(non_blocking)
30 .with_filter(filter.clone());
31
32 let stderr_layer = tracing_subscriber::fmt::layer()
33 .with_target(false)
34 .with_filter(filter);
35
36 tracing_subscriber::registry()
37 .with(file_layer)
38 .with(stderr_layer)
39 .init();
40
41 Ok(guard)
42}
43
44pub fn prune_old_logs(log_file: &Path, keep_days: u64) {
48 let dir = match log_file.parent() {
49 Some(d) => d,
50 None => return,
51 };
52 let prefix = match log_file.file_name().and_then(|n| n.to_str()) {
53 Some(p) => p.to_owned(),
54 None => return,
55 };
56 let cutoff = SystemTime::now()
57 .checked_sub(std::time::Duration::from_secs(keep_days * 24 * 3600))
58 .unwrap_or(SystemTime::UNIX_EPOCH);
59
60 let Ok(entries) = std::fs::read_dir(dir) else { return };
61 for entry in entries.flatten() {
62 let path = entry.path();
63 let Some(name) = path.file_name().and_then(|n| n.to_str()) else { continue };
64 if !name.starts_with(&prefix) || name == prefix { continue }
67 if let Ok(meta) = entry.metadata() {
68 if let Ok(modified) = meta.modified() {
69 if modified < cutoff {
70 let _ = std::fs::remove_file(&path);
71 }
72 }
73 }
74 }
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80 use std::fs;
81 use std::time::{Duration, SystemTime};
82
83 fn set_mtime(path: &std::path::Path, age_secs: u64) {
84 #[cfg(unix)]
88 {
89 let past = SystemTime::now()
90 .checked_sub(Duration::from_secs(age_secs))
91 .unwrap();
92 let secs = past.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
93 let ts = libc::timespec { tv_sec: secs as libc::time_t, tv_nsec: 0 };
94 let times = [ts, ts];
95 let path_cstr = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
96 unsafe { libc::utimensat(libc::AT_FDCWD, path_cstr.as_ptr(), times.as_ptr(), 0) };
97 }
98 let _ = path; let _ = age_secs;
100 }
101
102 #[test]
103 fn test_prune_old_logs_removes_stale_rotated_files() {
104 let dir = std::env::temp_dir().join(format!(
105 "shunt_prune_test_{}",
106 SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos()
107 ));
108 fs::create_dir_all(&dir).unwrap();
109
110 let log_file = dir.join("proxy.log");
111 fs::write(&log_file, b"current").unwrap();
113
114 let old = dir.join("proxy.log.2020-01-01");
116 fs::write(&old, b"old").unwrap();
117 set_mtime(&old, 10 * 24 * 3600);
118
119 let recent = dir.join("proxy.log.2024-12-31");
121 fs::write(&recent, b"recent").unwrap();
122 set_mtime(&recent, 24 * 3600);
123
124 let other = dir.join("other.log");
126 fs::write(&other, b"unrelated").unwrap();
127
128 prune_old_logs(&log_file, 7);
129
130 #[cfg(unix)]
131 {
132 assert!(log_file.exists(), "current log must survive");
133 assert!(!old.exists(), "old rotated log must be pruned");
134 assert!(recent.exists(), "recent rotated log must survive");
135 }
136 assert!(other.exists(), "unrelated file must survive");
137
138 fs::remove_dir_all(&dir).ok();
139 }
140
141 #[test]
142 fn test_prune_old_logs_keeps_current_log() {
143 let dir = std::env::temp_dir().join(format!(
144 "shunt_prune_current_{}",
145 SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_nanos()
146 ));
147 fs::create_dir_all(&dir).unwrap();
148
149 let log_file = dir.join("proxy.log");
150 fs::write(&log_file, b"current").unwrap();
151 set_mtime(&log_file, 30 * 24 * 3600);
153
154 prune_old_logs(&log_file, 1);
155 assert!(log_file.exists(), "exact log file name must never be pruned");
156
157 fs::remove_dir_all(&dir).ok();
158 }
159}