1#![deny(warnings)]
28
29use chrono::prelude::*;
30use std::{
31 convert::TryFrom,
32 fs::{self, File, OpenOptions},
33 io::{self, BufWriter, Write},
34 path::Path,
35};
36use symlink::{remove_symlink_auto, symlink_auto};
37
38pub trait RollingCondition {
40 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
42}
43
44#[derive(Copy, Clone, Debug, Eq, PartialEq)]
46pub enum RollingFrequency {
47 EveryDay,
48 EveryHour,
49 EveryMinute,
50}
51
52impl RollingFrequency {
53 pub fn equivalent_datetime(&self, dt: &DateTime<Local>) -> DateTime<Local> {
56 match self {
57 RollingFrequency::EveryDay => Local
58 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), 0, 0, 0)
59 .unwrap(),
60 RollingFrequency::EveryHour => Local
61 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), 0, 0)
62 .unwrap(),
63 RollingFrequency::EveryMinute => Local
64 .with_ymd_and_hms(dt.year(), dt.month(), dt.day(), dt.hour(), dt.minute(), 0)
65 .unwrap(),
66 }
67 }
68}
69
70#[derive(Copy, Clone, Debug, Eq, PartialEq)]
81pub struct RollingConditionBasic {
82 last_write_opt: Option<DateTime<Local>>,
83 frequency_opt: Option<RollingFrequency>,
84 max_size_opt: Option<u64>,
85}
86
87impl RollingConditionBasic {
88 pub fn new() -> RollingConditionBasic {
90 RollingConditionBasic {
91 last_write_opt: None,
92 frequency_opt: None,
93 max_size_opt: None,
94 }
95 }
96
97 pub fn frequency(mut self, x: RollingFrequency) -> RollingConditionBasic {
99 self.frequency_opt = Some(x);
100 self
101 }
102
103 pub fn daily(mut self) -> RollingConditionBasic {
105 self.frequency_opt = Some(RollingFrequency::EveryDay);
106 self
107 }
108
109 pub fn hourly(mut self) -> RollingConditionBasic {
111 self.frequency_opt = Some(RollingFrequency::EveryHour);
112 self
113 }
114
115 pub fn minutely(mut self) -> RollingConditionBasic {
116 self.frequency_opt = Some(RollingFrequency::EveryMinute);
117 self
118 }
119
120 pub fn max_size(mut self, x: u64) -> RollingConditionBasic {
122 self.max_size_opt = Some(x);
123 self
124 }
125}
126
127impl Default for RollingConditionBasic {
128 fn default() -> Self {
129 RollingConditionBasic::new().frequency(RollingFrequency::EveryDay)
130 }
131}
132
133impl RollingCondition for RollingConditionBasic {
134 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool {
135 let mut rollover = false;
136 if let Some(frequency) = self.frequency_opt.as_ref() {
137 if let Some(last_write) = self.last_write_opt.as_ref() {
138 if frequency.equivalent_datetime(now) != frequency.equivalent_datetime(last_write) {
139 rollover = true;
140 }
141 }
142 }
143 if let Some(max_size) = self.max_size_opt.as_ref() {
144 if current_filesize >= *max_size {
145 rollover = true;
146 }
147 }
148 self.last_write_opt = Some(*now);
149 rollover
150 }
151}
152
153#[derive(Debug)]
158pub struct RollingFileAppender<RC>
159where
160 RC: RollingCondition,
161{
162 condition: RC,
163 folder: String,
164 prefix: String,
165 max_files: usize,
166 buffer_capacity: Option<usize>,
167 current_filesize: u64,
168 writer_opt: Option<BufWriter<File>>,
169}
170
171impl<RC> RollingFileAppender<RC>
172where
173 RC: RollingCondition,
174{
175 pub fn new(folder: &str, prefix: &str, condition: RC, max_files: usize) -> io::Result<RollingFileAppender<RC>> {
178 Self::_new(folder, prefix, condition, max_files, None)
179 }
180
181 pub fn new_with_buffer_capacity(
184 folder: &str,
185 prefix: &str,
186 condition: RC,
187 max_files: usize,
188 buffer_capacity: usize,
189 ) -> io::Result<RollingFileAppender<RC>> {
190 Self::_new(folder, prefix, condition, max_files, Some(buffer_capacity))
191 }
192
193 fn _new(
194 folder: &str,
195 prefix: &str,
196 condition: RC,
197 max_files: usize,
198 buffer_capacity: Option<usize>,
199 ) -> io::Result<RollingFileAppender<RC>> {
200 let folder = folder.to_string();
201 let prefix = prefix.to_string();
202 let mut rfa = RollingFileAppender {
203 condition,
204 folder,
205 prefix,
206 max_files,
207 buffer_capacity,
208 current_filesize: 0,
209 writer_opt: None,
210 };
211 rfa.open_writer_if_needed(&Local::now())?;
213 Ok(rfa)
214 }
215
216 fn check_and_remove_log_file(&mut self) -> io::Result<()> {
217 let files = std::fs::read_dir(&self.folder)?;
218
219 let mut log_files = vec![];
220 for f in files.flatten() {
221 let fname = f.file_name().to_string_lossy().to_string();
222 if fname.starts_with(&self.prefix) && fname != self.prefix {
223 log_files.push(fname);
224 }
225 }
226
227 log_files.sort_by(|a, b| b.cmp(a));
228
229 if log_files.len() > self.max_files {
230 for f in log_files.drain(self.max_files..) {
231 let p = Path::new(&self.folder).join(f);
232 if let Err(e) = fs::remove_file(&p) {
233 tracing::error!("WARNING: Failed to remove old logfile {}: {}", p.to_string_lossy(), e);
234 }
235 }
236 }
237 Ok(())
238 }
239
240 pub fn rollover(&mut self, now: &DateTime<Local>) -> io::Result<()> {
242 self.flush()?;
244 self.writer_opt.take();
246 self.current_filesize = 0;
247 self.open_writer_if_needed(now)
248 }
249
250 pub fn condition_ref(&self) -> &RC {
252 &self.condition
253 }
254
255 pub fn condition_mut(&mut self) -> &mut RC {
257 &mut self.condition
258 }
259
260 fn new_file_name(&self, now: &DateTime<Local>) -> String {
261 let data_str = now.format("%Y%m%d.%H%M%S").to_string();
262 format!("{}.{}", self.prefix, data_str)
263 }
264
265 fn open_writer_if_needed(&mut self, now: &DateTime<Local>) -> io::Result<()> {
267 if self.writer_opt.is_none() {
268 let p = self.new_file_name(now);
269 let new_file_path = std::path::Path::new(&self.folder).join(&p);
270 if std::fs::metadata(&self.folder).is_err() {
271 std::fs::create_dir_all(&self.folder)?;
272 }
273 let f = OpenOptions::new().append(true).create(true).open(&new_file_path)?;
274 self.writer_opt = Some(if let Some(capacity) = self.buffer_capacity {
275 BufWriter::with_capacity(capacity, f)
276 } else {
277 BufWriter::new(f)
278 });
279 {
281 let folder = std::path::Path::new(&self.folder);
282 if let Ok(path) = folder.canonicalize() {
283 let latest_log_symlink = path.join(&self.prefix);
284 let _ = remove_symlink_auto(folder.join(&self.prefix));
285 let _ = symlink_auto(new_file_path.canonicalize().unwrap(), latest_log_symlink);
286 }
287 }
288 self.current_filesize = fs::metadata(&p).map_or(0, |m| m.len());
289 self.check_and_remove_log_file()?;
290 }
291 Ok(())
292 }
293
294 pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
296 if self.condition.should_rollover(now, self.current_filesize) {
297 if let Err(e) = self.rollover(now) {
298 eprintln!("WARNING: Failed to rotate logfile {}", e);
303 }
304 }
305 self.open_writer_if_needed(now)?;
306 if let Some(writer) = self.writer_opt.as_mut() {
307 let buf_len = buf.len();
308 writer.write_all(buf).map(|_| {
309 self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX);
310 buf_len
311 })
312 } else {
313 Err(io::Error::new(
314 io::ErrorKind::Other,
315 "unexpected condition: writer is missing",
316 ))
317 }
318 }
319}
320
321impl<RC> io::Write for RollingFileAppender<RC>
322where
323 RC: RollingCondition,
324{
325 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
326 let now = Local::now();
327 self.write_with_datetime(buf, &now)
328 }
329
330 fn flush(&mut self) -> io::Result<()> {
331 if let Some(writer) = self.writer_opt.as_mut() {
332 writer.flush()?;
333 }
334 Ok(())
335 }
336}
337
338pub type BasicRollingFileAppender = RollingFileAppender<RollingConditionBasic>;
340
341#[cfg(test)]
342mod t {
343 #[test]
344 fn test_number_of_log_files() {
345 use super::*;
346 let folder = "./log";
347 let prefix = "log.log";
348
349 let _ = std::fs::remove_dir_all(folder);
350 std::fs::create_dir(folder).unwrap();
351
352 let condition = RollingConditionBasic::new().hourly();
353 let max_files = 3;
354 let mut rfa = RollingFileAppender::new(folder, prefix, condition, max_files).unwrap();
355 rfa.write_with_datetime(b"Line 1\n", &Local.with_ymd_and_hms(2021, 3, 30, 1, 2, 3).unwrap())
356 .unwrap();
357 rfa.write_with_datetime(b"Line 2\n", &Local.with_ymd_and_hms(2021, 3, 30, 2, 3, 0).unwrap())
358 .unwrap();
359 rfa.write_with_datetime(b"Line 3\n", &Local.with_ymd_and_hms(2021, 3, 31, 1, 4, 0).unwrap())
360 .unwrap();
361 rfa.write_with_datetime(b"Line 4\n", &Local.with_ymd_and_hms(2021, 5, 31, 1, 4, 0).unwrap())
362 .unwrap();
363 rfa.write_with_datetime(b"Line 5\n", &Local.with_ymd_and_hms(2022, 5, 31, 2, 4, 0).unwrap())
364 .unwrap();
365 rfa.flush().unwrap();
366 let files = std::fs::read_dir(folder).unwrap();
367 let mut log_files = vec![];
368 for f in files {
369 if let Ok(f) = f {
370 let fname = f.file_name().to_string_lossy().to_string();
371 if fname.starts_with(prefix) && fname != "log.log" {
372 log_files.push(fname);
373 }
374 }
375 }
376 assert_eq!(log_files.len(), max_files);
377 }
378}