1use crate::config::LogRotation;
13use std::fs::{self, File, OpenOptions};
14use std::io::{self, Write};
15use std::path::{Path, PathBuf};
16use std::time::Instant;
17
18#[derive(Debug)]
20pub struct RotatingFile {
21 file: File,
23 path: PathBuf,
25 policy: LogRotation,
27 bytes_written: u64,
29 events_written: u32,
31 opened_at: Instant,
33 opened_date: String,
35}
36
37impl RotatingFile {
38 pub fn open(path: &Path, policy: LogRotation) -> io::Result<Self> {
44 let file =
45 OpenOptions::new().create(true).append(true).open(path)?;
46 let bytes_written =
47 file.metadata().map(|m| m.len()).unwrap_or(0);
48 Ok(Self {
49 file,
50 path: path.to_path_buf(),
51 policy,
52 bytes_written,
53 events_written: 0,
54 opened_at: Instant::now(),
55 opened_date: today_date_string(),
56 })
57 }
58
59 pub fn write_batch(
65 &mut self,
66 data: &[u8],
67 event_count: u32,
68 ) -> io::Result<()> {
69 self.file.write_all(data)?;
70 self.bytes_written += data.len() as u64;
71 self.events_written += event_count;
72
73 if self.should_rotate() {
74 self.rotate()?;
75 }
76 Ok(())
77 }
78
79 fn should_rotate(&self) -> bool {
81 match self.policy {
82 LogRotation::Size(max_bytes) => {
83 self.bytes_written >= max_bytes.get()
84 }
85 LogRotation::Time(seconds) => {
86 self.opened_at.elapsed().as_secs() >= seconds.get()
87 }
88 LogRotation::Date => {
89 today_date_string() != self.opened_date
90 }
91 LogRotation::Count(max_events) => {
92 self.events_written >= max_events
93 }
94 }
95 }
96
97 fn rotate(&mut self) -> io::Result<()> {
100 self.file.flush()?;
102
103 let timestamp = chrono_like_timestamp();
105 let rotated_name = if let Some(ext) = self.path.extension() {
106 let stem = self.path.with_extension("");
107 PathBuf::from(format!(
108 "{}.{timestamp}.{}",
109 stem.display(),
110 ext.to_string_lossy()
111 ))
112 } else {
113 PathBuf::from(format!(
114 "{}.{timestamp}",
115 self.path.display()
116 ))
117 };
118
119 fs::rename(&self.path, &rotated_name)?;
120
121 self.file = OpenOptions::new()
123 .create(true)
124 .append(true)
125 .open(&self.path)?;
126 self.bytes_written = 0;
127 self.events_written = 0;
128 self.opened_at = Instant::now();
129 self.opened_date = today_date_string();
130
131 Ok(())
132 }
133}
134
135fn today_date_string() -> String {
137 let now = std::time::SystemTime::now()
138 .duration_since(std::time::UNIX_EPOCH)
139 .unwrap_or_default();
140 let secs = now.as_secs();
141 let days = secs / 86400;
143 let (year, month, day) = days_to_ymd(days);
144 format!("{year:04}-{month:02}-{day:02}")
145}
146
147fn chrono_like_timestamp() -> String {
149 let now = std::time::SystemTime::now()
150 .duration_since(std::time::UNIX_EPOCH)
151 .unwrap_or_default();
152 let secs = now.as_secs();
153 let days = secs / 86400;
154 let (year, month, day) = days_to_ymd(days);
155 let day_secs = secs % 86400;
156 let h = day_secs / 3600;
157 let m = (day_secs % 3600) / 60;
158 let s = day_secs % 60;
159 format!("{year:04}{month:02}{day:02}-{h:02}{m:02}{s:02}")
160}
161
162const fn days_to_ymd(days: u64) -> (u64, u64, u64) {
164 let z = days + 719_468;
166 let era = z / 146_097;
167 let doe = z - era * 146_097;
168 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
169 let y = yoe + era * 400;
170 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
171 let mp = (5 * doy + 2) / 153;
172 let d = doy - (153 * mp + 2) / 5 + 1;
173 let m = if mp < 10 { mp + 3 } else { mp - 9 };
174 let y = if m <= 2 { y + 1 } else { y };
175 (y, m, d)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use std::num::NonZeroU64;
182
183 #[test]
184 fn test_today_date_string_format() {
185 let date = today_date_string();
186 assert_eq!(date.len(), 10);
188 assert_eq!(&date[4..5], "-");
189 assert_eq!(&date[7..8], "-");
190 }
191
192 #[test]
193 fn test_chrono_like_timestamp_format() {
194 let ts = chrono_like_timestamp();
195 assert_eq!(ts.len(), 15);
197 assert_eq!(&ts[8..9], "-");
198 }
199
200 #[test]
201 fn test_days_to_ymd_epoch() {
202 let (y, m, d) = days_to_ymd(0);
203 assert_eq!((y, m, d), (1970, 1, 1));
204 }
205
206 #[test]
207 fn test_rotating_file_size_based() {
208 let dir = tempfile::tempdir().unwrap();
209 let path = dir.path().join("test.log");
210 let policy = LogRotation::Size(NonZeroU64::new(100).unwrap());
211 let mut rf = RotatingFile::open(&path, policy).unwrap();
212 rf.write_batch(&[b'A'; 50], 1).unwrap();
214 assert!(path.exists());
215 rf.write_batch(&[b'B'; 60], 1).unwrap();
217 assert!(path.exists());
219 let entries: Vec<_> = fs::read_dir(dir.path())
221 .unwrap()
222 .filter_map(Result::ok)
223 .collect();
224 assert!(entries.len() >= 2, "expected rotated file");
225 }
226
227 #[test]
228 fn test_rotating_file_count_based() {
229 let dir = tempfile::tempdir().unwrap();
230 let path = dir.path().join("count.log");
231 let policy = LogRotation::Count(3);
232 let mut rf = RotatingFile::open(&path, policy).unwrap();
233 rf.write_batch(b"event1\n", 1).unwrap();
234 rf.write_batch(b"event2\n", 1).unwrap();
235 rf.write_batch(b"event3\n", 1).unwrap(); let entries: Vec<_> = fs::read_dir(dir.path())
237 .unwrap()
238 .filter_map(Result::ok)
239 .collect();
240 assert!(entries.len() >= 2, "expected rotated file");
241 }
242}