use std::{
fs::{self, File, OpenOptions},
io::Write,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize};
use crate::RuntimeResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
pub struct RotatingLog {
path: PathBuf,
file: File,
max_bytes: u64,
written: u64,
}
const MAX_ROTATED_FILES: u32 = 3;
impl LogLevel {
pub const fn as_cli_flag(self) -> &'static str {
match self {
Self::Error => "--error",
Self::Warn => "--warn",
Self::Info => "--info",
Self::Debug => "--debug",
Self::Trace => "--trace",
}
}
pub const fn as_tracing_level(self) -> tracing::Level {
match self {
Self::Error => tracing::Level::ERROR,
Self::Warn => tracing::Level::WARN,
Self::Info => tracing::Level::INFO,
Self::Debug => tracing::Level::DEBUG,
Self::Trace => tracing::Level::TRACE,
}
}
}
impl RotatingLog {
pub fn new(log_dir: &Path, prefix: &str, max_bytes: u64) -> RuntimeResult<Self> {
fs::create_dir_all(log_dir)?;
let path = log_dir.join(format!("{prefix}.log"));
let written = path.metadata().map(|m| m.len()).unwrap_or(0);
let file = OpenOptions::new().create(true).append(true).open(&path)?;
Ok(Self {
path,
file,
max_bytes,
written,
})
}
pub fn write(&mut self, data: &[u8]) -> RuntimeResult<()> {
if self.written + data.len() as u64 > self.max_bytes {
self.rotate()?;
}
self.file.write_all(data)?;
self.written += data.len() as u64;
Ok(())
}
pub fn flush(&mut self) -> RuntimeResult<()> {
self.file.flush()?;
Ok(())
}
}
impl RotatingLog {
fn rotate(&mut self) -> RuntimeResult<()> {
self.file.flush()?;
for i in (1..=MAX_ROTATED_FILES).rev() {
let from = format!("{}.{i}", self.path.display());
let to = format!("{}.{}", self.path.display(), i + 1);
if Path::new(&from).exists() {
fs::rename(&from, &to)?;
}
}
let rotated = format!("{}.1", self.path.display());
fs::rename(&self.path, &rotated)?;
self.file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
self.written = 0;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::LogLevel;
#[test]
fn test_log_level_cli_flags() {
assert_eq!(LogLevel::Error.as_cli_flag(), "--error");
assert_eq!(LogLevel::Warn.as_cli_flag(), "--warn");
assert_eq!(LogLevel::Info.as_cli_flag(), "--info");
assert_eq!(LogLevel::Debug.as_cli_flag(), "--debug");
assert_eq!(LogLevel::Trace.as_cli_flag(), "--trace");
}
}