tracing_rolling_file_inc/
lib.rs

1//! A rolling file appender with customizable rolling conditions.
2//! Includes built-in support for rolling conditions on date/time
3//! (daily, hourly, every minute) and/or size.
4//!
5//! Follows a Debian-style naming convention for logfiles,
6//! using basename, basename.1, ..., basename.N where N is
7//! the maximum number of allowed historical logfiles.
8//!
9//! This is useful to combine with the tracing crate and
10//! tracing_appender::non_blocking::NonBlocking -- use it
11//! as an alternative to tracing_appender::rolling::RollingFileAppender.
12//!
13//! # Examples
14//!
15//! ```rust
16//! # fn docs() {
17//! # use tracing_rolling_file_inc::*;
18//! let file_appender = RollingFileAppenderBase::new(
19//!     "./logs",
20//!     "log",
21//!     RollingConditionBase::new().daily(),
22//!     9
23//! ).unwrap();
24//! # }
25//! ```
26
27use chrono::prelude::*;
28use regex::Regex;
29use std::ffi::OsStr;
30use std::path::PathBuf;
31use std::str::FromStr;
32use std::sync::atomic::{AtomicUsize, Ordering};
33use std::{
34    convert::TryFrom,
35    fs::{self, File, OpenOptions},
36    io::{self, BufWriter, Write},
37    path::Path,
38};
39use thiserror::Error;
40
41#[derive(Error, Debug)]
42pub enum RollingFileError {
43    #[error("io error:")]
44    IOError(#[from] io::Error),
45    #[error("io error:")]
46    RegexError(#[from] regex::Error),
47}
48
49/// Determines when a file should be "rolled over".
50pub trait RollingCondition {
51    /// Determine and return whether or not the file should be rolled over.
52    fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
53}
54
55/// Determines how often a file should be rolled over
56#[derive(Copy, Clone, Debug, Eq, PartialEq)]
57pub enum RollingFrequency {
58    EveryDay,
59    EveryHour,
60    EveryMinute,
61}
62
63impl RollingFrequency {
64    /// Calculates a datetime that will be different if data should be in
65    /// different files.
66    pub fn equivalent_datetime(&self, dt: &DateTime<Local>) -> DateTime<Local> {
67        let (year, month, day) = (dt.year(), dt.month(), dt.day());
68        let (hour, min, sec) = match self {
69            RollingFrequency::EveryDay => (0, 0, 0),
70            RollingFrequency::EveryHour => (dt.hour(), 0, 0),
71            RollingFrequency::EveryMinute => (dt.hour(), dt.minute(), 0),
72        };
73        Local.with_ymd_and_hms(year, month, day, hour, min, sec).unwrap()
74    }
75}
76
77/// Writes data to a file, and "rolls over" to preserve older data in
78/// a separate set of files. Old files have a Debian-style naming scheme
79/// where we have base_filename, base_filename.1, ..., base_filename.N
80/// where N is the maximum number of rollover files to keep.
81#[derive(Debug)]
82pub struct RollingFileAppender<RC>
83where
84    RC: RollingCondition,
85{
86    condition: RC,
87    directory: PathBuf,
88    file_name: String,
89    suffix: String,
90    file_index: AtomicUsize,
91    max_file_count: usize,
92    current_filesize: u64,
93    writer_opt: Option<BufWriter<File>>,
94}
95
96impl<RC> RollingFileAppender<RC>
97where
98    RC: RollingCondition,
99{
100    /// Creates a new rolling file appender with the given condition.
101    /// The filename parent path must already exist.
102    pub fn new(
103        directory: impl AsRef<Path>,
104        suffix: &str,
105        condition: RC,
106        max_file_count: usize,
107    ) -> Result<RollingFileAppender<RC>, RollingFileError> {
108        let arg0 = std::env::args().next().unwrap_or_else(|| "rs".to_owned());
109        let file_name = Path::new(&arg0).file_stem().map(OsStr::to_string_lossy).unwrap(/*cannot fail*/).to_string();
110
111        let directory = directory.as_ref().to_owned();
112        let (file_index, current_filesize) = {
113            if !directory.exists() {
114                fs::create_dir_all(directory.as_path())?;
115                (AtomicUsize::new(1), 0)
116            } else {
117                let dirs = fs::read_dir(directory.as_path())?;
118                let mut current_indexes = vec![];
119                let re = Regex::new(r"\d+")?;
120                for dir in dirs {
121                    let dir = dir?;
122                    if dir.file_type()?.is_file() {
123                        if let Some(filename) = dir.file_name().to_str() {
124                            if let Some(cp) = re.captures(filename) {
125                                if let Ok(index) = usize::from_str(&cp[0]) {
126                                    current_indexes.push(index);
127                                }
128                            }
129                        }
130                    }
131                }
132
133                if !current_indexes.is_empty() {
134                    current_indexes.sort();
135                    current_indexes.reverse();
136
137                    let current_filesize = {
138                        let has_curr_log = directory.join(format!("{}.current.log", suffix));
139                        if has_curr_log.exists() {
140                            fs::metadata(has_curr_log)?.len()
141                        } else {
142                            0
143                        }
144                    };
145
146                    let max_index = current_indexes[0];
147                    (AtomicUsize::new(max_index + 1), current_filesize)
148                } else {
149                    (AtomicUsize::new(1), 0)
150                }
151            }
152        };
153
154        let mut appender = RollingFileAppender {
155            condition,
156            directory,
157            file_name,
158            suffix: suffix.to_string(),
159            file_index,
160            max_file_count,
161            current_filesize,
162            writer_opt: None,
163        };
164        // Fail if we can't open the file initially...
165        appender.open_writer_if_needed()?;
166        Ok(appender)
167    }
168
169    /// Determines the final filename, where n==0 indicates the current file
170    fn filename_for(&self, n: usize) -> PathBuf {
171        let f = self.file_name.as_str();
172        let s = self.suffix.as_str();
173        if n > 0 {
174            self.directory.join(format!("{}.{}.{}", f, n, s))
175        } else {
176            self.directory.join(format!("{}.current.{}", f, s))
177        }
178    }
179
180    /// Rotates old files to make room for a new one.
181    /// This may result in the deletion of the oldest file
182    fn rotate_files(&mut self) -> io::Result<()> {
183        let remove_index = self.file_index.load(Ordering::Acquire) as i64 - self.max_file_count as i64;
184        if remove_index > 0 {
185            let _ = fs::remove_file(self.filename_for(remove_index as usize));
186        }
187
188        let to_index = self.file_index.fetch_add(1, Ordering::Acquire);
189        let mut r = Ok(());
190        if let Err(e) = fs::rename(self.filename_for(0), self.filename_for(to_index)).or_else(|e| match e.kind() {
191            io::ErrorKind::NotFound => Ok(()),
192            _ => Err(e),
193        }) {
194            // capture the error, but continue the loop,
195            // to maximize ability to rename everything
196            r = Err(e);
197        }
198
199        r
200    }
201
202    /// Forces a rollover to happen immediately.
203    pub fn rollover(&mut self) -> io::Result<()> {
204        // Before closing, make sure all data is flushed successfully.
205        self.flush()?;
206        // We must close the current file before rotating files
207        self.writer_opt.take();
208        self.current_filesize = 0;
209        self.rotate_files()?;
210        self.open_writer_if_needed()
211    }
212
213    /// Opens a writer for the current file.
214    fn open_writer_if_needed(&mut self) -> io::Result<()> {
215        if self.writer_opt.is_none() {
216            let path = self.filename_for(0);
217            let path = Path::new(&path);
218            let mut open_options = OpenOptions::new();
219            open_options.append(true).create(true);
220            let new_file = match open_options.open(path) {
221                Ok(new_file) => new_file,
222                Err(err) => {
223                    let Some(parent) = path.parent() else {
224                        return Err(err);
225                    };
226                    fs::create_dir_all(parent)?;
227                    open_options.open(path)?
228                },
229            };
230            self.writer_opt = Some(BufWriter::new(new_file));
231            self.current_filesize = path.metadata().map_or(0, |m| m.len());
232        }
233        Ok(())
234    }
235
236    /// Writes data using the given datetime to calculate the rolling condition
237    pub fn write_with_datetime(&mut self, buf: &[u8], now: &DateTime<Local>) -> io::Result<usize> {
238        if self.condition.should_rollover(now, self.current_filesize) {
239            if let Err(e) = self.rollover() {
240                // If we can't rollover, just try to continue writing anyway
241                // (better than missing data).
242                // This will likely used to implement logging, so
243                // avoid using log::warn and log to stderr directly
244                eprintln!("WARNING: Failed to rotate logfile {}: {}", self.file_name, e);
245            }
246        }
247        self.open_writer_if_needed()?;
248        if let Some(writer) = self.writer_opt.as_mut() {
249            let buf_len = buf.len();
250            writer.write_all(buf).map(|_| {
251                self.current_filesize += u64::try_from(buf_len).unwrap_or(u64::MAX);
252                buf_len
253            })
254        } else {
255            Err(io::Error::new(
256                io::ErrorKind::Other,
257                "unexpected condition: writer is missing",
258            ))
259        }
260    }
261}
262
263impl<RC> io::Write for RollingFileAppender<RC>
264where
265    RC: RollingCondition,
266{
267    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
268        let now = Local::now();
269        self.write_with_datetime(buf, &now)
270    }
271
272    fn flush(&mut self) -> io::Result<()> {
273        if let Some(writer) = self.writer_opt.as_mut() {
274            writer.flush()?;
275        }
276        Ok(())
277    }
278}
279
280pub mod base;
281pub use base::*;