1use file_rotate::compression::Compression;
22use file_rotate::suffix::{
23 AppendCount, AppendTimestamp, DateFrom, FileLimit, Representation, SuffixScheme,
24};
25use file_rotate::SuffixInfo;
26use flate2::write::GzEncoder;
27use parking_lot::Mutex;
28use std::cell::UnsafeCell;
29use std::collections::BTreeSet;
30use std::fs::{self, File, OpenOptions};
31use std::io;
32use std::mem::transmute;
33use std::path::{Path, PathBuf};
34use std::sync::Arc;
35use std::thread;
36use std::time::{Duration, SystemTime};
37
38#[derive(Hash, Clone, Copy, PartialEq)]
39pub enum Age {
40 Day,
41 Hour,
42}
43
44#[derive(Hash, Clone, Copy, PartialEq)]
46pub struct ByAge {
47 pub age_type: Age,
49
50 pub use_last_time: bool,
54}
55
56#[derive(Hash, Clone, Copy, PartialEq)]
58pub enum Upkeep {
59 Age(chrono::TimeDelta),
61 Count(usize),
63 All,
65}
66
67#[derive(Hash)]
72pub struct Rotation {
73 pub by_age: Option<ByAge>,
74 pub by_size: Option<u64>,
75
76 pub time_fmt: Option<&'static str>,
80
81 pub upkeep: Upkeep,
83
84 pub archive_dir: Option<PathBuf>,
87
88 pub compress_exclude: Option<usize>,
91}
92
93impl Rotation {
94 pub fn by_size(size_limit: u64, max_files: Option<usize>) -> Self {
101 let upkeep =
102 if let Some(_max_files) = max_files { Upkeep::Count(_max_files) } else { Upkeep::All };
103 Self {
104 by_age: None,
105 by_size: Some(size_limit),
106 time_fmt: None,
107 upkeep,
108 archive_dir: None,
109 compress_exclude: None,
110 }
111 }
112
113 pub fn by_age(
127 age: Age, use_last_time: bool, time_fmt: &'static str, max_time: Option<chrono::TimeDelta>,
128 ) -> Self {
129 let upkeep =
130 if let Some(_max_time) = max_time { Upkeep::Age(_max_time) } else { Upkeep::All };
131 Self {
132 by_age: Some(ByAge { age_type: age, use_last_time }),
133 by_size: None,
134 time_fmt: Some(time_fmt),
135 upkeep,
136 compress_exclude: None,
137 archive_dir: None,
138 }
139 }
140
141 pub fn by_age_and_size(
155 age: Age, size_limit: u64, use_last_time: bool, time_fmt: &'static str,
156 max_time: Option<chrono::TimeDelta>,
157 ) -> Self {
158 let upkeep =
159 if let Some(_max_time) = max_time { Upkeep::Age(_max_time) } else { Upkeep::All };
160 Self {
161 by_age: Some(ByAge { age_type: age, use_last_time }),
162 by_size: Some(size_limit),
163 time_fmt: Some(time_fmt),
164 upkeep,
165 compress_exclude: None,
166 archive_dir: None,
167 }
168 }
169
170 pub fn compress_exclude(mut self, un_compress_files: usize) -> Self {
174 self.compress_exclude.replace(un_compress_files);
175 self
176 }
177
178 pub fn archive_dir<P: Into<PathBuf>>(mut self, archive_dir: P) -> Self {
180 self.archive_dir.replace(archive_dir.into());
181 self
182 }
183
184 pub(crate) fn build(&self, file_path: &Path) -> LogRotate {
185 assert!(
186 self.by_age.is_some() || self.by_size.is_some(),
187 "by_age and by_size can not be both None"
188 );
189 let archive_dir = if let Some(_dir) = &self.archive_dir {
190 _dir.clone()
191 } else {
192 file_path.parent().unwrap().to_path_buf()
194 };
195 let mut size = None;
196 let mut age = None;
197 let mut date_from = DateFrom::Now;
198 if let Some(by_age) = &self.by_age {
199 if by_age.use_last_time {
200 match by_age.age_type {
201 Age::Hour => {
202 date_from = DateFrom::DateHourAgo;
203 }
204 Age::Day => {
205 date_from = DateFrom::DateYesterday;
206 }
207 }
208 }
209 age.replace(LimiterAge::new(by_age.age_type));
210 }
211 if let Some(_size) = &self.by_size {
212 size.replace(LimiterSize::new(*_size));
213 }
214 let c = if let Some(compress) = &self.compress_exclude {
215 Compression::OnRotate(*compress)
216 } else {
217 Compression::None
218 };
219 let backend;
220 if let Some(time_fmt) = self.time_fmt {
221 let file_limit = match self.upkeep {
222 Upkeep::Age(d) => FileLimit::Age(d),
223 Upkeep::Count(c) => FileLimit::MaxFiles(c),
224 Upkeep::All => FileLimit::Unlimited,
225 };
226 let schema = AppendTimestamp { format: time_fmt, file_limit, date_from };
227 backend = Backend::Time(UnsafeCell::new(_Backend::new(
228 archive_dir.clone(),
229 file_path,
230 self.upkeep,
231 c,
232 schema,
233 )));
234 } else {
235 let file_limit = match self.upkeep {
236 Upkeep::Age(_) => 0,
237 Upkeep::Count(c) => c,
238 Upkeep::All => 0,
239 };
240 let schema = AppendCount::new(file_limit);
241 backend = Backend::Num(UnsafeCell::new(_Backend::new(
242 archive_dir.clone(),
243 file_path,
244 self.upkeep,
245 c,
246 schema,
247 )));
248 }
249 return LogRotate {
250 size_limit: size,
251 age_limit: age,
252 backend: Arc::new(backend),
253 th: Mutex::new(None),
254 };
255 }
256}
257
258pub(crate) struct LogRotate {
259 size_limit: Option<LimiterSize>,
260 age_limit: Option<LimiterAge>,
261 backend: Arc<Backend>,
262 th: Mutex<Option<thread::JoinHandle<()>>>,
263}
264
265impl LogRotate {
266 pub fn rotate<S: FileSinkTrait>(&self, sink: &S) -> bool {
267 let mut need_rotate = false;
268 if let Some(age) = self.age_limit.as_ref() {
269 if age.check(sink) {
270 need_rotate = true;
271 }
272 }
273 if let Some(size) = self.size_limit.as_ref() {
274 if size.check(sink) {
275 need_rotate = true;
276 }
277 }
278 if !need_rotate {
279 return false;
280 }
281 self.wait();
282
283 self.backend.rename_files();
284 let backend = self.backend.clone();
285 let th = thread::spawn(move || {
286 let _ = backend.handle_old_files();
287 });
288 self.th.lock().replace(th);
289 true
290 }
291
292 pub fn wait(&self) {
294 if let Some(th) = self.th.lock().take() {
295 let _ = th.join();
296 }
297 }
298}
299
300pub(crate) struct LimiterSize {
301 limit: u64,
302}
303
304impl LimiterSize {
305 pub fn new(size: u64) -> Self {
306 Self { limit: size }
307 }
308
309 #[inline]
310 pub fn check<S: FileSinkTrait>(&self, sink: &S) -> bool {
311 return sink.get_size() > self.limit;
312 }
313}
314
315pub(crate) struct LimiterAge {
316 limit: Duration,
317}
318
319impl LimiterAge {
320 pub fn new(limit: Age) -> Self {
321 Self {
322 limit: match limit {
323 Age::Hour => Duration::from_secs(60 * 60),
324 Age::Day => Duration::from_secs(24 * 60 * 60),
325 },
326 }
327 }
328
329 pub fn check<S: FileSinkTrait>(&self, sink: &S) -> bool {
330 let now = SystemTime::now();
331 let start_ts = sink.get_create_time();
332 match now.duration_since(start_ts) {
333 Ok(d) => return d > self.limit,
334 Err(_) => return true, }
336 }
337}
338
339pub(crate) trait FileSinkTrait {
340 fn get_create_time(&self) -> SystemTime;
341
342 fn get_size(&self) -> u64;
343}
344
345enum Backend {
346 Num(UnsafeCell<_Backend<AppendCount>>),
347 Time(UnsafeCell<_Backend<AppendTimestamp>>),
348}
349
350unsafe impl Send for Backend {}
351unsafe impl Sync for Backend {}
352
353impl Backend {
354 fn rename_files(&self) {
355 match self {
356 Self::Num(_inner) => {
357 let inner: &mut _Backend<AppendCount> = unsafe { transmute(_inner.get()) };
358 inner.rename_files();
359 }
360 Self::Time(_inner) => {
361 let inner: &mut _Backend<AppendTimestamp> = unsafe { transmute(_inner.get()) };
362 inner.rename_files();
363 }
364 }
365 }
366
367 fn handle_old_files(&self) -> io::Result<()> {
368 match self {
369 Self::Num(_inner) => {
370 let inner: &mut _Backend<AppendCount> = unsafe { transmute(_inner.get()) };
371 inner.handle_old_files()
372 }
373 Self::Time(_inner) => {
374 let inner: &mut _Backend<AppendTimestamp> = unsafe { transmute(_inner.get()) };
375 inner.handle_old_files()
376 }
377 }
378 }
379}
380
381struct _Backend<S: SuffixScheme> {
383 archive_dir: PathBuf,
384 base_path: PathBuf, log_path: PathBuf, compress: Compression,
387 suffix_scheme: S,
388 suffixes: BTreeSet<SuffixInfo<S::Repr>>,
390 upkeep: Upkeep,
391}
392
393fn compress(path: &Path) -> io::Result<()> {
394 let dest_path = PathBuf::from(format!("{}.gz", path.display()));
395
396 let mut src_file = File::open(path)?;
397 let dest_file = OpenOptions::new()
398 .write(true)
399 .create(true)
400 .truncate(true)
401 .append(false)
402 .open(&dest_path)?;
403
404 assert!(path.exists());
405 assert!(dest_path.exists());
406 let mut encoder = GzEncoder::new(dest_file, flate2::Compression::default());
407 io::copy(&mut src_file, &mut encoder)?;
408
409 fs::remove_file(path)?;
410
411 Ok(())
412}
413
414impl<S: SuffixScheme> _Backend<S> {
415 fn new(
416 archive_dir: PathBuf, file: &Path, upkeep: Upkeep, compress: Compression, schema: S,
417 ) -> Self {
418 let base_path = archive_dir.as_path().join(Path::new(file.file_name().unwrap()));
419 let mut s = Self {
420 archive_dir,
421 log_path: file.to_path_buf(),
422 base_path,
423 upkeep,
424 compress,
425 suffix_scheme: schema,
426 suffixes: BTreeSet::new(),
427 };
428 s.ensure_dir();
429 s.scan_suffixes();
430 s
431 }
432
433 #[inline]
434 fn ensure_dir(&self) {
435 if !self.archive_dir.exists() {
436 fs::create_dir_all(&self.archive_dir).expect("create dir");
437 }
438 }
439
440 #[inline]
441 fn scan_suffixes(&mut self) {
442 self.suffixes = self.suffix_scheme.scan_suffixes(&self.base_path);
443 }
444
445 #[inline]
446 fn rename_files(&mut self) {
447 self.ensure_dir();
448 let new_suffix_info = self._move_file_with_suffix(None).expect("move files");
449 self.suffixes.insert(new_suffix_info);
450 }
451
452 #[inline]
453 fn handle_old_files(&mut self) -> io::Result<()> {
454 let mut result = Ok(());
458 if let Upkeep::All = &self.upkeep {
459 } else {
460 let mut youngest_old = None;
461 for (i, suffix) in self.suffixes.iter().enumerate().rev() {
462 if self.suffix_scheme.too_old(&suffix.suffix, i) {
463 result = result.and(fs::remove_file(suffix.to_path(&self.base_path)));
464 youngest_old = Some((*suffix).clone());
465 } else {
466 break;
467 }
468 }
469 if let Some(youngest_old) = youngest_old {
470 let _ = self.suffixes.split_off(&youngest_old);
472 }
473 }
474
475 if let Compression::OnRotate(max_file_n) = self.compress {
477 let n = (self.suffixes.len() as i32 - max_file_n as i32).max(0) as usize;
478 let suffixes_to_compress = self
480 .suffixes
481 .iter()
482 .rev()
483 .take(n)
484 .filter(|info| !info.compressed)
485 .cloned()
486 .collect::<Vec<_>>();
487 for info in suffixes_to_compress {
488 let path = info.suffix.to_path(&self.base_path);
490 compress(&path)?;
491
492 self.suffixes.replace(SuffixInfo { compressed: true, ..info });
493 }
494 }
495 result
496 }
497
498 fn _move_file_with_suffix(
504 &mut self, old_suffix_info: Option<SuffixInfo<S::Repr>>,
505 ) -> io::Result<SuffixInfo<S::Repr>> {
506 let newest_suffix = self.suffixes.iter().next().map(|info| &info.suffix);
510
511 let new_suffix = self.suffix_scheme.rotate_file(
512 &self.base_path,
513 newest_suffix,
514 &old_suffix_info.clone().map(|i| i.suffix),
515 )?;
516
517 let new_suffix_info = SuffixInfo {
519 suffix: new_suffix,
520 compressed: old_suffix_info.as_ref().map(|x| x.compressed).unwrap_or(false),
521 };
522 let new_path = new_suffix_info.to_path(&self.base_path);
523
524 let existing_suffix_info = self.suffixes.get(&new_suffix_info).cloned();
526
527 let newly_created_suffix = if let Some(existing_suffix_info) = existing_suffix_info {
529 self.suffixes.replace(new_suffix_info);
533 self._move_file_with_suffix(Some(existing_suffix_info))?
535 } else {
536 new_suffix_info
537 };
538
539 let old_path = match old_suffix_info {
540 Some(suffix) => suffix.to_path(&self.base_path),
541 None => self.log_path.clone(), };
543 assert!(old_path.exists());
545 assert!(!new_path.exists());
546 fs::rename(old_path, new_path)?;
547
548 Ok(newly_created_suffix)
549 }
550}