1#![deny(warnings)]
26
27use chrono::prelude::*;
28use std::{
29 convert::TryFrom,
30 ffi::OsString,
31 fs,
32 fs::{File, OpenOptions},
33 io,
34 io::{BufWriter, Write},
35 path::Path,
36};
37
38pub trait RollingCondition {
40 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
42}
43
44#[derive(Copy, Clone, Debug, Eq, PartialEq)]
46pub enum RollingFrequency {
47 EveryDay,
48 EveryHour,
49 EveryMinute,
50}
51
52impl RollingFrequency {
53 pub fn equivalent_datetime(&self, dt: &DateTime<Local>) -> DateTime<Local> {
56 match self {
57 RollingFrequency::EveryDay => Local
58 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
59 .unwrap(),
60 RollingFrequency::EveryHour => Local
61 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
62 .unwrap(),
63 RollingFrequency::EveryMinute => Local
64 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), 0)
65 .unwrap(),
66 }
67 }
68}
69
70#[derive(Copy, Clone, Debug, Eq, PartialEq)]
81pub struct RollingConditionBasic {
82 last_write_opt: Option<DateTime<Local>>,
83 frequency_opt: Option<RollingFrequency>,
84 max_size_opt: Option<u64>,
85}
86
87impl RollingConditionBasic {
88 pub fn new() -> RollingConditionBasic {
90 RollingConditionBasic {
91 last_write_opt: None,
92 frequency_opt: None,
93 max_size_opt: None,
94 }
95 }
96
97 pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBasic {
99 self.frequency_opt = Some(x);
100 self
101 }
102
103 pub fn daily(mut self) -> RollingConditionBasic {
105 self.frequency_opt = Some(RollingFrequency::EveryDay);
106 self
107 }
108
109 pub fn hourly(mut self) -> RollingConditionBasic {
111 self.frequency_opt = Some(RollingFrequency::EveryHour);
112 self
113 }
114
115 pub fn max_size(mut self, x: u64) -> RollingConditionBasic {
117 self.max_size_opt = Some(x);
118 self
119 }
120}
121
122impl Default for RollingConditionBasic {
123 fn default() -> Self {
124 RollingConditionBasic::new().frequency(RollingFrequency::EveryDay)
125 }
126}
127
128impl RollingCondition for RollingConditionBasic {
129 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
130 let mut rollover = false;
131 if let Some(frequency) = self.frequency_opt.as_ref() {
132 if let Some(last_write) = self.last_write_opt.as_ref() {
133 if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
134 rollover = true;
135 }
136 }
137 }
138 if let Some(max_size) = self.max_size_opt.as_ref() {
139 if current_filesize >= *max_size {
140 rollover = true;
141 }
142 }
143 self.last_write_opt = Some(*now);
144 rollover
145 }
146}
147
148#[derive(Debug)]
153pub struct RollingFileAppender<RC>
154where
155 RC: RollingCondition,
156{
157 condition: RC,
158 base_filename: OsString,
159 max_files: usize,
160 buffer_capacity: Option<usize>,
161 current_filesize: u64,
162 writer_opt: Option<BufWriter<File>>,
163}
164
165impl<RC> RollingFileAppender<RC>
166where
167 RC: RollingCondition,
168{
169 pub fn new<P>(path: P, condition: RC, max_files: usize) -> io::Result<RollingFileAppender<RC>>
172 where
173 P: AsRef<Path>,
174 {
175 Self::_new(path, condition, max_files, None)
176 }
177
178 pub fn new_with_buffer_capacity<P>(
181 path: P,
182 condition: RC,
183 max_files: usize,
184 buffer_capacity: usize,
185 ) -> io::Result<RollingFileAppender<RC>>
186 where
187 P: AsRef<Path>,
188 {
189 Self::_new(path, condition, max_files, Some(buffer_capacity))
190 }
191
192 fn _new<P>(
193 path: P,
194 condition: RC,
195 max_files: usize,
196 buffer_capacity: Option<usize>,
197 ) -> io::Result<RollingFileAppender<RC>>
198 where
199 P: AsRef<Path>,
200 {
201 let mut rfa = RollingFileAppender {
202 condition,
203 base_filename: path.as_ref().as_os_str().to_os_string(),
204 max_files,
205 buffer_capacity,
206 current_filesize: 0,
207 writer_opt: None,
208 };
209 rfa.open_writer_if_needed()?;
211 Ok(rfa)
212 }
213
214 fn filename_for(&self, n: usize) -> OsString {
216 let mut f = self.base_filename.clone();
217 if n > 0 {
218 f.push(OsString::from(format!(".{}", n)))
219 }
220 f
221 }
222
223 fn rotate_files(&mut self) -> io::Result<()> {
226 let _ = fs::remove_file(self.filename_for(self.max_files.max(1)));
228 let mut r = Ok(());
229 for i in (0..self.max_files.max(1)).rev() {
230 let rotate_from = self.filename_for(i);
231 let rotate_to = self.filename_for(i + 1);
232 if let Err(e) = fs::rename(rotate_from, rotate_to).or_else(|e| match e.kind() {
233 io::ErrorKind::NotFound => Ok(()),
234 _ => Err(e),
235 }) {
236 r = Err(e);
239 }
240 }
241 r
242 }
243
244 pub fn rollover(&mut self) -> io::Result<()> {
246 self.flush()?;
248 self.writer_opt.take();
250 self.current_filesize = 0;
251 self.rotate_files()?;
252 self.open_writer_if_needed()
253 }
254
255 pub fn condition_ref(&self) -> &RC {
257 &self.condition
258 }
259
260 pub fn condition_mut(&mut self) -> &mut RC {
262 &mut self.condition
263 }
264
265 fn open_writer_if_needed(&mut self) -> io::Result<()> {
267 if self.writer_opt.is_none() {
268 let p = self.filename_for(0);
269 let f = OpenOptions::new().append(true).create(true).open(&p)?;
270 self.writer_opt = Some(if let Some(capacity) = self.buffer_capacity {
271 BufWriter::with_capacity(capacity, f)
272 } else {
273 BufWriter::new(f)
274 });
275 self.current_filesize = fs::metadata(&p).map_or(0, |m| m.len());
276 }
277 Ok(())
278 }
279
280 pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
282 if self.condition.should_rollover(now, self.current_filesize) {
283 if let Err(e) = self.rollover() {
284 eprintln!(
289 "WARNING: Failed to rotate logfile {}: {}",
290 self.base_filename.to_string_lossy(),
291 e
292 );
293 }
294 }
295 self.open_writer_if_needed()?;
296 if let Some(writer) = self.writer_opt.as_mut() {
297 let buf_len = buf.len();
298 writer.write_all(buf).map(|_| {
299 self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX);
300 buf_len
301 })
302 } else {
303 Err(io::Error::new(
304 io::ErrorKind::Other,
305 "unexpected condition: writer is missing",
306 ))
307 }
308 }
309}
310
311impl<RC> io::Write for RollingFileAppender<RC>
312where
313 RC: RollingCondition,
314{
315 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
316 let now = Local::now();
317 self.write_with_datetime(buf, &now)
318 }
319
320 fn flush(&mut self) -> io::Result<()> {
321 if let Some(writer) = self.writer_opt.as_mut() {
322 writer.flush()?;
323 }
324 Ok(())
325 }
326}
327
328pub type BasicRollingFileAppender = RollingFileAppender<RollingConditionBasic>;
330
331#[cfg(test)]
333mod t {
334 use super::*;
335
336 struct Context {
337 _tempdir: tempfile::TempDir,
338 rolling: BasicRollingFileAppender,
339 }
340
341 impl Context {
342 #[track_caller]
343 fn verify_contains(&self, needle: &str, n: usize) {
344 let heystack = self.read(n);
345 if !heystack.contains(needle) {
346 panic!("file {:?} did not contain expected contents {}", self.path(n), needle);
347 }
348 }
349
350 #[track_caller]
351 fn verify_not_contains(&self, needle: &str, n: usize) {
352 let heystack = self.read(n);
353 if heystack.contains(needle) {
354 panic!("file {:?} DID contain expected contents {}", self.path(n), needle);
355 }
356 }
357
358 fn flush(&mut self) {
359 self.rolling.flush().unwrap();
360 }
361
362 fn read(&self, n: usize) -> String {
363 fs::read_to_string(self.path(n)).unwrap()
364 }
365
366 fn path(&self, n: usize) -> OsString {
367 self.rolling.filename_for(n)
368 }
369 }
370
371 fn build_context(condition: RollingConditionBasic, max_files: usize, buffer_capacity: Option<usize>) -> Context {
372 let tempdir = tempfile::tempdir().unwrap();
373 let rolling = match buffer_capacity {
374 None => BasicRollingFileAppender::new(tempdir.path().join("test.log"), condition, max_files).unwrap(),
375 Some(capacity) => BasicRollingFileAppender::new_with_buffer_capacity(
376 tempdir.path().join("test.log"),
377 condition,
378 max_files,
379 capacity,
380 )
381 .unwrap(),
382 };
383 Context {
384 _tempdir: tempdir,
385 rolling,
386 }
387 }
388
389 #[test]
390 fn frequency_every_day() {
391 let mut c = build_context(RollingConditionBasic::new().daily(), 9, None);
392 c.rolling
393 .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
394 .unwrap();
395 c.rolling
396 .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap())
397 .unwrap();
398 c.rolling
399 .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
400 .unwrap();
401 c.rolling
402 .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
403 .unwrap();
404 c.rolling
405 .write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap())
406 .unwrap();
407 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
408 c.flush();
409 c.verify_contains("Line 1", 3);
410 c.verify_contains("Line 2", 3);
411 c.verify_contains("Line 3", 2);
412 c.verify_contains("Line 4", 1);
413 c.verify_contains("Line 5", 0);
414 }
415
416 #[test]
417 fn frequency_every_day_limited_files() {
418 let mut c = build_context(RollingConditionBasic::new().daily(), 2, None);
419 c.rolling
420 .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
421 .unwrap();
422 c.rolling
423 .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap())
424 .unwrap();
425 c.rolling
426 .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
427 .unwrap();
428 c.rolling
429 .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
430 .unwrap();
431 c.rolling
432 .write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap())
433 .unwrap();
434 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
435 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
436 c.flush();
437 c.verify_contains("Line 3", 2);
438 c.verify_contains("Line 4", 1);
439 c.verify_contains("Line 5", 0);
440 }
441
442 #[test]
443 fn frequency_every_hour() {
444 let mut c = build_context(RollingConditionBasic::new().hourly(), 9, None);
445 c.rolling
446 .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
447 .unwrap();
448 c.rolling
449 .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 2).unwrap())
450 .unwrap();
451 c.rolling
452 .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 1, 0).unwrap())
453 .unwrap();
454 c.rolling
455 .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 3, 31, 2, 1, 0).unwrap())
456 .unwrap();
457 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
458 c.flush();
459 c.verify_contains("Line 1", 2);
460 c.verify_contains("Line 2", 2);
461 c.verify_contains("Line 3", 1);
462 c.verify_contains("Line 4", 0);
463 }
464
465 #[test]
466 fn frequency_every_minute() {
467 let mut c = build_context(
468 RollingConditionBasic::new().frequency(RollingFrequency::EveryMinute),
469 9,
470 None,
471 );
472 c.rolling
473 .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
474 .unwrap();
475 c.rolling
476 .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
477 .unwrap();
478 c.rolling
479 .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 4).unwrap())
480 .unwrap();
481 c.rolling
482 .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap())
483 .unwrap();
484 c.rolling
485 .write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap())
486 .unwrap();
487 c.rolling
488 .write_with_datetime(b"Line 6\n", &Local.with_ymd_and_hms(2022, 3, 30, 2, 3, 0).unwrap())
489 .unwrap();
490 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
491 c.flush();
492 c.verify_contains("Line 1", 3);
493 c.verify_contains("Line 2", 3);
494 c.verify_contains("Line 3", 3);
495 c.verify_contains("Line 4", 2);
496 c.verify_contains("Line 5", 1);
497 c.verify_contains("Line 6", 0);
498 }
499
500 #[test]
501 fn max_size() {
502 let mut c = build_context(RollingConditionBasic::new().max_size(10), 9, None);
503 c.rolling
504 .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
505 .unwrap();
506 c.rolling
507 .write_with_datetime(b"6789", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap())
508 .unwrap();
509 c.rolling
510 .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
511 .unwrap();
512 c.rolling
513 .write_with_datetime(
514 b"abcdefghijklmn",
515 &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap(),
516 )
517 .unwrap();
518 c.rolling
519 .write_with_datetime(b"ZZZ", &Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap())
520 .unwrap();
521 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
522 c.flush();
523 c.verify_contains("1234567890", 2);
524 c.verify_contains("abcdefghijklmn", 1);
525 c.verify_contains("ZZZ", 0);
526 }
527
528 #[test]
529 fn max_size_existing() {
530 let mut c = build_context(RollingConditionBasic::new().max_size(10), 9, None);
531 c.rolling
532 .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
533 .unwrap();
534 c.rolling.writer_opt.take();
537 c.rolling.current_filesize = 0;
538 c.rolling
539 .write_with_datetime(b"6789", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap())
540 .unwrap();
541 c.rolling
542 .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
543 .unwrap();
544 c.rolling
545 .write_with_datetime(
546 b"abcdefghijklmn",
547 &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap(),
548 )
549 .unwrap();
550 c.rolling
551 .write_with_datetime(b"ZZZ", &Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap())
552 .unwrap();
553 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
554 c.flush();
555 c.verify_contains("1234567890", 2);
556 c.verify_contains("abcdefghijklmn", 1);
557 c.verify_contains("ZZZ", 0);
558 }
559
560 #[test]
561 fn daily_and_max_size() {
562 let mut c = build_context(RollingConditionBasic::new().daily().max_size(10), 9, None);
563 c.rolling
564 .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
565 .unwrap();
566 c.rolling
567 .write_with_datetime(b"6789", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
568 .unwrap();
569 c.rolling
570 .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap())
571 .unwrap();
572 c.rolling
573 .write_with_datetime(
574 b"abcdefghijklmn",
575 &Local.with_ymd_and_hms(2021, 3, 31, 3, 3, 3).unwrap(),
576 )
577 .unwrap();
578 c.rolling
579 .write_with_datetime(b"ZZZ", &Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap())
580 .unwrap();
581 assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
582 c.flush();
583 c.verify_contains("123456789", 2);
584 c.verify_contains("0abcdefghijklmn", 1);
585 c.verify_contains("ZZZ", 0);
586 }
587
588 #[test]
589 fn default_buffer_capacity() {
590 let c = build_context(RollingConditionBasic::new().daily(), 9, None);
591 let default_capacity = BufWriter::new(tempfile::tempfile().unwrap()).capacity();
594 if default_capacity != 8192 {
595 eprintln!(
596 "WARN: it seems std's default capacity is changed from 8192 to {}",
597 default_capacity
598 );
599 }
600 assert_eq!(c.rolling.writer_opt.map(|b| b.capacity()), Some(default_capacity));
601 }
602
603 #[test]
604 fn large_buffer_capacity_and_flush() {
605 let mut c = build_context(RollingConditionBasic::new().daily(), 9, Some(100_000));
606 assert_eq!(c.rolling.writer_opt.as_ref().map(|b| b.capacity()), Some(100_000));
607 c.verify_not_contains("12345", 0);
608
609 loop {
611 c.rolling
612 .write_with_datetime(b"dummy", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
613 .unwrap();
614 if c.rolling.current_filesize <= 100_000 {
615 c.verify_not_contains("dummy", 0);
616 } else {
617 break;
618 }
619 }
620 c.verify_contains("dummy", 0);
621
622 c.verify_not_contains("12345", 0);
624 c.rolling
625 .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
626 .unwrap();
627 c.flush();
628 c.verify_contains("12345", 0);
629 }
630}
631