tracing_rolling_file_inc/
lib.rs1use 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
49pub trait RollingCondition {
51 fn should_rollover(&mut self, now: &DateTime<Local>, current_filesize: u64) -> bool;
53}
54
55#[derive(Copy, Clone, Debug, Eq, PartialEq)]
57pub enum RollingFrequency {
58 EveryDay,
59 EveryHour,
60 EveryMinute,
61}
62
63impl RollingFrequency {
64 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#[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 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().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 appender.open_writer_if_needed()?;
166 Ok(appender)
167 }
168
169 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 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 r = Err(e);
197 }
198
199 r
200 }
201
202 pub fn rollover(&mut self) -> io::Result<()> {
204 self.flush()?;
206 self.writer_opt.take();
208 self.current_filesize = 0;
209 self.rotate_files()?;
210 self.open_writer_if_needed()
211 }
212
213 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 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 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::*;