use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;
const MAX_LOG_LINE: usize = 4096;
fn sanitize(s: &str) -> String {
s.chars()
.filter(|&c| c.is_ascii_graphic() || c == ' ' || c == '\t')
.take(256)
.collect()
}
#[derive(Debug)]
pub enum LogEvent {
Parse { run_id: String, ok: bool },
Validate { run_id: String, ok: bool },
SafetyGate { run_id: String, blocked: bool, rules: String },
Resolve { run_id: String, refs_total: usize, refs_unresolved: usize },
Expand { run_id: String, packet_type: String },
Run { run_id: String, status: String },
}
pub fn log_event(path: &Path, event: &LogEvent) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let timestamp = chrono_now();
let line = format_line(timestamp, event);
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
writeln!(file, "{}", line)?;
Ok(())
}
fn chrono_now() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let dur = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = dur.as_secs();
let millis = dur.subsec_millis();
let s = secs % 86400;
let h = s / 3600;
let m = (s % 3600) / 60;
let s = s % 60;
format!("{:02}:{:02}:{:02}.{:03}", h, m, s, millis)
}
fn format_line(timestamp: String, event: &LogEvent) -> String {
let line = match event {
LogEvent::Parse { run_id, ok } => {
format!("[{}] PARSE {} {}", timestamp, if *ok { "OK" } else { "FAIL" }, sanitize(run_id))
}
LogEvent::Validate { run_id, ok } => {
format!("[{}] VALIDATE {} {}", timestamp, if *ok { "OK" } else { "FAIL" }, sanitize(run_id))
}
LogEvent::SafetyGate { run_id, blocked, rules } => {
format!(
"[{}] SAFETY {} {} rules=[{}]",
timestamp,
if *blocked { "BLOCKED" } else { "PASS" },
sanitize(run_id),
sanitize(rules),
)
}
LogEvent::Resolve { run_id, refs_total, refs_unresolved } => {
format!(
"[{}] RESOLVE {} refs={} unresolved={}",
timestamp, sanitize(run_id), refs_total, refs_unresolved
)
}
LogEvent::Expand { run_id, packet_type } => {
format!("[{}] EXPAND {} type={}", timestamp, sanitize(run_id), sanitize(packet_type))
}
LogEvent::Run { run_id, status } => {
format!("[{}] RUN {} status={}", timestamp, sanitize(run_id), sanitize(status))
}
};
if line.len() > MAX_LOG_LINE {
line[..MAX_LOG_LINE].to_string()
} else {
line
}
}