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 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}