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}