//! Detect log rotation and truncation.
//!
//! Tools like `logrotate` typically rotate by renaming and replacing the
//! original path with a fresh, empty file. We detect rotation by watching
//! the inode and size of the currently-open file.
use std::os::unix::fs::MetadataExt;
use std::path::Path;
use anyhow::{Context, Result};
use tokio::fs;
/// Snapshot of the relevant stat fields.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Snapshot {
pub inode: u64,
pub size: u64,
pub dev: u64,
}
impl Snapshot {
pub async fn of(path: impl AsRef<Path>) -> Result<Self> {
let md = fs::metadata(path.as_ref()).await
.with_context(|| format!("stat {}", path.as_ref().display()))?;
Ok(Self {
inode: md.ino(),
size: md.size(),
dev: md.dev(),
})
}
}
/// Result of comparing two snapshots.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Delta {
/// Same inode, size is >= previous: normal append.
Appended,
/// Same inode, size dropped: truncation.
Truncated,
/// Different inode: rotation.
Rotated,
/// Device id changed: likely atomic mount swap.
Replaced,
/// Nothing changed.
Unchanged,
}
pub fn compare(prev: &Snapshot, current: &Snapshot) -> Delta {
if prev.dev != current.dev {
return Delta::Replaced;
}
if prev.inode != current.inode {
return Delta::Rotated;
}
match current.size.cmp(&prev.size) {
std::cmp::Ordering::Less => Delta::Truncated,
std::cmp::Ordering::Greater => Delta::Appended,
std::cmp::Ordering::Equal => Delta::Unchanged,
}
}
/// Convenience wrapper that re-reads the file's metadata and compares it
/// against `prev`, returning the new snapshot and a delta.
pub async fn poll(path: impl AsRef<Path>, prev: &Snapshot) -> Result<(Snapshot, Delta)> {
let current = Snapshot::of(path).await?;
let delta = compare(prev, ¤t);
Ok((current, delta))
}
impl Delta {
pub fn requires_reopen(self) -> bool {
matches!(self, Delta::Rotated | Delta::Replaced | Delta::Truncated)
}
}