diagprint 0.8.1

A Rust diagnostics lifecycle framework for structured diagnostics, rendering, remediation, CI, editors, and telemetry.
Documentation
use chrono::{DateTime, Local};
use std::{
    fs, io,
    path::{Path, PathBuf},
    time::SystemTime,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RotationCadence {
    Never,
    Hourly,
    Daily,
}
#[derive(Debug, Clone)]
pub struct RotationPolicy {
    pub max_file_size: Option<u64>,
    pub max_files: usize,
    pub cadence: RotationCadence,
}
impl Default for RotationPolicy {
    fn default() -> Self {
        Self {
            max_file_size: None,
            max_files: 5,
            cadence: RotationCadence::Never,
        }
    }
}
#[derive(Debug, Clone)]
pub struct RotationState {
    pub policy: RotationPolicy,
}
impl RotationState {
    pub fn new(policy: RotationPolicy) -> Self {
        Self { policy }
    }
    pub fn should_rotate(&self, path: &Path, incoming: usize) -> io::Result<bool> {
        if !path.exists() {
            return Ok(false);
        }
        if let Some(max) = self.policy.max_file_size {
            if fs::metadata(path)?.len().saturating_add(incoming as u64) > max {
                return Ok(true);
            }
        }
        let m = fs::metadata(path)?.modified().unwrap_or(SystemTime::now());
        let then: DateTime<Local> = m.into();
        let now = Local::now();
        Ok(match self.policy.cadence {
            RotationCadence::Never => false,
            RotationCadence::Hourly => {
                then.format("%Y%m%d%H").to_string() != now.format("%Y%m%d%H").to_string()
            }
            RotationCadence::Daily => {
                then.format("%Y%m%d").to_string() != now.format("%Y%m%d").to_string()
            }
        })
    }
    pub fn rotate(&self, path: &Path) -> io::Result<Option<PathBuf>> {
        if !path.exists() {
            return Ok(None);
        }
        if self.policy.max_files == 0 {
            fs::write(path, "")?;
            return Ok(None);
        }
        let stamp = Local::now().format("%Y%m%d-%H%M%S");
        let mut archive = path.as_os_str().to_owned();
        archive.push(format!(".{stamp}"));
        let archive = PathBuf::from(archive);
        fs::rename(path, &archive)?;
        self.cleanup(path)?;
        Ok(Some(archive))
    }
    fn cleanup(&self, path: &Path) -> io::Result<()> {
        let Some(parent) = path.parent() else {
            return Ok(());
        };
        let base = path
            .file_name()
            .unwrap_or_default()
            .to_string_lossy()
            .to_string()
            + ".";
        let mut v: Vec<_> = fs::read_dir(parent)?
            .filter_map(Result::ok)
            .filter(|e| e.file_name().to_string_lossy().starts_with(&base))
            .collect();
        v.sort_by_key(|e| e.metadata().and_then(|m| m.modified()).ok());
        while v.len() > self.policy.max_files {
            let e = v.remove(0);
            let _ = fs::remove_file(e.path());
        }
        Ok(())
    }
}