diff rust/hg-core/src/logging.rs @ 46638: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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/rust/hg-core/src/logging.rs	Thu Feb 11 15:51:11 2021 +0100
@@ -0,0 +1,101 @@
+use crate::errors::{HgError, HgResultExt, IoErrorContext, IoResultExt};
+use crate::repo::Vfs;
+use std::io::Write;
+
+/// An utility to append to a log file with the given name, and optionally
+/// rotate it after it reaches a certain maximum size.
+///
+/// Rotation works by renaming "example.log" to "example.log.1", after renaming
+/// "example.log.1" to "example.log.2" etc up to the given maximum number of
+/// files.
+pub struct LogFile<'a> {
+    vfs: Vfs<'a>,
+    name: &'a str,
+    max_size: Option<u64>,
+    max_files: u32,
+}
+
+impl<'a> LogFile<'a> {
+    pub fn new(vfs: Vfs<'a>, name: &'a str) -> Self {
+        Self {
+            vfs,
+            name,
+            max_size: None,
+            max_files: 0,
+        }
+    }
+
+    /// Rotate before writing to a log file that was already larger than the
+    /// given size, in bytes. `None` disables rotation.
+    pub fn max_size(mut self, value: Option<u64>) -> Self {
+        self.max_size = value;
+        self
+    }
+
+    /// Keep this many rotated files `{name}.1` up to `{name}.{max}`, in
+    /// addition to the original `{name}` file.
+    pub fn max_files(mut self, value: u32) -> Self {
+        self.max_files = value;
+        self
+    }
+
+    /// Append the given `bytes` as-is to the log file, after rotating if
+    /// needed.
+    ///
+    /// No trailing newline is added. Make sure to include one in `bytes` if
+    /// desired.
+    pub fn write(&self, bytes: &[u8]) -> Result<(), HgError> {
+        let path = self.vfs.join(self.name);
+        let context = || IoErrorContext::WritingFile(path.clone());
+        let open = || {
+            std::fs::OpenOptions::new()
+                .create(true)
+                .append(true)
+                .open(&path)
+                .with_context(context)
+        };
+        let mut file = open()?;
+        if let Some(max_size) = self.max_size {
+            if file.metadata().with_context(context)?.len() >= max_size {
+                // For example with `max_files == 5`, the first iteration of
+                // this loop has `i == 4` and renames `{name}.4` to `{name}.5`.
+                // The last iteration renames `{name}.1` to
+                // `{name}.2`
+                for i in (1..self.max_files).rev() {
+                    self.vfs
+                        .rename(
+                            format!("{}.{}", self.name, i),
+                            format!("{}.{}", self.name, i + 1),
+                        )
+                        .io_not_found_as_none()?;
+                }
+                // Then rename `{name}` to `{name}.1`. This is the
+                // previously-opened `file`.
+                self.vfs
+                    .rename(self.name, format!("{}.1", self.name))
+                    .io_not_found_as_none()?;
+                // Finally, create a new `{name}` file and replace our `file`
+                // handle.
+                file = open()?;
+            }
+        }
+        file.write_all(bytes).with_context(context)?;
+        file.sync_all().with_context(context)
+    }
+}
+
+#[test]
+fn test_rotation() {
+    let temp = tempfile::tempdir().unwrap();
+    let vfs = Vfs { base: temp.path() };
+    let logger = LogFile::new(vfs, "log").max_size(Some(3)).max_files(2);
+    logger.write(b"one\n").unwrap();
+    logger.write(b"two\n").unwrap();
+    logger.write(b"3\n").unwrap();
+    logger.write(b"four\n").unwrap();
+    logger.write(b"five\n").unwrap();
+    assert_eq!(vfs.read("log").unwrap(), b"five\n");
+    assert_eq!(vfs.read("log.1").unwrap(), b"3\nfour\n");
+    assert_eq!(vfs.read("log.2").unwrap(), b"two\n");
+    assert!(vfs.read("log.3").io_not_found_as_none().unwrap().is_none());
+}