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 {
17        run_id: String,
18        ok: bool,
19    },
20    Validate {
21        run_id: String,
22        ok: bool,
23    },
24    SafetyGate {
25        run_id: String,
26        blocked: bool,
27        rules: String,
28    },
29    Resolve {
30        run_id: String,
31        refs_total: usize,
32        refs_unresolved: usize,
33    },
34    Expand {
35        run_id: String,
36        packet_type: String,
37    },
38    Run {
39        run_id: String,
40        status: String,
41    },
42}
43
44pub fn log_event(path: &Path, event: &LogEvent) -> std::io::Result<()> {
45    if let Some(parent) = path.parent() {
46        std::fs::create_dir_all(parent)?;
47    }
48
49    let timestamp = chrono_now();
50    let line = format_line(timestamp, event);
51
52    let mut file = OpenOptions::new().create(true).append(true).open(path)?;
53    writeln!(file, "{}", line)?;
54    Ok(())
55}
56
57fn chrono_now() -> String {
58    // Simple timestamp without chrono dependency
59    use std::time::{SystemTime, UNIX_EPOCH};
60    let dur = SystemTime::now()
61        .duration_since(UNIX_EPOCH)
62        .unwrap_or_default();
63    let secs = dur.as_secs();
64    let millis = dur.subsec_millis();
65    let s = secs % 86400;
66    let h = s / 3600;
67    let m = (s % 3600) / 60;
68    let s = s % 60;
69    format!("{:02}:{:02}:{:02}.{:03}", h, m, s, millis)
70}
71
72fn format_line(timestamp: String, event: &LogEvent) -> String {
73    let line = match event {
74        LogEvent::Parse { run_id, ok } => {
75            format!(
76                "[{}] PARSE {} {}",
77                timestamp,
78                if *ok { "OK" } else { "FAIL" },
79                sanitize(run_id)
80            )
81        }
82        LogEvent::Validate { run_id, ok } => {
83            format!(
84                "[{}] VALIDATE {} {}",
85                timestamp,
86                if *ok { "OK" } else { "FAIL" },
87                sanitize(run_id)
88            )
89        }
90        LogEvent::SafetyGate {
91            run_id,
92            blocked,
93            rules,
94        } => {
95            format!(
96                "[{}] SAFETY {} {} rules=[{}]",
97                timestamp,
98                if *blocked { "BLOCKED" } else { "PASS" },
99                sanitize(run_id),
100                sanitize(rules),
101            )
102        }
103        LogEvent::Resolve {
104            run_id,
105            refs_total,
106            refs_unresolved,
107        } => {
108            format!(
109                "[{}] RESOLVE {} refs={} unresolved={}",
110                timestamp,
111                sanitize(run_id),
112                refs_total,
113                refs_unresolved
114            )
115        }
116        LogEvent::Expand {
117            run_id,
118            packet_type,
119        } => {
120            format!(
121                "[{}] EXPAND {} type={}",
122                timestamp,
123                sanitize(run_id),
124                sanitize(packet_type)
125            )
126        }
127        LogEvent::Run { run_id, status } => {
128            format!(
129                "[{}] RUN {} status={}",
130                timestamp,
131                sanitize(run_id),
132                sanitize(status)
133            )
134        }
135    };
136    if line.len() > MAX_LOG_LINE {
137        line[..MAX_LOG_LINE].to_string()
138    } else {
139        line
140    }
141}