logs_wheel/
lib.rs

1#![doc(html_favicon_url = "https://codeberg.org/mo8it/logs-wheel/raw/branch/main/wheel.svg")]
2#![doc(html_logo_url = "https://codeberg.org/mo8it/logs-wheel/raw/branch/main/wheel.svg")]
3#![doc = include_str!("../README.md")]
4
5mod date_stamp;
6mod formatters;
7mod oldest_path_finder;
8mod rolling;
9
10use std::{
11    fs::{create_dir_all, File, OpenOptions},
12    io,
13    path::Path,
14};
15
16use rolling::{delete_oldest_file_over_max, roll, RollingStatus};
17
18/// The initializer of the log file.
19/// See [`Self::init`].
20pub struct LogFileInitializer<'a, P>
21where
22    P: AsRef<Path>,
23{
24    /// The directory where the log files will be placed in.
25    ///
26    /// The directory and its parent directories will be recursively created if they don't already exist while calling [`init`](Self::init)
27    /// (equivalent to `mkdir -p`).
28    ///
29    /// The directory should be used exclusively for the log files.
30    /// Other files in the directory will slow down the process of counting existing old files
31    /// and finding the oldest one to delete if [`max_n_old_files`](Self::max_n_old_files) is exceeded.
32    pub directory: P,
33
34    /// The file name of the uncompressed log file that will be opened and returned (`directory/filename`).
35    pub filename: &'a str,
36
37    /// The maximum number of old (compressed) log files.
38    ///
39    /// The value 0 leads to directly returning the file in append mode.
40    /// The value of [`preferred_max_file_size_mib`](Self::preferred_max_file_size_mib) will be ignored in that case.
41    ///
42    /// You shouldn't manually set [`max_n_old_files`](Self::max_n_old_files) to 0.
43    /// If you don't want rolling, just create the directory and the file manually and open the file in append mode
44    /// instead of having this library as a dependency.
45    /// The value 0 is just supported as a fallback in case the user of your program wants to deactivate rolling.
46    pub max_n_old_files: usize,
47
48    /// The preferred maximum size of the uncompressed log file `directory/filename` in MiB.
49    ///
50    /// It is called the _preferred_ maximum file size because this file size can be exceeded before the next initialization.
51    /// If the file size exceeds the preferred maximum, rolling will only happen if it was not already done on the same day (in UTC).
52    pub preferred_max_file_size_mib: u64,
53}
54
55impl<'a, P> LogFileInitializer<'a, P>
56where
57    P: AsRef<Path>,
58{
59    /// Return a file at `directory/filename` for appending new logs.
60    ///
61    /// Rolling will be applied to the file `directory/file` if all the following conditions are true:
62    /// - The file already exists.
63    /// - The file has a size >= [`preferred_max_file_size_mib`](Self::preferred_max_file_size_mib) (in MiB).
64    /// - No rolling was already done today.
65    ///
66    /// In the case of rolling, the file will be compressed with GZIP to `directory/filename-YYYYMMDD.gz`
67    /// with today's date (in UTC).
68    ///
69    /// If rolling was applied and the number of old files exceeds [`max_n_old_files`](Self::max_n_old_files),
70    /// the oldest file will be deleted.
71    ///
72    /// # Example
73    /// ```
74    /// # use logs_wheel::LogFileInitializer;
75    /// let log_file = LogFileInitializer {
76    ///   directory: "logs",
77    ///   filename: "test",
78    ///   max_n_old_files: 2,
79    ///   preferred_max_file_size_mib: 1,
80    /// }.init()?;
81    /// # Ok::<(), std::io::Error>(())
82    /// ```
83    ///
84    /// # Compatibility
85    /// Only UTF8 paths are supported.
86    pub fn init(self) -> io::Result<File> {
87        let directory = self.directory.as_ref();
88        create_dir_all(directory)?;
89
90        let log_path = directory.join(self.filename);
91        let append_or_new = || OpenOptions::new().append(true).create(true).open(&log_path);
92
93        if self.max_n_old_files == 0 {
94            return append_or_new();
95        }
96
97        let log_metadata = match log_path.metadata() {
98            Ok(v) => v,
99            Err(e) => match e.kind() {
100                io::ErrorKind::NotFound => return append_or_new(),
101                _ => return Err(e),
102            },
103        };
104
105        let size_mib = log_metadata.len() >> 20;
106        if size_mib < self.preferred_max_file_size_mib {
107            return append_or_new();
108        }
109
110        match roll(&log_path, directory, self.filename)? {
111            RollingStatus::Done => {
112                delete_oldest_file_over_max(directory, self.filename, self.max_n_old_files)?;
113                OpenOptions::new().write(true).truncate(true).open(log_path)
114            }
115            RollingStatus::AlreadyDoneToday => append_or_new(),
116        }
117    }
118}