polyverse_file_rotate/
synchronous.rs

1//! Write output to a file and rotate the files when limits have been exceeded.
2//!
3//! Defines a simple [std::io::Write] object that you can plug into your writers as middleware.
4//!
5//! # Rotating by Lines #
6//!
7//! We can rotate log files by using the amount of lines as a limit.
8//!
9//! ```
10//! use file_rotate::{FileRotate, RotationMode};
11//! use std::{fs, io::Write};
12//!
13//! // Create a directory to store our logs, this is not strictly needed but shows how we can
14//! // arbitrary paths.
15//! fs::create_dir("target/my-log-directory-lines");
16//!
17//! // Create a new log writer. The first argument is anything resembling a path. The
18//! // basename is used for naming the log files.
19//! //
20//! // Here we choose to limit logs by 10 lines, and have at most 2 rotated log files. This
21//! // makes the total amount of log files 4, since the original file is present as well as
22//! // file 0.
23//! let mut log = FileRotate::new("target/my-log-directory-lines/my-log-file", RotationMode::Lines(3), 2).unwrap();
24//!
25//! // Write a bunch of lines
26//! writeln!(log, "Line 1: Hello World!");
27//! for idx in 2..11 {
28//!     writeln!(log, "Line {}", idx);
29//! }
30//!
31//! assert_eq!("Line 10\n", fs::read_to_string("target/my-log-directory-lines/my-log-file").unwrap());
32//!
33//! assert_eq!("Line 1: Hello World!\nLine 2\nLine 3\n", fs::read_to_string("target/my-log-directory-lines/my-log-file.0").unwrap());
34//! assert_eq!("Line 4\nLine 5\nLine 6\n", fs::read_to_string("target/my-log-directory-lines/my-log-file.1").unwrap());
35//! assert_eq!("Line 7\nLine 8\nLine 9\n", fs::read_to_string("target/my-log-directory-lines/my-log-file.2").unwrap());
36//!
37//! fs::remove_dir_all("target/my-log-directory-lines");
38//! ```
39//!
40//! # Rotating by Bytes surpassing a threshold, but without splitting a buffer mid-way#
41//!
42//! We can rotate log files but never splitting a buffer half-way. This means a single buffer may
43//! end up surpassing the number of expected bytes in a file, but that entire buffer will be
44//! written. When the file surpasses the number of bytes to rotate, it'll be rotated for the
45//! next buffer.
46//!
47//! When lines are written in a single buffer as demonstrated below, this ensures the logs
48//! contain complete lines which do not split across files.
49//!
50//! ```
51//! use file_rotate::{FileRotate, RotationMode};
52//! use std::{fs, io::Write};
53//!
54//! // Create a directory to store our logs, this is not strictly needed but shows how we can
55//! // arbitrary paths.
56//! fs::create_dir("target/my-log-directory-lines");
57//!
58//! // Create a new log writer. The first argument is anything resembling a path. The
59//! // basename is used for naming the log files.
60//! //
61//! // Here we choose to limit logs by 10 lines, and have at most 2 rotated log files. This
62//! // makes the total amount of log files 4, since the original file is present as well as
63//! // file 0.
64//! let mut log = FileRotate::new("target/my-log-directory-lines/my-log-file", RotationMode::BytesSurpassed(2), 2).unwrap();
65//!
66//! // Write a bunch of lines
67//! log.write("Line 1: Hello World!\n".as_bytes());
68//! for idx in 2..11 {
69//!     log.write(format!("Line {}", idx).as_bytes());
70//! }
71//!
72//! // the latest file is empty - since the previous file surpassed bytes and was rotated out
73//! assert_eq!("", fs::read_to_string("target/my-log-directory-lines/my-log-file").unwrap());
74//!
75//! assert_eq!("Line 10", fs::read_to_string("target/my-log-directory-lines/my-log-file.0").unwrap());
76//! assert_eq!("Line 8", fs::read_to_string("target/my-log-directory-lines/my-log-file.1").unwrap());
77//! assert_eq!("Line 9", fs::read_to_string("target/my-log-directory-lines/my-log-file.2").unwrap());
78//!
79//! fs::remove_dir_all("target/my-log-directory-lines");
80//! ```
81//!
82//!
83//! # Rotating by Bytes #
84//!
85//! Another method of rotation is by bytes instead of lines.
86//!
87//! ```
88//! use file_rotate::{FileRotate, RotationMode};
89//! use std::{fs, io::Write};
90//!
91//! fs::create_dir("target/my-log-directory-bytes");
92//!
93//! let mut log = FileRotate::new("target/my-log-directory-bytes/my-log-file", RotationMode::Bytes(5), 2).unwrap();
94//!
95//! writeln!(log, "Test file");
96//!
97//! assert_eq!("Test ", fs::read_to_string("target/my-log-directory-bytes/my-log-file.0").unwrap());
98//! assert_eq!("file\n", fs::read_to_string("target/my-log-directory-bytes/my-log-file").unwrap());
99//!
100//! fs::remove_dir_all("target/my-log-directory-bytes");
101//! ```
102//!
103//! # Rotation Method #
104//!
105//! The rotation method used is to always write to the base path, and then move the file to a new
106//! location when the limit is exceeded. The moving occurs in the sequence 0, 1, 2, n, 0, 1, 2...
107//!
108//! Here's an example with 1 byte limits:
109//!
110//! ```
111//! use file_rotate::{FileRotate, RotationMode};
112//! use std::{fs, io::Write};
113//!
114//! fs::create_dir("target/my-log-directory-small");
115//!
116//! let mut log = FileRotate::new("target/my-log-directory-small/my-log-file", RotationMode::Bytes(1), 3).unwrap();
117//!
118//! write!(log, "A");
119//! assert_eq!("A", fs::read_to_string("target/my-log-directory-small/my-log-file").unwrap());
120//!
121//! write!(log, "B");
122//! assert_eq!("A", fs::read_to_string("target/my-log-directory-small/my-log-file.0").unwrap());
123//! assert_eq!("B", fs::read_to_string("target/my-log-directory-small/my-log-file").unwrap());
124//!
125//! write!(log, "C");
126//! assert_eq!("A", fs::read_to_string("target/my-log-directory-small/my-log-file.0").unwrap());
127//! assert_eq!("B", fs::read_to_string("target/my-log-directory-small/my-log-file.1").unwrap());
128//! assert_eq!("C", fs::read_to_string("target/my-log-directory-small/my-log-file").unwrap());
129//!
130//! write!(log, "D");
131//! assert_eq!("A", fs::read_to_string("target/my-log-directory-small/my-log-file.0").unwrap());
132//! assert_eq!("B", fs::read_to_string("target/my-log-directory-small/my-log-file.1").unwrap());
133//! assert_eq!("C", fs::read_to_string("target/my-log-directory-small/my-log-file.2").unwrap());
134//! assert_eq!("D", fs::read_to_string("target/my-log-directory-small/my-log-file").unwrap());
135//!
136//! write!(log, "E");
137//! assert_eq!("A", fs::read_to_string("target/my-log-directory-small/my-log-file.0").unwrap());
138//! assert_eq!("B", fs::read_to_string("target/my-log-directory-small/my-log-file.1").unwrap());
139//! assert_eq!("C", fs::read_to_string("target/my-log-directory-small/my-log-file.2").unwrap());
140//! assert_eq!("D", fs::read_to_string("target/my-log-directory-small/my-log-file.3").unwrap());
141//! assert_eq!("E", fs::read_to_string("target/my-log-directory-small/my-log-file").unwrap());
142//!
143//!
144//! // Here we overwrite the 0 file since we're out of log files, restarting the sequencing
145//! write!(log, "F");
146//! assert_eq!("E", fs::read_to_string("target/my-log-directory-small/my-log-file.0").unwrap());
147//! assert_eq!("B", fs::read_to_string("target/my-log-directory-small/my-log-file.1").unwrap());
148//! assert_eq!("C", fs::read_to_string("target/my-log-directory-small/my-log-file.2").unwrap());
149//! assert_eq!("D", fs::read_to_string("target/my-log-directory-small/my-log-file.3").unwrap());
150//! assert_eq!("F", fs::read_to_string("target/my-log-directory-small/my-log-file").unwrap());
151//!
152//! fs::remove_dir_all("target/my-log-directory-small");
153//! ```
154//!
155//! # Filesystem Errors #
156//!
157//! If the directory containing the logs is deleted or somehow made inaccessible then the rotator
158//! will simply continue operating without fault. When a rotation occurs, it attempts to open a
159//! file in the directory. If it can, it will just continue logging. If it can't then the written
160//! date is sent to the void.
161//!
162//! This logger never panics.
163#![deny(
164    missing_docs,
165    trivial_casts,
166    trivial_numeric_casts,
167    unsafe_code,
168    unused_import_braces,
169    unused_qualifications
170)]
171
172use crate::error;
173use std::{
174    fs::{self, File},
175    io::{self, Write},
176    path::{Path, PathBuf},
177};
178
179type Result<T> = std::result::Result<T, error::Error>;
180
181/// Condition on which a file is rotated.
182#[derive(Debug)]
183pub enum RotationMode {
184    /// Cut the log at the exact size in bytes.
185    Bytes(usize),
186    /// Cut the log file at line breaks.
187    Lines(usize),
188    /// Cut the log file after surpassing size in bytes (but having written a complete buffer from a write call.)
189    BytesSurpassed(usize),
190}
191
192/// The main writer used for rotating logs.
193#[derive(Debug)]
194pub struct FileRotate {
195    basename: PathBuf,
196    count: usize,
197    file: File,
198    file_number: usize,
199    max_file_number: usize,
200    mode: RotationMode,
201}
202
203impl FileRotate {
204    /// Create a new [FileRotate].
205    ///
206    /// The basename of the `path` is used to create new log files by appending an extension of the
207    /// form `.N`, where N is `0..=max_file_number`.
208    ///
209    /// `rotation_mode` specifies the limits for rotating a file.
210    pub fn new<P: AsRef<Path>>(
211        path: P,
212        rotation_mode: RotationMode,
213        max_file_number: usize,
214    ) -> Result<Self> {
215        match rotation_mode {
216            RotationMode::Bytes(bytes) if bytes == 0 => {
217                return Err(error::Error::ZeroBytes);
218            }
219            RotationMode::Lines(lines) if lines == 0 => {
220                return Err(error::Error::ZeroLines);
221            }
222            RotationMode::BytesSurpassed(bytes) if bytes == 0 => {
223                return Err(error::Error::ZeroBytes);
224            }
225            _ => {}
226        };
227
228        Ok(Self {
229            basename: path.as_ref().to_path_buf(),
230            count: 0,
231            file: File::create(&path)?,
232            file_number: 0,
233            max_file_number,
234            mode: rotation_mode,
235        })
236    }
237
238    fn rotate(&mut self) -> io::Result<()> {
239        let mut path = self.basename.clone();
240        path.set_extension(self.file_number.to_string());
241
242        // flush the file we have
243        self.file.flush()?;
244
245        // ignore renaming errors - the directory may have been deleted
246        // and may be recreated later
247        let _ = fs::rename(&self.basename, path);
248        self.file = File::create(&self.basename)?;
249
250        self.file_number = (self.file_number + 1) % (self.max_file_number + 1);
251        self.count = 0;
252
253        Ok(())
254    }
255
256    fn write_bytes(&mut self, mut buf: &[u8], bytes: usize) -> io::Result<usize> {
257        let mut written: usize = 0;
258
259        while self.count + buf.len() > bytes {
260            let bytes_left = bytes - self.count;
261            written += self.file.write(&buf[..bytes_left])?;
262            self.rotate()?;
263            buf = &buf[bytes_left..];
264        }
265        written += self.file.write(&buf[..])?;
266        self.count += written;
267
268        Ok(written)
269    }
270
271    fn write_bytes_surpassed(&mut self, buf: &[u8], bytes: usize) -> io::Result<usize> {
272        let mut written: usize = 0;
273
274        written += self.file.write(&buf)?;
275        self.count += written;
276        if self.count > bytes {
277            self.rotate()?
278        }
279
280        Ok(written)
281    }
282
283    fn write_lines(&mut self, mut buf: &[u8], lines: usize) -> io::Result<usize> {
284        let mut written: usize = 0;
285
286        while let Some((idx, _)) = buf.iter().enumerate().find(|(_, byte)| *byte == &b'\n') {
287            written += self.file.write(&buf[..idx + 1])?;
288            self.count += 1;
289            buf = &buf[idx + 1..];
290            if self.count >= lines {
291                self.rotate()?;
292            }
293        }
294        written += self.file.write(buf)?;
295
296        Ok(written)
297    }
298}
299
300impl Write for FileRotate {
301    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
302        match self.mode {
303            RotationMode::Bytes(bytes) => self.write_bytes(buf, bytes),
304            RotationMode::Lines(lines) => self.write_lines(buf, lines),
305            RotationMode::BytesSurpassed(bytes) => self.write_bytes_surpassed(buf, bytes),
306        }
307    }
308
309    fn flush(&mut self) -> io::Result<()> {
310        self.file.flush()
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317
318    #[test]
319    fn zero_bytes() {
320        let zerobyteserr =
321            FileRotate::new("target/zero_bytes", RotationMode::Bytes(0), 0).unwrap_err();
322        if let error::Error::ZeroBytes = zerobyteserr {
323        } else {
324            assert!(false, "Expected Error::ZeroBytes");
325        };
326    }
327
328    #[test]
329    fn zero_bytes_surpassed() {
330        let zerobyteserr =
331            FileRotate::new("target/zero_bytes", RotationMode::BytesSurpassed(0), 0).unwrap_err();
332        if let error::Error::ZeroBytes = zerobyteserr {
333        } else {
334            assert!(false, "Expected Error::ZeroBytes");
335        };
336    }
337
338    #[test]
339    fn zero_lines() {
340        let zerolineserr =
341            FileRotate::new("target/zero_lines", RotationMode::Lines(0), 0).unwrap_err();
342        if let error::Error::ZeroLines = zerolineserr {
343        } else {
344            assert!(false, "Expected Error::ZeroLines");
345        };
346    }
347
348    #[test]
349    fn rotate_to_deleted_directory() {
350        let _ = fs::remove_dir_all("target/rotate");
351        fs::create_dir("target/rotate").unwrap();
352
353        let mut rot = FileRotate::new("target/rotate/log", RotationMode::Lines(1), 0).unwrap();
354        writeln!(rot, "a").unwrap();
355        assert_eq!("", fs::read_to_string("target/rotate/log").unwrap());
356        assert_eq!("a\n", fs::read_to_string("target/rotate/log.0").unwrap());
357
358        fs::remove_dir_all("target/rotate").unwrap();
359
360        assert!(writeln!(rot, "b").is_err());
361
362        rot.flush().unwrap();
363        assert!(fs::read_dir("target/rotate").is_err());
364        fs::create_dir("target/rotate").unwrap();
365
366        writeln!(rot, "c").unwrap();
367        assert_eq!("", fs::read_to_string("target/rotate/log").unwrap());
368
369        writeln!(rot, "d").unwrap();
370        assert_eq!("", fs::read_to_string("target/rotate/log").unwrap());
371        assert_eq!("d\n", fs::read_to_string("target/rotate/log.0").unwrap());
372    }
373
374    #[test]
375    fn write_complete_record_until_bytes_surpassed() {
376        let _ = fs::remove_dir_all("target/surpassed_bytes");
377        fs::create_dir("target/surpassed_bytes").unwrap();
378
379        let mut rot = FileRotate::new(
380            "target/surpassed_bytes/log",
381            RotationMode::BytesSurpassed(1),
382            1,
383        )
384        .unwrap();
385
386        write!(rot, "0123456789").unwrap();
387        rot.flush().unwrap();
388        assert!(Path::new("target/surpassed_bytes/log.0").exists());
389        // shouldn't exist yet - because entire record was written in one shot
390        assert!(!Path::new("target/surpassed_bytes/log.1").exists());
391
392        // This should create the second file
393        write!(rot, "0123456789").unwrap();
394        rot.flush().unwrap();
395        assert!(Path::new("target/surpassed_bytes/log.1").exists());
396
397        fs::remove_dir_all("target/surpassed_bytes").unwrap();
398    }
399
400    #[quickcheck_macros::quickcheck]
401    fn arbitrary_lines(count: usize) {
402        let _ = fs::remove_dir_all("target/arbitrary_lines");
403        fs::create_dir("target/arbitrary_lines").unwrap();
404
405        let count = count.max(1);
406        let mut rot =
407            FileRotate::new("target/arbitrary_lines/log", RotationMode::Lines(count), 0).unwrap();
408
409        for _ in 0..count - 1 {
410            writeln!(rot).unwrap();
411        }
412
413        rot.flush().unwrap();
414        assert!(!Path::new("target/arbitrary_lines/log.0").exists());
415        writeln!(rot).unwrap();
416        assert!(Path::new("target/arbitrary_lines/log.0").exists());
417
418        fs::remove_dir_all("target/arbitrary_lines").unwrap();
419    }
420
421    #[quickcheck_macros::quickcheck]
422    fn arbitrary_bytes() {
423        let _ = fs::remove_dir_all("target/arbitrary_bytes");
424        fs::create_dir("target/arbitrary_bytes").unwrap();
425
426        let count = 0.max(1);
427        let mut rot =
428            FileRotate::new("target/arbitrary_bytes/log", RotationMode::Bytes(count), 0).unwrap();
429
430        for _ in 0..count {
431            write!(rot, "0").unwrap();
432        }
433
434        rot.flush().unwrap();
435        assert!(!Path::new("target/arbitrary_bytes/log.0").exists());
436        write!(rot, "1").unwrap();
437        assert!(Path::new("target/arbitrary_bytes/log.0").exists());
438        assert_eq!(
439            "0",
440            fs::read_to_string("target/arbitrary_bytes/log.0").unwrap()
441        );
442
443        fs::remove_dir_all("target/arbitrary_bytes").unwrap();
444    }
445}