1#![deny(warnings)]
29
30use chrono::prelude::*;
31use std::{
32 convert::TryFrom,
33 fs::{self, File, OpenOptions},
34 io::{self, BufWriter, Write},
35 path::Path,
36};
37use symlink::{remove_symlink_auto, symlink_auto};
38
39pub trait RollingCondition {
41 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
43}
44
45#[derive(Copy, Clone, Debug, Eq, PartialEq)]
47pub enum RollingFrequency {
48 EveryDay,
49 EveryHour,
50 EveryMinute,
51}
52
53impl RollingFrequency {
54 pub fn equivalent_datetime(&self, dt: &DateTime<Local>) -> DateTime<Local> {
57 match self {
58 RollingFrequency::EveryDay => Local
59 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
60 .unwrap(),
61 RollingFrequency::EveryHour => Local
62 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
63 .unwrap(),
64 RollingFrequency::EveryMinute => Local
65 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), 0)
66 .unwrap(),
67 }
68 }
69}
70
71#[derive(Copy, Clone, Debug, Eq, PartialEq)]
82pub struct RollingConditionBasic {
83 last_write_opt: Option<DateTime<Local>>,
84 frequency_opt: Option<RollingFrequency>,
85 max_size_opt: Option<u64>,
86}
87
88impl RollingConditionBasic {
89 pub fn new() -> RollingConditionBasic {
91 RollingConditionBasic {
92 last_write_opt: Some(Local::now()),
93 frequency_opt: None,
94 max_size_opt: None,
95 }
96 }
97
98 pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBasic {
100 self.frequency_opt = Some(x);
101 self
102 }
103
104 pub fn daily(mut self) -> RollingConditionBasic {
106 self.frequency_opt = Some(RollingFrequency::EveryDay);
107 self
108 }
109
110 pub fn hourly(mut self) -> RollingConditionBasic {
112 self.frequency_opt = Some(RollingFrequency::EveryHour);
113 self
114 }
115
116 pub fn minutely(mut self) -> RollingConditionBasic {
117 self.frequency_opt = Some(RollingFrequency::EveryMinute);
118 self
119 }
120
121 pub fn max_size(mut self, x: u64) -> RollingConditionBasic {
123 self.max_size_opt = Some(x);
124 self
125 }
126}
127
128impl Default for RollingConditionBasic {
129 fn default() -> Self {
130 RollingConditionBasic::new().frequency(RollingFrequency::EveryDay)
131 }
132}
133
134impl RollingCondition for RollingConditionBasic {
135 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
136 let mut rollover = false;
137 if let Some(frequency) = self.frequency_opt.as_ref() {
138 if let Some(last_write) = self.last_write_opt.as_ref() {
139 if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
140 rollover = true;
141 }
142 }
143 }
144 if let Some(max_size) = self.max_size_opt.as_ref() {
145 if current_filesize >= *max_size {
146 rollover = true;
147 }
148 }
149 self.last_write_opt = Some(*now);
150 rollover
151 }
152}
153
154#[derive(Debug)]
159pub struct RollingFileAppender<RC>
160where
161 RC: RollingCondition,
162{
163 condition: RC,
164 folder: String,
165 prefix: String,
166 max_files: usize,
167 buffer_capacity: Option<usize>,
168 current_filesize: u64,
169 writer_opt: Option<BufWriter<File>>,
170}
171
172impl<RC> RollingFileAppender<RC>
173where
174 RC: RollingCondition,
175{
176 pub fn new(folder: &str, prefix: &str, condition: RC, max_files: usize) -> io::Result<RollingFileAppender<RC>> {
179 Self::_new(folder, prefix, condition, max_files, None)
180 }
181
182 pub fn new_with_buffer_capacity(
185 folder: &str,
186 prefix: &str,
187 condition: RC,
188 max_files: usize,
189 buffer_capacity: usize,
190 ) -> io::Result<RollingFileAppender<RC>> {
191 Self::_new(folder, prefix, condition, max_files, Some(buffer_capacity))
192 }
193
194 fn _new(
195 folder: &str,
196 prefix: &str,
197 condition: RC,
198 max_files: usize,
199 buffer_capacity: Option<usize>,
200 ) -> io::Result<RollingFileAppender<RC>> {
201 let folder = folder.to_string();
202 let prefix = prefix.to_string();
203 let mut rfa = RollingFileAppender {
204 condition,
205 folder,
206 prefix,
207 max_files,
208 buffer_capacity,
209 current_filesize: 0,
210 writer_opt: None,
211 };
212 rfa.open_writer_if_needed(&Local::now())?;
214 Ok(rfa)
215 }
216
217 fn check_and_remove_log_file(&mut self) -> io::Result<()> {
218 let files = std::fs::read_dir(&self.folder)?;
219
220 let mut log_files = vec![];
221 for f in files.flatten() {
222 let fname = f.file_name().to_string_lossy().to_string();
223 if fname.starts_with(&self.prefix) && fname != self.prefix {
224 log_files.push(fname);
225 }
226 }
227
228 log_files.sort_by(|a, b| b.cmp(a));
229
230 if log_files.len() > self.max_files {
231 for f in log_files.drain(self.max_files..) {
232 let p = Path::new(&self.folder).join(f);
233 if let Err(e) = fs::remove_file(&p) {
234 tracing::error!("WARNING: Failed to remove old logfile {}: {}", p.to_string_lossy(), e);
235 }
236 }
237 }
238 Ok(())
239 }
240
241 pub fn rollover(&mut self) -> io::Result<()> {
243 self.flush()?;
245 self.writer_opt.take();
247 self.current_filesize = 0;
248 Ok(())
249 }
250
251 pub fn condition_ref(&self) -> &RC {
253 &self.condition
254 }
255
256 pub fn condition_mut(&mut self) -> &mut RC {
258 &mut self.condition
259 }
260
261 fn new_file_name(&self, now: &DateTime<Local>) -> String {
262 let data_str = now.format("%Y%m%d.%H%M%S").to_string();
263 format!("{}.{}", self.prefix, data_str)
264 }
265
266 fn open_writer_if_needed(&mut self, now: &DateTime<Local>) -> io::Result<()> {
268 if self.writer_opt.is_none() {
269 let p = self.new_file_name(now);
270 let new_file_path = std::path::Path::new(&self.folder).join(&p);
271 if std::fs::metadata(&self.folder).is_err() {
272 std::fs::create_dir_all(&self.folder)?;
273 }
274 let f = OpenOptions::new().append(true).create(true).open(&new_file_path)?;
275 self.writer_opt = Some(if let Some(capacity) = self.buffer_capacity {
276 BufWriter::with_capacity(capacity, f)
277 } else {
278 BufWriter::new(f)
279 });
280 {
282 let folder = std::path::Path::new(&self.folder);
283 if let Ok(path) = folder.canonicalize() {
284 let latest_log_symlink = path.join(&self.prefix);
285 let _ = remove_symlink_auto(folder.join(&self.prefix));
286 let _ = symlink_auto(new_file_path.canonicalize().unwrap(), latest_log_symlink);
287 }
288 }
289 self.current_filesize = fs::metadata(&p).map_or(0, |m| m.len());
290 self.check_and_remove_log_file()?;
291 }
292 Ok(())
293 }
294
295 pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
297 if self.condition.should_rollover(now, self.current_filesize) {
298 if let Err(e) = self.rollover() {
299 eprintln!("WARNING: Failed to rotate logfile {}", e);
304 }
305 }
306 self.open_writer_if_needed(now)?;
307 if let Some(writer) = self.writer_opt.as_mut() {
308 let buf_len = buf.len();
309 writer.write_all(buf).map(|_| {
310 self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX);
311 buf_len
312 })
313 } else {
314 Err(io::Error::new(
315 io::ErrorKind::Other,
316 "unexpected condition: writer is missing",
317 ))
318 }
319 }
320}
321
322impl<RC> io::Write for RollingFileAppender<RC>
323where
324 RC: RollingCondition,
325{
326 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
327 let now = Local::now();
328 self.write_with_datetime(buf, &now)
329 }
330
331 fn flush(&mut self) -> io::Result<()> {
332 if let Some(writer) = self.writer_opt.as_mut() {
333 writer.flush()?;
334 }
335 Ok(())
336 }
337}
338
339pub type BasicRollingFileAppender = RollingFileAppender<RollingConditionBasic>;
341
342#[cfg(test)]
343mod t {
344 #[test]
345 fn test_number_of_log_files() {
346 use super::*;
347 let folder = "./log";
348 let prefix = "log.log";
349
350 let _ = std::fs::remove_dir_all(folder);
351 std::fs::create_dir(folder).unwrap();
352
353 let condition = RollingConditionBasic::new().hourly();
354 let max_files = 3;
355 let mut rfa = RollingFileAppender::new(folder, prefix, condition, max_files).unwrap();
356 rfa.write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
357 .unwrap();
358 rfa.write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap())
359 .unwrap();
360 rfa.write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
361 .unwrap();
362 rfa.write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
363 .unwrap();
364 rfa.write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 2, 4, 0).unwrap())
365 .unwrap();
366 rfa.flush().unwrap();
367 let files = std::fs::read_dir(folder).unwrap();
368 let mut log_files = vec![];
369 for f in files.flatten() {
370 let fname = f.file_name().to_string_lossy().to_string();
371 if fname.starts_with(prefix) && fname != "log.log" {
372 log_files.push(fname);
373 }
374 }
375 assert_eq!(log_files.len(), max_files);
376 }
377}