tracing_rolling_file/
base.rs

1//! Implements a rolling condition based on a certain frequency
2//! and/or a size limit. The default condition is to rotate daily.
3//!
4//! # Examples
5//!
6//! ```rust
7//! use tracing_rolling_file::*;
8//! let c = RollingConditionBase::new().daily();
9//! let c = RollingConditionBase::new().hourly().max_size(1024 * 1024);
10//! ```
11
12use crate::*;
13
14#[derive(Copy, Clone, Debug, Eq, PartialEq)]
15pub struct RollingConditionBase {
16    last_write_opt: Option<DateTime<Local>>,
17    frequency_opt: Option<RollingFrequency>,
18    max_size_opt: Option<u64>,
19}
20
21impl RollingConditionBase {
22    /// Constructs a new struct that does not yet have any condition set.
23    pub fn new() -> RollingConditionBase {
24        RollingConditionBase {
25            last_write_opt: None,
26            frequency_opt: None,
27            max_size_opt: None,
28        }
29    }
30
31    /// Sets a condition to rollover on the given frequency
32    pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBase {
33        self.frequency_opt = Some(x);
34        self
35    }
36
37    /// Sets a condition to rollover when the date changes
38    pub fn daily(mut self) -> RollingConditionBase {
39        self.frequency_opt = Some(RollingFrequency::EveryDay);
40        self
41    }
42
43    /// Sets a condition to rollover when the date or hour changes
44    pub fn hourly(mut self) -> RollingConditionBase {
45        self.frequency_opt = Some(RollingFrequency::EveryHour);
46        self
47    }
48
49    /// Sets a condition to rollover when the date or minute changes
50    pub fn minutely(mut self) -> RollingConditionBase {
51        self.frequency_opt = Some(RollingFrequency::EveryMinute);
52        self
53    }
54
55    /// Sets a condition to rollover when a certain size is reached
56    pub fn max_size(mut self, x: u64) -> RollingConditionBase {
57        self.max_size_opt = Some(x);
58        self
59    }
60}
61
62impl Default for RollingConditionBase {
63    fn default() -> Self {
64        RollingConditionBase::new().frequency(RollingFrequency::EveryDay)
65    }
66}
67
68impl RollingCondition for RollingConditionBase {
69    fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
70        let mut rollover = false;
71        if let Some(frequency) = self.frequency_opt.as_ref() {
72            if let Some(last_write) = self.last_write_opt.as_ref() {
73                if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
74                    rollover = true;
75                }
76            }
77        }
78        if let Some(max_size) = self.max_size_opt.as_ref() {
79            if current_filesize >= *max_size {
80                rollover = true;
81            }
82        }
83        self.last_write_opt = Some(*now);
84        rollover
85    }
86}
87
88pub struct RollingFileAppenderBaseBuilder {
89    condition: RollingConditionBase,
90    filename: String,
91    max_filecount: usize,
92    current_filesize: u64,
93    writer_opt: Option<BufWriter<File>>,
94}
95
96impl Default for RollingFileAppenderBaseBuilder {
97    fn default() -> Self {
98        RollingFileAppenderBaseBuilder {
99            condition: RollingConditionBase::default(),
100            filename: String::new(),
101            max_filecount: 10,
102            current_filesize: 0,
103            writer_opt: None,
104        }
105    }
106}
107
108impl RollingFileAppenderBaseBuilder {
109    /// Sets the log filename. Uses absolute path if provided, otherwise
110    /// creates files in the current working directory.
111    pub fn filename(mut self, filename: String) -> Self {
112        self.filename = filename;
113        self
114    }
115
116    /// Sets a condition for the maximum number of files to create before rolling
117    /// over and deleting the oldest one.
118    pub fn max_filecount(mut self, max_filecount: usize) -> Self {
119        self.max_filecount = max_filecount;
120        self
121    }
122
123    /// Sets a condition to rollover on a daily basis
124    pub fn condition_daily(mut self) -> Self {
125        self.condition.frequency_opt = Some(RollingFrequency::EveryDay);
126        self
127    }
128
129    /// Sets a condition to rollover when the date or hour changes
130    pub fn condition_hourly(mut self) -> Self {
131        self.condition.frequency_opt = Some(RollingFrequency::EveryHour);
132        self
133    }
134
135    /// Sets a condition to rollover when the date or minute changes
136    pub fn condition_minutely(mut self) -> Self {
137        self.condition.frequency_opt = Some(RollingFrequency::EveryMinute);
138        self
139    }
140
141    /// Sets a condition to rollover when a certain size is reached
142    pub fn condition_max_file_size(mut self, x: u64) -> Self {
143        self.condition.max_size_opt = Some(x);
144        self
145    }
146
147    /// Builds a RollingFileAppenderBase instance from the current settings.
148    ///
149    /// Returns an error if the filename is empty.
150    pub fn build(self) -> Result<RollingFileAppenderBase, &'static str> {
151        if self.filename.is_empty() {
152            return Err("A filename is required to be set and can not be blank");
153        }
154        Ok(RollingFileAppenderBase {
155            condition: self.condition,
156            filename: self.filename,
157            max_filecount: self.max_filecount,
158            current_filesize: self.current_filesize,
159            writer_opt: self.writer_opt,
160        })
161    }
162}
163
164impl RollingFileAppenderBase {
165    /// Creates a new rolling file appender builder instance with the default
166    /// settings without a filename set.
167    pub fn builder() -> RollingFileAppenderBaseBuilder {
168        RollingFileAppenderBaseBuilder::default()
169    }
170}
171
172#[cfg(feature = "non-blocking")]
173use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
174
175#[cfg(feature = "non-blocking")]
176use tracing_appender::non_blocking;
177
178#[cfg(feature = "non-blocking")]
179impl RollingFileAppenderBase {
180    /// Generates a non-blocking Struct wrapping the RollingFileAppender
181    /// instance inside and WorkerGuard returned as a tuple.
182    pub fn get_non_blocking_appender(self) -> (NonBlocking, WorkerGuard) {
183        let (non_blocking, _guard) = non_blocking(self);
184        (non_blocking, _guard)
185    }
186}
187
188/// A rolling file appender with a rolling condition based on date/time or size.
189pub type RollingFileAppenderBase = RollingFileAppender<RollingConditionBase>;
190
191// LCOV_EXCL_START
192#[cfg(test)]
193mod test {
194    use super::*;
195
196    struct Context {
197        _tempdir: tempfile::TempDir,
198        rolling: RollingFileAppenderBase,
199    }
200
201    impl Context {
202        fn verify_contains(&mut self, needle: &str, n: usize) {
203            self.rolling.flush().unwrap();
204            let p = self.rolling.filename_for(n);
205            let haystack = fs::read_to_string(&p).unwrap();
206            if !haystack.contains(needle) {
207                panic!("file {:?} did not contain expected contents {}", p, needle);
208            }
209        }
210    }
211
212    fn build_context(condition: RollingConditionBase, max_files: usize) -> Context {
213        let tempdir = tempfile::tempdir().unwrap();
214        let filename = tempdir.path().join("test.log");
215        let rolling = RollingFileAppenderBase::new(filename, condition, max_files).unwrap();
216        Context {
217            _tempdir: tempdir,
218            rolling,
219        }
220    }
221
222    fn build_builder_context(mut builder: RollingFileAppenderBaseBuilder) -> Context {
223        if builder.filename.is_empty() {
224            builder = builder.filename(String::from("test.log"));
225        }
226        let tempdir = tempfile::tempdir().unwrap();
227        let filename = tempdir.path().join(&builder.filename);
228        builder = builder.filename(String::from(filename.as_os_str().to_str().unwrap()));
229        Context {
230            _tempdir: tempdir,
231            rolling: builder.build().unwrap(),
232        }
233    }
234
235    #[test]
236    fn frequency_every_day() {
237        let mut c = build_context(RollingConditionBase::new().daily(), 9);
238        c.rolling
239            .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
240            .unwrap();
241        c.rolling
242            .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap())
243            .unwrap();
244        c.rolling
245            .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
246            .unwrap();
247        c.rolling
248            .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
249            .unwrap();
250        c.rolling
251            .write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap())
252            .unwrap();
253        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
254        c.verify_contains("Line 1", 3);
255        c.verify_contains("Line 2", 3);
256        c.verify_contains("Line 3", 2);
257        c.verify_contains("Line 4", 1);
258        c.verify_contains("Line 5", 0);
259    }
260
261    #[test]
262    fn frequency_every_day_limited_files() {
263        let mut c = build_context(RollingConditionBase::new().daily(), 2);
264        c.rolling
265            .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
266            .unwrap();
267        c.rolling
268            .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap())
269            .unwrap();
270        c.rolling
271            .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
272            .unwrap();
273        c.rolling
274            .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
275            .unwrap();
276        c.rolling
277            .write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 1, 4, 0).unwrap())
278            .unwrap();
279        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
280        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
281        c.verify_contains("Line 3", 2);
282        c.verify_contains("Line 4", 1);
283        c.verify_contains("Line 5", 0);
284    }
285
286    #[test]
287    fn frequency_every_hour() {
288        let mut c = build_context(RollingConditionBase::new().hourly(), 9);
289        c.rolling
290            .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
291            .unwrap();
292        c.rolling
293            .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 2).unwrap())
294            .unwrap();
295        c.rolling
296            .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 1, 0).unwrap())
297            .unwrap();
298        c.rolling
299            .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 3, 31, 2, 1, 0).unwrap())
300            .unwrap();
301        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
302        c.verify_contains("Line 1", 2);
303        c.verify_contains("Line 2", 2);
304        c.verify_contains("Line 3", 1);
305        c.verify_contains("Line 4", 0);
306    }
307
308    #[test]
309    fn frequency_every_minute() {
310        let mut c = build_context(RollingConditionBase::new().frequency(RollingFrequency::EveryMinute), 9);
311        c.rolling
312            .write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
313            .unwrap();
314        c.rolling
315            .write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
316            .unwrap();
317        c.rolling
318            .write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 4).unwrap())
319            .unwrap();
320        c.rolling
321            .write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 0).unwrap())
322            .unwrap();
323        c.rolling
324            .write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap())
325            .unwrap();
326        c.rolling
327            .write_with_datetime(b"Line 6\n", &Local.with_ymd_and_hms(2022, 3, 30, 2, 3, 0).unwrap())
328            .unwrap();
329        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(4)).exists());
330        c.verify_contains("Line 1", 3);
331        c.verify_contains("Line 2", 3);
332        c.verify_contains("Line 3", 3);
333        c.verify_contains("Line 4", 2);
334        c.verify_contains("Line 5", 1);
335        c.verify_contains("Line 6", 0);
336    }
337
338    #[test]
339    fn max_size() {
340        let mut c = build_context(RollingConditionBase::new().max_size(10), 9);
341        c.rolling
342            .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
343            .unwrap();
344        c.rolling
345            .write_with_datetime(b"6789", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap())
346            .unwrap();
347        c.rolling
348            .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
349            .unwrap();
350        c.rolling
351            .write_with_datetime(b"abcdefghijkl", &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap())
352            .unwrap();
353        c.rolling
354            .write_with_datetime(b"ZZZ", &Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap())
355            .unwrap();
356        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
357        c.verify_contains("1234567890", 2);
358        c.verify_contains("abcdefghijkl", 1);
359        c.verify_contains("ZZZ", 0);
360    }
361
362    #[test]
363    fn max_size_existing() {
364        let mut c = build_context(RollingConditionBase::new().max_size(10), 9);
365        c.rolling
366            .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
367            .unwrap();
368        // close the file and make sure that it can re-open it, and that it
369        // resets the file size properly.
370        c.rolling.writer_opt.take();
371        c.rolling.current_filesize = 0;
372        c.rolling
373            .write_with_datetime(b"6789", &Local.with_ymd_and_hms(2021, 3, 30, 1, 3, 3).unwrap())
374            .unwrap();
375        c.rolling
376            .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
377            .unwrap();
378        c.rolling
379            .write_with_datetime(b"abcdefghijkl", &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap())
380            .unwrap();
381        c.rolling
382            .write_with_datetime(b"ZZZ", &Local.with_ymd_and_hms(2022, 3, 31, 1, 2, 3).unwrap())
383            .unwrap();
384        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
385        c.verify_contains("1234567890", 2);
386        c.verify_contains("abcdefghijkl", 1);
387        c.verify_contains("ZZZ", 0);
388    }
389
390    #[test]
391    fn daily_and_max_size() {
392        let mut c = build_context(RollingConditionBase::new().daily().max_size(10), 9);
393        c.rolling
394            .write_with_datetime(b"12345", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
395            .unwrap();
396        c.rolling
397            .write_with_datetime(b"6789", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 3).unwrap())
398            .unwrap();
399        c.rolling
400            .write_with_datetime(b"0", &Local.with_ymd_and_hms(2021, 3, 31, 2, 3, 3).unwrap())
401            .unwrap();
402        c.rolling
403            .write_with_datetime(b"abcdefghijkl", &Local.with_ymd_and_hms(2021, 3, 31, 3, 3, 3).unwrap())
404            .unwrap();
405        c.rolling
406            .write_with_datetime(b"ZZZ", &Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap())
407            .unwrap();
408        assert!(!AsRef::<Path>::as_ref(&c.rolling.filename_for(3)).exists());
409        c.verify_contains("123456789", 2);
410        c.verify_contains("0abcdefghijkl", 1);
411        c.verify_contains("ZZZ", 0);
412    }
413
414    #[test]
415    fn rolling_file_appender_builder() {
416        let builder = RollingFileAppender::builder();
417
418        let builder = builder.condition_daily().condition_max_file_size(10);
419        let mut c = build_builder_context(builder);
420        c.rolling
421            .write_with_datetime(
422                b"abcdefghijklmnop",
423                &Local.with_ymd_and_hms(2021, 3, 31, 4, 4, 4).unwrap(),
424            )
425            .unwrap();
426        c.rolling
427            .write_with_datetime(b"12345678", &Local.with_ymd_and_hms(2021, 3, 31, 5, 4, 4).unwrap())
428            .unwrap();
429        assert!(AsRef::<Path>::as_ref(&c.rolling.filename_for(1)).exists());
430        assert!(Path::new(&c.rolling.filename_for(0)).exists());
431        c.verify_contains("abcdefghijklmnop", 1);
432        c.verify_contains("12345678", 0);
433    }
434
435    #[test]
436    fn rolling_file_appender_builder_no_filename() {
437        let builder = RollingFileAppender::builder();
438        let appender = builder.condition_daily().build();
439        assert!(appender.is_err());
440    }
441}
442// LCOV_EXCL_STOP