Skip to main content

pidgin_lang/
logging.rs

1use std::fs::OpenOptions;
2use std::io::Write;
3use std::path::Path;
4
5const MAX_LOG_LINE: usize = 4096;
6
7fn sanitize(s: &str) -> String {
8    s.chars()
9        .filter(|&c| c.is_ascii_graphic() || c == ' ' || c == '\t')
10        .take(256)
11        .collect()
12}
13
14#[derive(Debug)]
15pub enum LogEvent {
16    Parse { run_id: String, ok: bool },
17    Validate { run_id: String, ok: bool },
18    SafetyGate { run_id: String, blocked: bool, rules: String },
19    Resolve { run_id: String, refs_total: usize, refs_unresolved: usize },
20    Expand { run_id: String, packet_type: String },
21    Run { run_id: String, status: String },
22}
23
24pub fn log_event(path: &Path, event: &LogEvent) -> std::io::Result<()> {
25    if let Some(parent) = path.parent() {
26        std::fs::create_dir_all(parent)?;
27    }
28
29    let timestamp = chrono_now();
30    let line = format_line(timestamp, event);
31
32    let mut file = OpenOptions::new()
33        .create(true)
34        .append(true)
35        .open(path)?;
36    writeln!(file, "{}", line)?;
37    Ok(())
38}
39
40fn chrono_now() -> String {
41    // Simple timestamp without chrono dependency
42    use std::time::{SystemTime, UNIX_EPOCH};
43    let dur = SystemTime::now()
44        .duration_since(UNIX_EPOCH)
45        .unwrap_or_default();
46    let secs = dur.as_secs();
47    let millis = dur.subsec_millis();
48    let s = secs % 86400;
49    let h = s / 3600;
50    let m = (s % 3600) / 60;
51    let s = s % 60;
52    format!("{:02}:{:02}:{:02}.{:03}", h, m, s, millis)
53}
54
55fn format_line(timestamp: String, event: &LogEvent) -> String {
56    let line = match event {
57        LogEvent::Parse { run_id, ok } => {
58            format!("[{}] PARSE {} {}", timestamp, if *ok { "OK" } else { "FAIL" }, sanitize(run_id))
59        }
60        LogEvent::Validate { run_id, ok } => {
61            format!("[{}] VALIDATE {} {}", timestamp, if *ok { "OK" } else { "FAIL" }, sanitize(run_id))
62        }
63        LogEvent::SafetyGate { run_id, blocked, rules } => {
64            format!(
65                "[{}] SAFETY {} {} rules=[{}]",
66                timestamp,
67                if *blocked { "BLOCKED" } else { "PASS" },
68                sanitize(run_id),
69                sanitize(rules),
70            )
71        }
72        LogEvent::Resolve { run_id, refs_total, refs_unresolved } => {
73            format!(
74                "[{}] RESOLVE {} refs={} unresolved={}",
75                timestamp, sanitize(run_id), refs_total, refs_unresolved
76            )
77        }
78        LogEvent::Expand { run_id, packet_type } => {
79            format!("[{}] EXPAND {} type={}", timestamp, sanitize(run_id), sanitize(packet_type))
80        }
81        LogEvent::Run { run_id, status } => {
82            format!("[{}] RUN {} status={}", timestamp, sanitize(run_id), sanitize(status))
83        }
84    };
85    if line.len() > MAX_LOG_LINE {
86        line[..MAX_LOG_LINE].to_string()
87    } else {
88        line
89    }
90}