arcbox_logging/
rotating.rs1use std::fs::{self, File, OpenOptions};
8use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10
11use parking_lot::Mutex;
12
13pub struct SizeRotatingWriter {
16 inner: Mutex<RotatingState>,
17}
18
19struct RotatingState {
20 path: PathBuf,
21 max_size: u64,
22 max_files: usize,
23 file: File,
24 current_size: u64,
25}
26
27impl SizeRotatingWriter {
28 pub fn new(path: PathBuf, max_size: u64, max_files: usize) -> Self {
37 assert!(max_size > 0, "max_size must be > 0");
38 assert!(max_files > 0, "max_files must be > 0");
39
40 let (file, current_size) = open_log_file(&path);
41 Self {
42 inner: Mutex::new(RotatingState {
43 path,
44 max_size,
45 max_files,
46 file,
47 current_size,
48 }),
49 }
50 }
51}
52
53impl Write for SizeRotatingWriter {
54 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
55 let mut state = self.inner.lock();
56 write_and_maybe_rotate(&mut state, buf)
57 }
58
59 fn flush(&mut self) -> io::Result<()> {
60 let mut state = self.inner.lock();
61 state.file.flush()
62 }
63}
64
65impl Write for &SizeRotatingWriter {
68 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
69 let mut state = self.inner.lock();
70 write_and_maybe_rotate(&mut state, buf)
71 }
72
73 fn flush(&mut self) -> io::Result<()> {
74 let mut state = self.inner.lock();
75 state.file.flush()
76 }
77}
78
79fn write_and_maybe_rotate(state: &mut RotatingState, buf: &[u8]) -> io::Result<usize> {
80 if state.current_size > 0 && state.current_size + buf.len() as u64 > state.max_size {
82 rotate(state);
83 }
84
85 let written = state.file.write(buf)?;
86 state.current_size += written as u64;
87 Ok(written)
88}
89
90fn rotate(state: &mut RotatingState) {
93 let _ = state.file.flush();
95
96 let oldest = rotated_path(&state.path, state.max_files);
98 if let Err(e) = fs::remove_file(&oldest) {
99 if e.kind() != io::ErrorKind::NotFound {
100 eprintln!("log rotate: failed to remove {}: {e}", oldest.display());
101 }
102 }
103
104 for i in (1..state.max_files).rev() {
106 let from = rotated_path(&state.path, i);
107 let to = rotated_path(&state.path, i + 1);
108 if let Err(e) = fs::rename(&from, &to) {
109 if e.kind() != io::ErrorKind::NotFound {
110 eprintln!(
111 "log rotate: failed to rename {} → {}: {e}",
112 from.display(),
113 to.display()
114 );
115 }
116 }
117 }
118
119 let rotated = rotated_path(&state.path, 1);
121 if let Err(e) = fs::rename(&state.path, &rotated) {
122 eprintln!(
123 "log rotate: failed to rename {} → {}: {e}",
124 state.path.display(),
125 rotated.display()
126 );
127 return;
129 }
130
131 let (file, size) = open_log_file(&state.path);
133 state.file = file;
134 state.current_size = size;
135}
136
137fn rotated_path(base: &Path, index: usize) -> PathBuf {
138 let mut p = base.as_os_str().to_os_string();
139 p.push(format!(".{index}"));
140 PathBuf::from(p)
141}
142
143fn open_log_file(path: &Path) -> (File, u64) {
144 let file = OpenOptions::new()
145 .create(true)
146 .append(true)
147 .open(path)
148 .unwrap_or_else(|e| panic!("failed to open log file {}: {e}", path.display()));
149
150 let size = file.metadata().map_or(0, |m| m.len());
151 (file, size)
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use std::io::Write;
158
159 #[test]
160 fn rotation_creates_numbered_files() {
161 let dir = tempfile::tempdir().unwrap();
162 let log_path = dir.path().join("test.log");
163
164 let mut writer = SizeRotatingWriter::new(log_path.clone(), 100, 3);
166
167 let data = vec![b'A'; 60];
169 writer.write_all(&data).unwrap();
170 writer.write_all(&data).unwrap();
171 assert!(log_path.exists());
172
173 writer.write_all(&data).unwrap();
175 assert!(log_path.exists());
176 assert!(dir.path().join("test.log.1").exists());
177 }
178
179 #[test]
180 fn oldest_file_is_deleted() {
181 let dir = tempfile::tempdir().unwrap();
182 let log_path = dir.path().join("test.log");
183
184 let mut writer = SizeRotatingWriter::new(log_path.clone(), 50, 3);
186
187 let data = vec![b'X'; 60];
188
189 for _ in 0..6 {
191 writer.write_all(&data).unwrap();
192 }
193
194 assert!(log_path.exists());
195 assert!(dir.path().join("test.log.1").exists());
196 assert!(dir.path().join("test.log.2").exists());
197 assert!(dir.path().join("test.log.3").exists());
198 assert!(!dir.path().join("test.log.4").exists());
200 }
201}