use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::Path;
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
static AUDIT: OnceLock<Option<Mutex<File>>> = OnceLock::new();
pub fn init(log_path: &Path) {
AUDIT.get_or_init(|| {
if log_path.as_os_str().is_empty() {
return None;
}
match OpenOptions::new()
.create(true)
.append(true)
.open(log_path)
{
Ok(f) => Some(Mutex::new(f)),
Err(e) => {
eprintln!(
"kevy: audit log {} could not open: {e}",
log_path.display()
);
None
}
}
});
}
pub fn record(args: &[&[u8]]) {
let Some(Some(mu)) = AUDIT.get() else { return };
let mut line = String::with_capacity(128);
let micros = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_micros())
.unwrap_or(0);
line.push_str(µs.to_string());
for arg in args {
line.push('\t');
let s = String::from_utf8_lossy(&arg[..arg.len().min(256)]);
for c in s.chars() {
match c {
'\t' | '\n' | '\r' => line.push(' '),
_ => line.push(c),
}
}
if arg.len() > 256 {
line.push('…');
}
}
line.push('\n');
if let Ok(mut f) = mu.lock() {
let _ = f.write_all(line.as_bytes());
let _ = f.flush();
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(name: &str) -> std::path::PathBuf {
let nanos = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos();
std::env::temp_dir().join(format!("kevy-audit-{name}-{nanos}"))
}
#[test]
fn record_off_path_noop() {
let path = tmp("off");
record(&[b"CONFIG", b"SET", b"maxmemory", b"1g"]);
assert!(!path.exists());
}
}