1#![deny(missing_docs)]
2#![doc=include_str!("../README.md")]
3
4#[cfg(test)]
5use std::eprintln as tprintln;
6
7#[cfg(not(test))]
8macro_rules! tprintln { {$($x:tt)* } => {} }
9
10use std::collections::{HashMap, BTreeMap};
11use std::fmt::{self, Debug, Display};
12use std::fs::{self, File};
13use std::io::{self, LineWriter, Write as _};
14use std::ops::Add;
15use std::path::{Path, PathBuf};
16use std::sync::{Mutex, MutexGuard};
17use std::time::{Duration, SystemTime as RawSystemTime};
18
19use chrono::{NaiveDateTime, NaiveTime, Weekday, Utc};
20use chrono::{Datelike as _, Timelike as _};
21use derive_more::{From, Into};
22use itertools::Itertools;
23use serde::{Deserialize, Deserializer, Serialize};
24use thiserror::Error;
25use tracing::{Level, Metadata};
26use tracing::metadata::LevelFilter;
27use tracing_subscriber::fmt::MakeWriter;
28
29type DateTimeUtc = chrono::DateTime<Utc>;
30
31#[cfg(test)]
32use humantime_serde::re::humantime::format_rfc3339_nanos as f3339;
33
34const SUFFIX: &str = ".log";
35
36#[derive(Copy, Clone, PartialEq, Eq, Debug)]
40#[derive(strum::EnumString, strum::Display)]
41#[derive(Serialize)]
42#[cfg_attr(test, derive(strum::EnumIter))]
43#[strum(serialize_all = "snake_case")]
44#[serde(into = "String")]
45#[allow(missing_docs)]
46pub enum Interval {
47 Hour,
48 Day,
49 Week,
50 Month,
51}
52
53pub struct Writer<'a> {
55 level: Level,
56 appender: &'a Appender,
57}
58
59#[derive(From, Into, Deserialize, Serialize, Debug, Default, Clone)]
67#[serde(try_from = "ConfigSerde", into="ConfigSerde")]
68#[cfg_attr(test, derive(Eq, PartialEq))]
69pub struct Config(BTreeMap<LevelFilter, LevelConfig>);
70
71pub struct Appender {
73 inner: Mutex<Inner>,
74}
75
76#[derive(Clone, Debug, Deserialize, Serialize)]
81#[cfg_attr(test, derive(Eq, PartialEq))]
82pub struct LevelConfig {
83 pub interval: Interval,
85
86 #[serde(default = "max_files_default")]
90 pub max_files: u32,
91}
92
93fn max_files_default() -> u32 { 15 }
94
95#[derive(PartialEq, Eq, Debug)]
98struct Period {
99 start: NaiveDateTime,
100 interval: Interval,
101}
102
103struct Inner {
104 common: Common,
105 outputs: Vec<Output>,
106}
107
108struct Common {
109 dir: PathBuf,
110 tprov: TimeProvider,
111 max_max_age_backstop: Duration,
112}
113
114struct Output {
115 level: LevelFilter,
116 config: LevelConfig,
117 period: Period,
118 file: OutputFile,
119}
120
121struct OutputFile {
122 file: LineWriter<File>,
123 path: PathBuf,
124}
125
126#[derive(Error, Debug)]
127#[error("{}: {op}: {ioe}", path.display())]
128struct Error {
129 #[source]
130 ioe: io::Error,
131 path: PathBuf,
132 op: String,
133}
134
135type ConfigSerde = HashMap<String, LevelConfig>;
136
137#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
140struct SystemTime(RawSystemTime);
141
142enum TimeProvider {
143 System,
144
145 #[cfg(test)]
146 Test(test::TestTimeProvider),
147}
148
149impl Appender {
154 pub fn new(dir: PathBuf, levels: &Config) -> io::Result<Self> {
161 Self::new_with_time_provider(dir, levels, TimeProvider::System)
162 }
163
164 fn new_with_time_provider(
165 dir: PathBuf,
166 levels: &Config,
167 tprov: TimeProvider,
168 ) -> io::Result<Self> {
169 let now = tprov.now();
170 let max_max_age_backstop = levels.0.iter().map(|(_level, config)| {
171 config.max_age_backstop()
172 })
173 .max()
174 .unwrap_or(Duration::MAX);
175
176 let common = Common { dir, tprov, max_max_age_backstop };
177 let outputs = levels.0.iter()
178 .filter(|(_, config)| config.max_files > 0)
179 .map(|(&level, config)| {
180 let period = Period::containing(now, config.interval);
181 let file = common.open_file(now, level, &config, &period)?;
182 let config = config.clone();
183 Ok::<_, Error>(Output { level, config, period, file })
184 }).try_collect()?;
185 let inner = Mutex::new(Inner { common, outputs });
186 Ok(Appender { inner })
187 }
188
189 fn lock(&self) -> MutexGuard<'_, Inner> {
190 self.inner.lock().expect("lock poisoned!")
191 }
192
193 fn make_writer_for_level<'a>(&'a self, level: Level) -> Writer<'a> {
194 Writer {
195 level,
196 appender: &self,
197 }
198 }
199}
200
201impl Common {
202 fn open_file(
203 &self,
204 now: SystemTime,
205 level: LevelFilter,
206 config: &LevelConfig,
207 period: &Period,
208 ) -> Result<OutputFile, Error> {
209 let prefix = {
210 let mut l = format!("{}_", level);
211 l.make_ascii_lowercase();
212 l
213 };
214
215 let this_leaf = format!("{prefix}{period}{SUFFIX}");
216 let this_path = self.leaf2path(&this_leaf);
217
218 tprintln!("XX R{} OPENING {}", f3339(RawSystemTime::now()), this_leaf);
219
220 let this_file = File::options()
221 .append(true)
222 .create(true)
223 .read(false)
224 .truncate(false)
225 .open(&this_path)
226 .map_err(Error::err_mapper("open", &this_path))?;
227
228 let mut del_candidates = vec![];
229 let max_age_backstop = config.max_age_backstop();
230
231 self.scan_for_expire(
232 now,
233 |del_leaf, age| {
234 if age > self.max_max_age_backstop {
235 return self.delete_leaf(del_leaf, ">max_backstop");
236 }
237 if ! del_leaf.starts_with(&prefix) {
238 return Ok(())
239 }
240 if age > max_age_backstop && *del_leaf < *this_leaf {
241 self.delete_leaf(del_leaf, ">backstop")?;
242 } else {
243 del_candidates.push(del_leaf.to_owned());
244 }
245 Ok(())
246 },
247 )?;
248
249 del_candidates.sort_by(|a, b| b.cmp(a));
251
252 while {
253 del_candidates.len()
254 >
255 usize::try_from(config.max_files).unwrap_or(usize::MAX)
256 } {
257 let del_leaf = del_candidates.pop().expect("empty!");
258 if del_leaf >= this_leaf { break; }
259 self.delete_leaf(&del_leaf, "max_files")?;
260 }
261
262 Ok(OutputFile {
263 file: LineWriter::new(this_file),
264 path: this_path,
265 })
266 }
267
268 fn leaf2path(&self, leaf: &str) -> PathBuf {
269 let mut path = self.dir.clone();
270 path.push(leaf);
271 path
272 }
273}
274
275impl Output {
276 fn if_level(&mut self, event: Level) -> Option<&mut Self> {
277 if event <= self.level { Some(self)
279 } else {
280 None
281 }
282 }
283
284 fn consider_reopen(&mut self, common: &Common) -> Result<(), Error> {
285 let now = common.tprov.now();
286 let period = Period::containing(now, self.config.interval);
287 if self.period == period { return Ok(()) }
288
289 self.file.file.flush().map_err(self.err_mapper("flush"))?;
290 let file = common.open_file(now, self.level, &self.config, &period)?;
291 self.file = file;
292 self.period = period;
293 Ok(())
294 }
295}
296
297impl Common {
300 fn scan_for_expire(
301 &self,
302 now: SystemTime,
303 mut each_leaf: impl FnMut(&str, Duration) -> Result<(), Error>,
304 ) -> Result<(), Error> {
305 let err_mapper_readdir = || Error::err_mapper("read dir", &self.dir);
306
307 for ent in fs::read_dir(&self.dir)
308 .map_err(err_mapper_readdir())?
309 {
310 let ent = ent.map_err(err_mapper_readdir())?;
311 let del_leaf = ent.file_name();
312 let Ok(del_leaf) = <&str>::try_from(&*del_leaf)
313 else { continue };
314
315 tprintln!("{:37} SCAN {}", "", del_leaf);
316
317 if !del_leaf.ends_with(&SUFFIX) { continue }
318
319 let ent_path = ent.file_name();
320 let ent_path = ent_path.as_ref();
321
322 let age = (|| {
323 let modified = ent
324 .metadata()
325 .map_err(Error::err_mapper("stat existing/old logfile",
326 ent_path))?
327 .modified()
328 .map_err(Error::err_mapper("convert mtime", ent_path))?;
329 let age = now.duration_since(&self.tprov, modified)
330 .unwrap_or_default();
336 Ok(age)
337 })()?;
338
339 each_leaf(del_leaf, age)?;
340 }
341 Ok(())
342 }
343
344 fn delete_leaf(&self, del_leaf: &str, _why: &str) -> Result<(), Error> {
345 tprintln!(" {_why:31} DELETE {}", del_leaf);
346
347 let del_path = self.leaf2path(del_leaf);
348 fs::remove_file(&del_path)
349 .map_err(Error::err_mapper("remove old file", &del_path))?;
350 Ok(())
351 }
352}
353
354impl<'a> io::Write for Writer<'a> {
357 fn write(&mut self, s: &[u8]) -> io::Result<usize> {
358 let mut inner = self.appender.lock();
359 let inner = &mut *inner;
360 for o in &mut inner.outputs {
361 if let Some(o) = o.if_level(self.level) {
362 o.consider_reopen(&inner.common)?;
363 o.file.file.write_all(s).map_err(o.err_mapper("write"))?;
364 }
365 }
366 Ok(s.len())
367 }
368 fn flush(&mut self) -> io::Result<()> {
369 let mut inner = self.appender.lock();
370 let inner = &mut *inner;
371 for o in &mut inner.outputs {
372 if let Some(o) = o.if_level(self.level) {
373 o.file.file.flush().map_err(o.err_mapper("write"))?;
374 }
375 }
376 Ok(())
377 }
378}
379
380impl<'a> MakeWriter<'a> for Appender {
381 type Writer = Writer<'a>;
382
383 fn make_writer(&'a self) -> Writer<'a> {
384 self.make_writer_for_level(Level::ERROR )
385 }
386 fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Writer<'a> {
387 self.make_writer_for_level(*meta.level())
388 }
389}
390
391impl LevelConfig {
394 fn max_age_backstop(&self) -> Duration {
399 self.interval.duration() * (self.max_files + 1)
402 }
403}
404
405impl Period {
406 pub fn containing(now: SystemTime, interval: Interval) -> Self {
407 use Interval as I;
408 let from_date = |d| NaiveDateTime::new(d, NaiveTime::MIN);
409 let now = now.chrono_naive_utc();
410 let start = match interval {
411 I::Hour => {
412 let hour = now.hour();
413 NaiveDateTime::new(
414 now.date(),
415 NaiveTime::from_hms_opt(hour, 0, 0)
416 .expect("hour out of range?!")
417 )
418 }
419 I::Day => {
420 from_date(now.date())
421 }
422 I::Week => {
423 let week = now.date().week(Weekday::Sun);
424 from_date(week.first_day())
425 }
426 I::Month => {
427 let start = now.date().with_day0(0).expect("month no start!");
428 from_date(start)
429 }
430 };
431 Period { start, interval }
432 }
433}
434
435impl Interval {
436 pub fn duration(&self) -> Duration {
440 use Interval as I;
441 Duration::from_secs(match self {
442 I::Hour => 3600,
443 I::Day => 86400,
444 I::Week => 86400 * 7,
445 I::Month => 86400 * 31,
446 })
447 }
448}
449
450impl Display for Period {
451 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
452 use Interval as I;
453 let i = match self.interval {
454 I::Hour => "%Y-%m-%dT%H",
455 I::Day => "%Y-%m-%d",
456 I::Week => "%Y-%m-%d+",
457 I::Month => "%Y-%m",
458 };
459 write!(f, "{}", self.start.format(i))
460 }
461}
462
463impl Config {
466 pub fn most_verbose_level(&self) -> LevelFilter {
468 self.0.keys().max().copied().unwrap_or(LevelFilter::OFF)
469 }
470}
471
472impl TryFrom<ConfigSerde> for Config {
473 type Error = tracing::level_filters::ParseLevelFilterError;
474
475 fn try_from(hm: ConfigSerde) -> Result<Self, Self::Error> {
476 let bm = hm.into_iter().map(|(k, v)| {
477 let k: LevelFilter = k.parse()?;
478 Ok::<_, Self::Error>((k, v))
479 }).try_collect()?;
480 Ok(Config(bm))
481 }
482}
483
484impl From<Config> for ConfigSerde {
485 fn from(sc: Config) -> Self {
486 sc.0.into_iter().map(|(k, v)| {
487 let mut k: String = k.to_string();
488 k.make_ascii_lowercase();
489 (k, v)
490 }).collect()
491 }
492}
493
494impl<'de> Deserialize<'de> for Interval {
495 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
496 struct V;
497
498 const EXP: &str = "log rotation interval";
499
500 impl serde::de::Visitor<'_> for V {
501 type Value = Interval;
502 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
503 where E: serde::de::Error
504 {
505 v.parse().map_err(
506 |_| E::invalid_value(
507 serde::de::Unexpected::Str(v),
508 &EXP,
509 )
510 )
511 }
512 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
513 write!(f, "{EXP}")
514 }
515 }
516
517 d.deserialize_str(V)
518 }
519}
520
521impl From<Interval> for String {
522 fn from(i: Interval) -> String { i.to_string() }
523}
524
525impl TimeProvider {
530 fn now(&self) -> SystemTime {
531 use TimeProvider as TP;
532 let now = RawSystemTime::now();
533 match self {
534 TP::System => SystemTime(now),
535
536 #[cfg(test)]
537 TP::Test(t) => {
538 let mut t = TimeProvider::lock(t);
539 tprintln!("TP R{} now", f3339(now));
540 assert!(now >= t.last_seen);
541 t.raw2pretend(now)
542 }
543 }
544 }
545}
546
547impl SystemTime {
548 fn duration_since(
549 &self,
550 t: &TimeProvider,
551 file_age: RawSystemTime
552 ) -> Result<Duration, std::time::SystemTimeError> {
553 use TimeProvider as TP;
554 let file_age = match t {
555 TP::System => SystemTime(file_age),
556
557 #[cfg(test)]
558 TP::Test(t) => {
559 let mut t = TimeProvider::lock(t);
560 tprintln!("TP R{} duration_since", f3339(file_age));
561 t.raw2pretend(file_age)
562 },
563 };
564 self.0.duration_since(file_age.0)
565 }
566
567 fn chrono_naive_utc(self) -> NaiveDateTime {
568 DateTimeUtc::from(self.0).naive_utc()
569 }
570}
571
572impl Add<Duration> for SystemTime {
573 type Output = SystemTime;
574 fn add(self, rhs: Duration) -> SystemTime {
575 SystemTime(self.0 + rhs)
576 }
577}
578
579impl From<Error> for io::Error {
582 fn from(e: Error) -> io::Error {
583 io::Error::new(e.ioe.kind(), e)
584 }
585}
586
587impl Output {
588 fn err_mapper<'o>(&'o self, op: &'o str)
589 -> impl FnOnce(io::Error) -> Error + 'o
590 {
591 Error::err_mapper(op, &self.file.path)
592 }
593}
594
595impl Error {
596 fn err_mapper<'o>(op: &'o str, path: &'o Path)
597 -> impl FnOnce(io::Error) -> Error + 'o
598 {
599 |ioe| Error {
600 ioe,
601 path: path.to_owned(),
602 op: op.to_owned(),
603 }
604 }
605}
606
607#[cfg(test)]
610mod test {
611 use super::*;
612 use std::collections::BTreeSet;
613 use std::cmp;
614 use std::process::Command;
615 use std::str::FromStr;
616 use std::sync::Arc;
617
618 use anyhow::Context;
619 use humantime_serde::re::humantime;
620 use itertools::izip;
621 use serde_json::json;
622 use strum::IntoEnumIterator;
623 use test_temp_dir::test_temp_dir;
624 use testresult::{TestError, TestResult};
625
626 const FS_TIMESTAMP_RESOLUTION: Duration = Duration::from_millis(10);
655
656 pub type TestTimeProvider = Arc<Mutex<TimeProviderInner>>;
657
658 const TEST_START_3339: &str = "2024-10-01T19:39:45Z";
659
660 pub struct TimeProviderInner {
661 pub since: Vec<(RawSystemTime, SystemTime)>,
663 pub last_seen: RawSystemTime,
664 }
665
666 impl TimeProvider {
667 pub fn lock(t: &TestTimeProvider)
668 -> MutexGuard<'_, TimeProviderInner>
669 {
670 t.lock().expect("tprov poison!")
671 }
672 }
673
674 impl FromStr for SystemTime {
675 type Err = humantime::TimestampError;
676 fn from_str(s: &str) -> Result<Self, Self::Err> {
677 humantime::parse_rfc3339(s).map(SystemTime)
678 }
679 }
680
681 pub fn raw_system_time_start_period() -> RawSystemTime {
682 let now = RawSystemTime::now();
683 std::thread::sleep(FS_TIMESTAMP_RESOLUTION);
684 now
685 }
686
687 impl TimeProviderInner {
688 pub fn new_for_test() -> TestTimeProvider {
689 TimeProviderInner::new(
690 TEST_START_3339.parse().expect("bad constant")
691 )
692 }
693
694 pub fn new(start: SystemTime) -> TestTimeProvider {
695 let now = raw_system_time_start_period();
696 tprintln!("TP R{} start S{}", f3339(now), f3339(start.0));
697 let tprov = TimeProviderInner {
698 since: vec![(now, start)],
699 last_seen: now,
700 };
701 Arc::new(Mutex::new(tprov))
702 }
703
704 pub fn raw2pretend(&mut self, raw: RawSystemTime) -> SystemTime {
705 tprintln!("TP R{} raw2pretend", f3339(raw));
706 self.last_seen = cmp::max(self.last_seen, raw);
707 assert!(raw >= self.since[0].0, "{raw:?} {:?}", self.since);
708 let ent_pos = self.since
709 .binary_search_by(|ent| ent.0.cmp(&raw))
710 .unwrap_or_else(|insert| insert - 1);
711 let r = self.since[ent_pos].1;
712 tprintln!("TP R{} raw2pretend S{}", f3339(raw), f3339(r.0));
713 r
714 }
715
716 pub fn advance(&mut self, by_secs: u64) {
717 let pstart = raw_system_time_start_period();
718 assert!(pstart > self.last_seen);
719 let last = self.since.last_mut().expect("empty!");
720 assert!(pstart > last.0);
721 let new_period = (pstart, last.1 + Duration::from_secs(by_secs));
722 self.since.push(new_period);
723 }
724 }
725
726 struct Ctx {
729 a: Appender,
730 dir: PathBuf,
731 tprov: TestTimeProvider,
732 }
733
734 impl Ctx {
735 fn advance(&self, secs: u64) {
736 TimeProvider::lock(&self.tprov)
737 .advance(secs)
738 }
739
740 fn expect_files(&self, shell: &str) -> TestResult<()> {
741 let shell = shell.chars()
742 .filter(|c| !c.is_ascii_whitespace())
743 .collect::<String>();
744
745 let output = Command::new("bash")
746 .args(["-ec", &format!("echo {shell}")])
747 .output()?;
748 assert!(output.status.success(), "{}", output.status);
749 assert!(output.stderr.is_empty());
750 let output = String::from_utf8(output.stdout)?;
751 let expect = output
752 .trim()
753 .split_ascii_whitespace()
754 .map(str::to_owned)
755 .collect::<BTreeSet<_>>();
756
757 let actual = fs::read_dir(&self.dir)?
758 .map(|ent| {
759 let leaf = PathBuf::from(ent?.file_name())
760 .to_str().expect("non-utf8").to_owned();
761 Ok::<_, TestError>(leaf)
762 })
763 .filter_ok(|f| !(f == "." || f == ".."))
764 .collect::<Result<BTreeSet<_>, _>>()?;
765 assert_eq!(actual, expect,
766 "actual={actual:#?} expect={expect:#?}");
767 Ok(())
768 }
769
770 fn shutdown(self) -> TestTimeProvider {
771 let Ctx { tprov, .. } = self;
772 tprov
773 }
774 }
775
776 fn usual_levels() -> Config {
777 use LevelFilter as L;
778 use Interval as I;
779 [
780 (L::DEBUG, I::Hour),
781 (L::INFO, I::Day),
782 ]
783 .into_iter().map(|(level, interval)| {
784 (level, LevelConfig { max_files: 5, interval })
785 })
786 .collect::<BTreeMap<_, _>>()
787 .into()
788 }
789
790 fn setup(dir: &Path) -> TestResult<Ctx> {
791 let tprov = TimeProviderInner::new_for_test();
792 Ctx::new(dir, tprov)
793 }
794
795 impl Ctx {
796 fn new(
797 dir: &Path,
798 tprov: TestTimeProvider,
799 ) -> TestResult<Self> {
800 Ctx::new_with_levels(dir, usual_levels(), tprov)
801 }
802
803 fn new_with_levels(
804 dir: &Path,
805 levels: Config,
806 tprov: TestTimeProvider,
807 ) -> TestResult<Self> {
808 let a = Appender::new_with_time_provider(
809 dir.to_owned(),
810 &levels,
811 TimeProvider::Test(tprov.clone()),
812 )?;
813
814 Ok(Ctx { a, tprov, dir: dir.to_owned() })
815 }
816
817 fn log(&self, m: impl Display) -> TestResult<()> {
818 let mut w = self.a.make_writer();
819 writeln!(w, "{m}")?;
820 w.flush()?;
821 Ok(())
822 }
823 }
824
825 #[test]
828 fn rotate() -> TestResult<()> { test_temp_dir!().used_by(|dir| {
829 let c = setup(dir)?;
830 c.log("started")?;
831 for h in 0..7 {
832 c.advance(3600);
833 c.log(format!("h={h}"))?;
834 }
835 c.expect_files(
836 r"{ debug_2024-10-{ 02T{ 02, 01, 00 }, 01T{ 23, 22 }},
837 info_2024-10-{ 02, 01 } }.log"
838 )?;
839 Ok(())
840 }).into_untracked() }
841
842 #[test]
843 fn level() -> TestResult<()> { test_temp_dir!().used_by(|dir| {
844 let msg_error = "this message is an error";
845 let msg_debug = "this message is debugging";
846
847 let c = setup(dir)?;
848 c.log(msg_error)?;
849
850 {
851 let mut w = c.a.make_writer_for_level(Level::DEBUG);
852 writeln!(w, "{msg_debug}")?;
853 w.flush()?;
854 }
855
856 let read = |leaf: &str| {
857 let mut path = dir.to_owned();
858 path.push(leaf);
859 let s = fs::read_to_string(path)
860 .context(leaf.to_owned())?;
861 Ok::<_, anyhow::Error>(s)
862 };
863
864 assert_eq!(
865 read("debug_2024-10-01T19.log")?,
866 format!("{msg_error}\n{msg_debug}\n"),
867 );
868 assert_eq!(
869 read("info_2024-10-01.log")?,
870 format!("{msg_error}\n"),
871 );
872
873 Ok(())
874 }).into_untracked() }
875
876 #[test]
877 fn restart() -> TestResult<()> { test_temp_dir!().used_by(|dir| {
878 let c = setup(dir)?;
879 c.log("earlier")?;
880
881 let tprov = c.shutdown();
882
883 TimeProvider::lock(&tprov).advance(86400);
884
885 let c = Ctx::new(&dir, tprov)?;
886
887 c.expect_files(
888 r"{ debug_2024-10-02T19,
889 info_2024-10-{ 02, 01 } }.log"
890 )?;
891 Ok(())
892 }).into_untracked() }
893
894 #[test]
895 fn reconfig() -> TestResult<()> { test_temp_dir!().used_by(|dir| {
896 use LevelFilter as L;
897 let levels = [
898 L::TRACE,
899 L::DEBUG,
900 L::INFO,
901 L::WARN,
902 L::ERROR,
903 ];
904
905 let c = Ctx::new_with_levels(
906 &dir,
907 izip!(
908 levels.into_iter().cycle(),
909 Interval::iter(),
910 ).map(|(level, interval)| {
911 (level, LevelConfig { max_files: 5, interval })
912 }).collect::<BTreeMap<_, _>>().into(),
913 TimeProviderInner::new_for_test(),
914 )?;
915
916 c.log("started")?;
917 c.advance(86400 * 3);
918 c.log("3 days later")?;
919
920 c.expect_files(
921 r"{ trace_2024-10-04T19,
922 debug_2024-10-01,
923 debug_2024-10-04,
924 info_2024-09-29+,
925 warn_2024-10 }.log"
926 )?;
927
928 let tprov = c.shutdown();
929
930 TimeProvider::lock(&tprov).advance(86400 * 4);
931
932 let c = Ctx::new(&dir, tprov)?;
933
934 c.log("a further 4 days later")?;
935
936 c.expect_files(
937 r"{ trace_2024-10-04T19,
938 debug_2024-10-08T19,
939 info_2024-09-29+,
940 info_2024-10-08,
941 warn_2024-10 }.log"
942 )?;
943
944 c.advance(86400 * 3);
945 c.log("surely that trace log from 2024-10-04 goes away")?;
946
947 c.expect_files(
948 r"{ debug_2024-10-11T19,
949 info_2024-10-08,
950 info_2024-10-11 }.log"
951 )?;
952
953 Ok(())
954 }).into_untracked() }
955
956 impl Period {
957 fn start_system_time(&self) -> SystemTime {
958 SystemTime(self.start.and_utc().into())
959 }
960 }
961
962 #[test]
963 fn intervals() -> TestResult<()> {
964 let start: SystemTime = TEST_START_3339.parse()?;
965 for i in Interval::iter() {
966 tprintln!("{i:?}");
967 let containing = |t| Period::containing(t, i);
968 let mut t = start;
969 for n in 0..30 {
970 tprintln!("{i:?} {n} {}", f3339(t.0));
971 let period = containing(t);
972 let start = period.start_system_time();
973 assert_eq!(period, containing(start));
974
975 let d = if i != Interval::Month {
976 i.duration()
977 } else {
978 let days = |days: u64| Duration::from_secs(days * 86400);
979 assert_eq!(i.duration(), days(31));
980
981 let month = period.start.month();
982 if month == 2 {
983 days(28) } else if [4,6,9,11].contains(&month) {
985 days(30)
986 } else {
987 days(31)
988 }
989 };
990 let d_minus = d - Duration::from_secs(1);
991
992 assert_eq!(period, containing(start + d_minus));
993 let next_period = containing(start + d);
994 assert_ne!(period, next_period);
995 assert_ne!(period.to_string(), next_period.to_string());
996 t = next_period.start_system_time();
997 }
998 }
999 Ok(())
1000 }
1001
1002 #[test]
1003 fn serde() -> TestResult<()> {
1004 let j = json!{ {
1005 "debug": {
1006 "interval": "hour",
1007 "max_files": 5,
1008 },
1009 "info": {
1010 "interval": "day",
1011 "max_files": 5,
1012 }
1013 } };
1014
1015 let fj: Config = serde_json::from_value(j.clone())?;
1016
1017 assert_eq!(fj, usual_levels());
1018 assert_eq!(serde_json::to_value(&fj)?, j);
1019
1020 assert_eq!(fj.most_verbose_level(), LevelFilter::DEBUG);
1021 assert_eq!(
1022 Config(BTreeMap::new()).most_verbose_level(),
1023 LevelFilter::OFF,
1024 );
1025
1026 Ok(())
1027 }
1028}