Mercurial > public > mercurial-scm > hg
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 } |