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