comparison rust/hg-core/src/logging.rs @ 46599:1f55cd5b292f

rust: Add a log file rotation utility This is ported to Rust from `mercurial/loggingutil.py`. The "builder" pattern is used to make it visible at call sites what the two numeric parameters mean. In Python they might simply by keyword arguments. Differential Revision: https://phab.mercurial-scm.org/D10010
author Simon Sapin <simon.sapin@octobus.net>
date Thu, 11 Feb 2021 15:51:11 +0100
parents
children 9cd35c8c6044
comparison
equal deleted inserted replaced
46598:bc08c2331f99 46599:1f55cd5b292f
1 use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt};
2 use crate::repo::Vfs;
3 use std::io::Write;
4
5 /// An utility to append to a log file with the given name, and optionally
6 /// rotate it after it reaches a certain maximum size.
7 ///
8 /// Rotation works by renaming "example.log" to "example.log.1", after renaming
9 /// "example.log.1" to "example.log.2" etc up to the given maximum number of
10 /// files.
11 pub struct LogFile<'a> {
12 vfs: Vfs<'a>,
13 name: &'a str,
14 max_size: Option<u64>,
15 max_files: u32,
16 }
17
18 impl<'a> LogFile<'a> {
19 pub fn new(vfs: Vfs<'a>, name: &'a str) -> Self {
20 Self {
21 vfs,
22 name,
23 max_size: None,
24 max_files: 0,
25 }
26 }
27
28 /// Rotate before writing to a log file that was already larger than the
29 /// given size, in bytes. `None` disables rotation.
30 pub fn max_size(mut self, value: Option<u64>) -> Self {
31 self.max_size = value;
32 self
33 }
34
35 /// Keep this many rotated files `{name}.1` up to `{name}.{max}`, in
36 /// addition to the original `{name}` file.
37 pub fn max_files(mut self, value: u32) -> Self {
38 self.max_files = value;
39 self
40 }
41
42 /// Append the given `bytes` as-is to the log file, after rotating if
43 /// needed.
44 ///
45 /// No trailing newline is added. Make sure to include one in `bytes` if
46 /// desired.
47 pub fn write(&self, bytes: &[u8]) -> Result<(), HgError> {
48 let path = self.vfs.join(self.name);
49 let context = || IoErrorContext::WritingFile(path.clone());
50 let open = || {
51 std::fs::OpenOptions::new()
52 .create(true)
53 .append(true)
54 .open(&path)
55 .with_context(context)
56 };
57 let mut file = open()?;
58 if let Some(max_size) = self.max_size {
59 if file.metadata().with_context(context)?.len() >= max_size {
60 // For example with `max_files == 5`, the first iteration of
61 // this loop has `i == 4` and renames `{name}.4` to `{name}.5`.
62 // The last iteration renames `{name}.1` to
63 // `{name}.2`
64 for i in (1..self.max_files).rev() {
65 self.vfs
66 .rename(
67 format!("{}.{}", self.name, i),
68 format!("{}.{}", self.name, i + 1),
69 )
70 .io_not_found_as_none()?;
71 }
72 // Then rename `{name}` to `{name}.1`. This is the
73 // previously-opened `file`.
74 self.vfs
75 .rename(self.name, format!("{}.1", self.name))
76 .io_not_found_as_none()?;
77 // Finally, create a new `{name}` file and replace our `file`
78 // handle.
79 file = open()?;
80 }
81 }
82 file.write_all(bytes).with_context(context)?;
83 file.sync_all().with_context(context)
84 }
85 }
86
87 #[test]
88 fn test_rotation() {
89 let temp = tempfile::tempdir().unwrap();
90 let vfs = Vfs { base: temp.path() };
91 let logger = LogFile::new(vfs, "log").max_size(Some(3)).max_files(2);
92 logger.write(b"one\n").unwrap();
93 logger.write(b"two\n").unwrap();
94 logger.write(b"3\n").unwrap();
95 logger.write(b"four\n").unwrap();
96 logger.write(b"five\n").unwrap();
97 assert_eq!(vfs.read("log").unwrap(), b"five\n");
98 assert_eq!(vfs.read("log.1").unwrap(), b"3\nfour\n");
99 assert_eq!(vfs.read("log.2").unwrap(), b"two\n");
100 assert!(vfs.read("log.3").io_not_found_as_none().unwrap().is_none());
101 }