1use crate::logging::logger::byte_buffer::BufBytesSink;
15use async_compression::tokio::write::GzipEncoder;
16use bytes::{Buf, Bytes};
17use conjure_error::Error;
18use conjure_object::chrono::NaiveDate;
19use conjure_object::Utc;
20use futures_sink::Sink;
21use futures_util::ready;
22use pin_project::pin_project;
23use regex::Regex;
24use std::future::Future;
25use std::path::{Path, PathBuf};
26use std::pin::Pin;
27use std::sync::Arc;
28use std::task::{Context, Poll};
29use tokio::fs::{self, File, OpenOptions};
30use tokio::io::{self, AsyncWrite, AsyncWriteExt};
31use tokio::task;
32
33const MAX_LOG_SIZE: u64 = 1024 * 1024 * 1024;
34
35struct CurrentFile {
36 sink: BufBytesSink<FileBytesSink>,
37 len: u64,
38 date: NaiveDate,
39}
40
41impl CurrentFile {
42 fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
43 Pin::new(&mut self.sink).poll_ready(cx)
44 }
45
46 fn start_send(&mut self, item: Bytes) -> io::Result<()> {
47 self.len += item.remaining() as u64;
48 Pin::new(&mut self.sink).start_send(item)
49 }
50
51 fn poll_flush(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
52 Pin::new(&mut self.sink).poll_flush(cx)
53 }
54
55 fn poll_close(&mut self, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
56 Pin::new(&mut self.sink).poll_close(cx)
57 }
58}
59
60#[allow(clippy::large_enum_variant)]
61enum State {
62 Live(CurrentFile),
63 Rotating(Pin<Box<dyn Future<Output = io::Result<File>> + Sync + Send>>),
64}
65
66pub struct RollingFileAppender {
67 state: State,
68 next_archive_index: u32,
69 name: &'static str,
70 max_archive_size: u64,
71 max_archive_days: u32,
72 archive_locator: Arc<ArchiveLocator>,
73}
74
75impl RollingFileAppender {
76 pub async fn new(
77 name: &'static str,
78 size_limit_gb: u32,
79 max_archive_days: u32,
80 ) -> Result<Self, Error> {
81 let max_archive_size = u64::from(size_limit_gb) * 1024 * 1024 * 1024;
82
83 let dir = log_dir();
84 fs::create_dir_all(&dir)
85 .await
86 .map_err(Error::internal_safe)?;
87 let file_path = log_path(dir, name);
88 let file = open_log(&file_path).await.map_err(Error::internal_safe)?;
89 let len = file.metadata().await.map_err(Error::internal_safe)?.len();
90
91 let archive_locator = ArchiveLocator::new(name);
92 let date = Utc::now().date_naive();
93
94 let next_archive_index = archive_locator
95 .archived_logs(dir)
96 .await
97 .map_err(Error::internal_safe)?
98 .iter()
99 .chain(
100 archive_locator
101 .uncompressed_logs(dir)
102 .await
103 .map_err(Error::internal_safe)?
104 .iter(),
105 )
106 .filter(|l| l.date == date)
107 .map(|l| l.number)
108 .max()
109 .map_or(0, |n| n + 1);
110
111 clear_old_archives(
112 dir,
113 date,
114 max_archive_size,
115 max_archive_days,
116 &archive_locator,
117 )
118 .await
119 .map_err(Error::internal_safe)?;
120
121 clear_tmp_files(dir, &archive_locator)
122 .await
123 .map_err(Error::internal_safe)?;
124 restart_compression(dir, name, &archive_locator)
125 .await
126 .map_err(Error::internal_safe)?;
127
128 Ok(RollingFileAppender {
129 state: State::Live(CurrentFile {
130 sink: BufBytesSink::new(FileBytesSink::new(file)),
131 len,
132 date,
133 }),
134 next_archive_index,
135 name,
136 max_archive_size,
137 max_archive_days,
138 archive_locator: Arc::new(archive_locator),
139 })
140 }
141}
142
143impl Sink<Bytes> for RollingFileAppender {
144 type Error = io::Error;
145
146 fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
147 loop {
148 let this = &mut *self;
149 match &mut this.state {
150 State::Live(file) => {
151 let date = Utc::now().date_naive();
152 if file.len < MAX_LOG_SIZE && date <= file.date {
153 return file.poll_ready(cx);
154 }
155
156 ready!(file.poll_close(cx))?;
157
158 let number = this.next_archive_index;
159 if date > file.date {
160 this.next_archive_index = 0;
161 } else {
162 this.next_archive_index += 1;
163 }
164
165 this.state = State::Rotating(Box::pin(rotate(
166 log_dir(),
167 this.name,
168 file.date,
169 number,
170 this.max_archive_size,
171 this.max_archive_days,
172 this.archive_locator.clone(),
173 )));
174 }
175 State::Rotating(future) => match ready!(future.as_mut().poll(cx)) {
176 Ok(file) => {
177 self.state = State::Live(CurrentFile {
178 sink: BufBytesSink::new(FileBytesSink::new(file)),
179 len: 0,
180 date: Utc::now().date_naive(),
181 });
182 }
183 Err(e) => {
184 let path = log_path(log_dir(), this.name);
185 this.state =
186 State::Rotating(Box::pin(async move { open_log(&path).await }));
187 return Poll::Ready(Err(e));
188 }
189 },
190 }
191 }
192 }
193
194 fn start_send(mut self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
195 match &mut self.state {
196 State::Live(file) => file.start_send(item),
197 State::Rotating(_) => panic!("start_send called without poll_ready"),
198 }
199 }
200
201 fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
202 match &mut self.state {
203 State::Live(file) => file.poll_flush(cx),
204 State::Rotating(_) => Poll::Ready(Ok(())),
205 }
206 }
207
208 fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
209 match &mut self.state {
210 State::Live(file) => file.poll_close(cx),
211 State::Rotating(_) => Poll::Ready(Ok(())),
212 }
213 }
214}
215
216async fn open_log(path: &Path) -> io::Result<File> {
217 OpenOptions::new()
218 .write(true)
219 .append(true)
220 .create(true)
221 .open(path)
222 .await
223}
224
225fn log_dir() -> &'static Path {
226 Path::new("var/log")
227}
228
229fn log_path(dir: &Path, name: &str) -> PathBuf {
230 let mut path = dir.to_path_buf();
231 path.push(format!("{name}.log"));
232 path
233}
234
235fn archive_path(dir: &Path, name: &str, date: NaiveDate, number: u32) -> PathBuf {
236 let mut path = dir.to_path_buf();
237 path.push(format!("{name}-{date}-{number}.log"));
238 path
239}
240
241fn archive_gz_tmp_path(dir: &Path, name: &str, date: NaiveDate, number: u32) -> PathBuf {
242 let mut path = dir.to_path_buf();
243 path.push(format!("{name}-{date}-{number}.log.gz.tmp"));
244 path
245}
246
247fn archive_gz_path(dir: &Path, name: &str, date: NaiveDate, number: u32) -> PathBuf {
248 let mut path = dir.to_path_buf();
249 path.push(format!("{name}-{date}-{number}.log.gz"));
250 path
251}
252
253async fn clear_old_archives(
254 dir: &Path,
255 date: NaiveDate,
256 max_archive_size: u64,
257 max_archive_days: u32,
258 archive_locator: &ArchiveLocator,
259) -> io::Result<()> {
260 let logs = archive_locator.archived_logs(dir).await?;
261 clear_old_archives_inner(date, max_archive_size, max_archive_days, logs).await
262}
263
264async fn clear_old_archives_inner(
266 date: NaiveDate,
267 max_archive_size: u64,
268 max_archive_days: u32,
269 mut logs: Vec<ArchivedLog>,
270) -> io::Result<()> {
271 logs.sort_by_key(|l| (l.date, l.number));
272
273 let mut total_size = logs.iter().map(|l| l.len).sum::<u64>();
274
275 let mut date_cutoff = date;
276 for _ in 0..max_archive_days {
278 date_cutoff = date_cutoff.pred_opt().unwrap();
279 }
280
281 for log in logs {
282 if log.date >= date_cutoff && total_size < max_archive_size {
283 break;
284 }
285
286 let _ = fs::remove_file(&log.path).await;
288 total_size -= log.len;
289 }
290
291 Ok(())
292}
293
294async fn clear_tmp_files(dir: &Path, archive_locator: &ArchiveLocator) -> io::Result<()> {
295 for log in archive_locator.tmp_files(dir).await? {
296 fs::remove_file(&log.path).await?;
297 }
298
299 Ok(())
300}
301
302async fn restart_compression(
303 dir: &Path,
304 name: &str,
305 archive_locator: &ArchiveLocator,
306) -> io::Result<()> {
307 for log in archive_locator.uncompressed_logs(dir).await? {
308 let dir = dir.to_path_buf();
309 let name = name.to_string();
310 task::spawn(async move {
311 let _ = compress(&dir, &name, log.date, log.number).await;
312 });
313 }
314
315 Ok(())
316}
317
318async fn compress(dir: &Path, name: &str, date: NaiveDate, number: u32) -> io::Result<()> {
319 let source_path = archive_path(dir, name, date, number);
320 let mut source = File::open(&source_path).await?;
321
322 let tmp_path = archive_gz_tmp_path(dir, name, date, number);
323 let target = File::create(&tmp_path).await?;
324 let mut target = GzipEncoder::new(target);
325
326 io::copy(&mut source, &mut target).await?;
327 target.shutdown().await?;
328
329 let path = archive_gz_path(dir, name, date, number);
330 fs::rename(&tmp_path, &path).await?;
331
332 fs::remove_file(&source_path).await?;
333
334 Ok(())
335}
336
337async fn rotate(
338 dir: &Path,
339 name: &'static str,
340 date: NaiveDate,
341 number: u32,
342 max_archive_size: u64,
343 max_archive_days: u32,
344 archive_locator: Arc<ArchiveLocator>,
345) -> io::Result<File> {
346 let log_path = log_path(dir, name);
347 let tmp_path = archive_path(dir, name, date, number);
348
349 fs::rename(&log_path, &tmp_path).await?;
350
351 let dir = dir.to_path_buf();
352 task::spawn(async move {
353 let _ = compress(&dir, name, date, number).await;
354 let _ = clear_old_archives(
356 &dir,
357 Utc::now().date_naive(),
358 max_archive_size,
359 max_archive_days,
360 &archive_locator,
361 )
362 .await;
363 });
364
365 open_log(&log_path).await
366}
367
368struct ArchiveLocator {
369 gz_regex: Regex,
370 gz_tmp_regex: Regex,
371 raw_regex: Regex,
372}
373
374impl ArchiveLocator {
375 fn new(name: &str) -> ArchiveLocator {
376 let gz_regex = format!(
377 r"^{}-(\d{{4}})-(\d{{2}})-(\d{{2}})-(\d+)\.log\.gz$",
378 regex::escape(name)
379 );
380 let gz_tmp_regex = format!(
381 r"^{}-(\d{{4}})-(\d{{2}})-(\d{{2}})-(\d+)\.log\.gz\.tmp$",
382 regex::escape(name)
383 );
384 let raw_regex = format!(
385 r"^{}-(\d{{4}})-(\d{{2}})-(\d{{2}})-(\d+)\.log$",
386 regex::escape(name)
387 );
388 ArchiveLocator {
389 gz_regex: Regex::new(&gz_regex).unwrap(),
390 gz_tmp_regex: Regex::new(&gz_tmp_regex).unwrap(),
391 raw_regex: Regex::new(&raw_regex).unwrap(),
392 }
393 }
394
395 async fn uncompressed_logs(&self, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
396 self.get_logs(&self.raw_regex, dir).await
397 }
398
399 async fn tmp_files(&self, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
400 self.get_logs(&self.gz_tmp_regex, dir).await
401 }
402
403 async fn archived_logs(&self, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
404 self.get_logs(&self.gz_regex, dir).await
405 }
406
407 async fn get_logs(&self, regex: &Regex, dir: &Path) -> io::Result<Vec<ArchivedLog>> {
408 let mut logs = vec![];
409 let mut files = fs::read_dir(dir).await?;
410 while let Some(file) = files.next_entry().await? {
411 let name = file.file_name();
412 let name = match name.to_str() {
413 Some(name) => name,
414 None => continue,
415 };
416
417 let captures = match regex.captures(name) {
418 Some(captures) => captures,
419 None => continue,
420 };
421
422 let year = captures[1].parse().unwrap();
423 let month = captures[2].parse().unwrap();
424 let day = captures[3].parse().unwrap();
425 let date = match NaiveDate::from_ymd_opt(year, month, day) {
426 Some(date) => date,
427 None => continue,
428 };
429
430 let number = match captures[4].parse() {
431 Ok(number) => number,
432 Err(_) => continue,
433 };
434
435 let len = file.metadata().await?.len();
436
437 let log = ArchivedLog {
438 path: file.path(),
439 date,
440 number,
441 len,
442 };
443 logs.push(log);
444 }
445 Ok(logs)
446 }
447}
448
449#[derive(Debug, PartialEq)]
450struct ArchivedLog {
451 path: PathBuf,
452 date: NaiveDate,
453 number: u32,
454 len: u64,
455}
456
457#[pin_project]
458struct FileBytesSink {
459 #[pin]
460 file: File,
461 pending: Bytes,
462}
463
464impl FileBytesSink {
465 fn new(file: File) -> Self {
466 FileBytesSink {
467 file,
468 pending: Bytes::new(),
469 }
470 }
471}
472
473impl Sink<Bytes> for FileBytesSink {
474 type Error = io::Error;
475
476 fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
477 self.poll_flush(cx)
478 }
479
480 fn start_send(self: Pin<&mut Self>, item: Bytes) -> Result<(), Self::Error> {
481 let this = self.project();
482 debug_assert!(this.pending.is_empty());
483 *this.pending = item;
484
485 Ok(())
486 }
487
488 fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
489 let mut this = self.project();
490
491 while !this.pending.is_empty() {
492 let nwritten = ready!(this.file.as_mut().poll_write(cx, this.pending))?;
493 this.pending.advance(nwritten);
494 }
495 ready!(this.file.poll_flush(cx))?;
496
497 Poll::Ready(Ok(()))
498 }
499
500 fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
501 self.poll_flush(cx)
502 }
503}
504
505#[cfg(test)]
506mod test {
507 use super::*;
508 use async_compression::tokio::bufread::GzipDecoder;
509 use tokio::io::AsyncReadExt;
510
511 #[test]
512 fn log_path_format() {
513 let name = "service";
514
515 assert_eq!(log_path(log_dir(), name), Path::new("var/log/service.log"));
516 }
517
518 #[test]
519 fn archive_tmp_path_format() {
520 let name = "service";
521 let date = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
522 let number = 3;
523
524 assert_eq!(
525 archive_path(log_dir(), name, date, number),
526 Path::new("var/log/service-2017-04-20-3.log"),
527 );
528 }
529
530 #[test]
531 fn archive_path_format() {
532 let name = "service";
533 let date = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
534 let number = 3;
535
536 assert_eq!(
537 archive_gz_path(log_dir(), name, date, number),
538 Path::new("var/log/service-2017-04-20-3.log.gz"),
539 );
540 }
541
542 #[tokio::test]
543 async fn compress_validity() {
544 let dir = tempfile::tempdir().unwrap();
545
546 let name = "service";
547 let date = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
548 let number = 3;
549
550 let tmp_path = archive_path(dir.path(), name, date, number);
551 let mut file = open_log(&tmp_path).await.unwrap();
552 file.write_all(b"hello world").await.unwrap();
553 file.flush().await.unwrap();
554
555 compress(dir.path(), name, date, number).await.unwrap();
556
557 let archive_path = archive_gz_path(dir.path(), name, date, number);
558 let file = fs::read(&archive_path).await.unwrap();
559 let mut buf = vec![];
560 GzipDecoder::new(&mut &file[..])
561 .read_to_end(&mut buf)
562 .await
563 .unwrap();
564
565 assert_eq!(buf, b"hello world");
566 }
567
568 #[tokio::test]
569 async fn archive_locator() {
570 let dir = tempfile::tempdir().unwrap();
571
572 let service_path = log_path(dir.path(), "service");
573 let file = File::create(&service_path).await.unwrap();
574 file.set_len(1).await.unwrap();
575
576 let requests_path = log_path(dir.path(), "requests");
577 let file = File::create(&requests_path).await.unwrap();
578 file.set_len(2).await.unwrap();
579
580 let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
581 let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
582 let file = File::create(&service_archive_1_0_path).await.unwrap();
583 file.set_len(3).await.unwrap();
584
585 let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
586 let file = File::create(&service_archive_1_1_path).await.unwrap();
587 file.set_len(4).await.unwrap();
588
589 let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
590 let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
591 let file = File::create(&service_archive_2_0_path).await.unwrap();
592 file.set_len(5).await.unwrap();
593
594 let service_archive_2_1_path = archive_gz_path(dir.path(), "service", day2, 1);
595 let file = File::create(&service_archive_2_1_path).await.unwrap();
596 file.set_len(6).await.unwrap();
597
598 let requests_archive_1_0_path = archive_gz_path(dir.path(), "requests", day1, 0);
599 let file = File::create(&requests_archive_1_0_path).await.unwrap();
600 file.set_len(7).await.unwrap();
601
602 let locator = ArchiveLocator::new("service");
603
604 let mut logs = locator.archived_logs(dir.path()).await.unwrap();
605 logs.sort_by_key(|l| (l.date, l.number));
606
607 let expected = [
608 ArchivedLog {
609 path: service_archive_1_0_path,
610 date: day1,
611 number: 0,
612 len: 3,
613 },
614 ArchivedLog {
615 path: service_archive_1_1_path,
616 date: day1,
617 number: 1,
618 len: 4,
619 },
620 ArchivedLog {
621 path: service_archive_2_0_path,
622 date: day2,
623 number: 0,
624 len: 5,
625 },
626 ArchivedLog {
627 path: service_archive_2_1_path,
628 date: day2,
629 number: 1,
630 len: 6,
631 },
632 ];
633
634 assert_eq!(logs, expected);
635 }
636
637 #[tokio::test]
638 async fn clear_old_archives_always_deletes_old_logs() {
639 let dir = tempfile::tempdir().unwrap();
640
641 let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
642 let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
643 File::create(&service_archive_1_0_path).await.unwrap();
644
645 let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
646 File::create(&service_archive_1_1_path).await.unwrap();
647
648 let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
649 let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
650 File::create(&service_archive_2_0_path).await.unwrap();
651
652 let service_archive_2_1_tmp_path = archive_path(dir.path(), "service", day2, 1);
653 File::create(&service_archive_2_1_tmp_path).await.unwrap();
654
655 let logs = vec![
656 ArchivedLog {
657 path: service_archive_1_0_path.clone(),
658 date: day1,
659 number: 0,
660 len: 0,
661 },
662 ArchivedLog {
663 path: service_archive_1_1_path.clone(),
664 date: day1,
665 number: 1,
666 len: 0,
667 },
668 ArchivedLog {
669 path: service_archive_2_0_path.clone(),
670 date: day2,
671 number: 0,
672 len: 0,
673 },
674 ArchivedLog {
675 path: service_archive_2_1_tmp_path.clone(),
676 date: day2,
677 number: 1,
678 len: 0,
679 },
680 ];
681
682 let date = NaiveDate::from_ymd_opt(2017, 5, 21).unwrap();
683 clear_old_archives_inner(date, 1024 * 1024 * 1024, 30, logs)
684 .await
685 .unwrap();
686
687 assert!(!service_archive_1_0_path.exists());
688 assert!(!service_archive_1_1_path.exists());
689 assert!(service_archive_2_0_path.exists());
690 assert!(service_archive_2_1_tmp_path.exists());
691 }
692
693 #[tokio::test]
694 async fn clear_old_archives_deletes_to_save_space() {
695 let dir = tempfile::tempdir().unwrap();
696
697 let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
698 let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
699 File::create(&service_archive_1_0_path).await.unwrap();
700
701 let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
702 File::create(&service_archive_1_1_path).await.unwrap();
703
704 let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
705 let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
706 File::create(&service_archive_2_0_path).await.unwrap();
707
708 let service_archive_2_1_tmp_path = archive_gz_tmp_path(dir.path(), "service", day2, 1);
709 File::create(&service_archive_2_1_tmp_path).await.unwrap();
710
711 let logs = vec![
712 ArchivedLog {
713 path: service_archive_1_0_path.clone(),
714 date: day1,
715 number: 0,
716 len: 4,
717 },
718 ArchivedLog {
719 path: service_archive_1_1_path.clone(),
720 date: day1,
721 number: 1,
722 len: 5,
723 },
724 ArchivedLog {
725 path: service_archive_2_0_path.clone(),
726 date: day2,
727 number: 0,
728 len: 1,
729 },
730 ArchivedLog {
731 path: service_archive_2_1_tmp_path.clone(),
732 date: day2,
733 number: 1,
734 len: 1023,
735 },
736 ];
737
738 let date = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
739 clear_old_archives_inner(date, 1024, 30, logs)
740 .await
741 .unwrap();
742
743 assert!(!service_archive_1_0_path.exists());
744 assert!(!service_archive_1_1_path.exists());
745 assert!(!service_archive_2_0_path.exists());
746 assert!(service_archive_2_1_tmp_path.exists());
747 }
748
749 #[tokio::test]
750 async fn clear_old_archives_ignores_missing_files() {
751 let dir = tempfile::tempdir().unwrap();
752
753 let day1 = NaiveDate::from_ymd_opt(2017, 4, 20).unwrap();
754 let service_archive_1_0_path = archive_gz_path(dir.path(), "service", day1, 0);
755 let service_archive_1_1_path = archive_gz_path(dir.path(), "service", day1, 1);
758 File::create(&service_archive_1_1_path).await.unwrap();
759
760 let day2 = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
761 let service_archive_2_0_path = archive_gz_path(dir.path(), "service", day2, 0);
762 File::create(&service_archive_2_0_path).await.unwrap();
763
764 let service_archive_2_1_tmp_path = archive_gz_tmp_path(dir.path(), "service", day2, 1);
765 File::create(&service_archive_2_1_tmp_path).await.unwrap();
766
767 let logs = vec![
768 ArchivedLog {
769 path: service_archive_1_0_path.clone(),
770 date: day1,
771 number: 0,
772 len: 4,
773 },
774 ArchivedLog {
775 path: service_archive_1_1_path.clone(),
776 date: day1,
777 number: 1,
778 len: 5,
779 },
780 ArchivedLog {
781 path: service_archive_2_0_path.clone(),
782 date: day2,
783 number: 0,
784 len: 1,
785 },
786 ArchivedLog {
787 path: service_archive_2_1_tmp_path.clone(),
788 date: day2,
789 number: 1,
790 len: 1023,
791 },
792 ];
793
794 let date = NaiveDate::from_ymd_opt(2017, 4, 21).unwrap();
795 clear_old_archives_inner(date, 1024, 30, logs)
796 .await
797 .unwrap();
798
799 assert!(!service_archive_1_0_path.exists());
800 assert!(!service_archive_1_1_path.exists());
801 assert!(!service_archive_2_0_path.exists());
802 assert!(service_archive_2_1_tmp_path.exists());
803 }
804}