1use std::fs::{File, OpenOptions};
9use std::io::Write;
10use std::sync::Mutex;
11use std::time::Instant;
12
13use chrono::Local;
14use fs2::FileExt;
15use serde::Serialize;
16
17use crate::IdbError;
18
19#[derive(Serialize)]
21#[serde(tag = "event")]
22pub enum AuditEvent {
23 #[serde(rename = "session_start")]
25 SessionStart {
26 timestamp: String,
27 args: Vec<String>,
28 version: String,
29 },
30
31 #[serde(rename = "page_write")]
33 PageWrite {
34 timestamp: String,
35 file: String,
36 page_number: u64,
37 operation: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 old_checksum: Option<u32>,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 new_checksum: Option<u32>,
42 },
43
44 #[serde(rename = "file_write")]
46 FileWrite {
47 timestamp: String,
48 file: String,
49 operation: String,
50 pages_written: u64,
51 },
52
53 #[serde(rename = "backup_created")]
55 BackupCreated {
56 timestamp: String,
57 source: String,
58 backup_path: String,
59 },
60
61 #[serde(rename = "session_end")]
63 SessionEnd {
64 timestamp: String,
65 duration_ms: u64,
66 pages_written: u64,
67 files_written: u64,
68 },
69}
70
71struct AuditLoggerInner {
72 file: File,
73 pages_written: u64,
74 files_written: u64,
75}
76
77pub struct AuditLogger {
82 inner: Mutex<AuditLoggerInner>,
83 start: Instant,
84}
85
86impl AuditLogger {
87 pub fn open(path: &str) -> Result<Self, IdbError> {
89 let file = OpenOptions::new()
90 .create(true)
91 .append(true)
92 .open(path)
93 .map_err(|e| IdbError::Io(format!("Cannot open audit log {}: {}", path, e)))?;
94
95 Ok(Self {
96 inner: Mutex::new(AuditLoggerInner {
97 file,
98 pages_written: 0,
99 files_written: 0,
100 }),
101 start: Instant::now(),
102 })
103 }
104
105 pub fn emit(&self, event: &AuditEvent) -> Result<(), IdbError> {
107 let line = serde_json::to_string(event)
108 .map_err(|e| IdbError::Parse(format!("Audit JSON error: {}", e)))?;
109
110 let mut inner = self.inner.lock().unwrap();
111 inner
112 .file
113 .lock_exclusive()
114 .map_err(|e| IdbError::Io(format!("Audit log lock error: {}", e)))?;
115 writeln!(inner.file, "{}", line)
116 .map_err(|e| IdbError::Io(format!("Audit log write error: {}", e)))?;
117 inner
118 .file
119 .flush()
120 .map_err(|e| IdbError::Io(format!("Audit log flush error: {}", e)))?;
121 inner
122 .file
123 .unlock()
124 .map_err(|e| IdbError::Io(format!("Audit log unlock error: {}", e)))?;
125
126 Ok(())
127 }
128
129 pub fn start_session(&self, args: Vec<String>) -> Result<(), IdbError> {
131 self.emit(&AuditEvent::SessionStart {
132 timestamp: now(),
133 args,
134 version: env!("CARGO_PKG_VERSION").to_string(),
135 })
136 }
137
138 pub fn end_session(&self) -> Result<(), IdbError> {
140 let inner = self.inner.lock().unwrap();
141 let duration_ms = self.start.elapsed().as_millis() as u64;
142 let event = AuditEvent::SessionEnd {
143 timestamp: now(),
144 duration_ms,
145 pages_written: inner.pages_written,
146 files_written: inner.files_written,
147 };
148 drop(inner);
149 self.emit(&event)
150 }
151
152 pub fn log_page_write(
154 &self,
155 file: &str,
156 page_number: u64,
157 operation: &str,
158 old_checksum: Option<u32>,
159 new_checksum: Option<u32>,
160 ) -> Result<(), IdbError> {
161 self.emit(&AuditEvent::PageWrite {
162 timestamp: now(),
163 file: file.to_string(),
164 page_number,
165 operation: operation.to_string(),
166 old_checksum,
167 new_checksum,
168 })?;
169 self.inner.lock().unwrap().pages_written += 1;
170 Ok(())
171 }
172
173 pub fn log_file_write(
175 &self,
176 file: &str,
177 operation: &str,
178 pages_written: u64,
179 ) -> Result<(), IdbError> {
180 self.emit(&AuditEvent::FileWrite {
181 timestamp: now(),
182 file: file.to_string(),
183 operation: operation.to_string(),
184 pages_written,
185 })?;
186 self.inner.lock().unwrap().files_written += 1;
187 Ok(())
188 }
189
190 pub fn log_backup(&self, source: &str, backup_path: &str) -> Result<(), IdbError> {
192 self.emit(&AuditEvent::BackupCreated {
193 timestamp: now(),
194 source: source.to_string(),
195 backup_path: backup_path.to_string(),
196 })
197 }
198}
199
200fn now() -> String {
201 Local::now().to_rfc3339()
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207 use std::io::{BufRead, BufReader};
208 use tempfile::NamedTempFile;
209
210 fn temp_logger() -> (AuditLogger, String) {
211 let tmp = NamedTempFile::new().unwrap();
212 let path = tmp.path().to_str().unwrap().to_string();
213 drop(tmp);
215 let logger = AuditLogger::open(&path).unwrap();
216 (logger, path)
217 }
218
219 #[test]
220 fn test_writes_ndjson_lines() {
221 let (logger, path) = temp_logger();
222 logger
223 .start_session(vec!["inno".into(), "repair".into()])
224 .unwrap();
225 logger
226 .log_page_write("test.ibd", 1, "repair", Some(0xDEAD), Some(0xBEEF))
227 .unwrap();
228 logger.end_session().unwrap();
229
230 let file = File::open(&path).unwrap();
231 let lines: Vec<String> = BufReader::new(file).lines().map(|l| l.unwrap()).collect();
232 assert_eq!(lines.len(), 3);
233
234 let v0: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
236 assert_eq!(v0["event"], "session_start");
237
238 let v1: serde_json::Value = serde_json::from_str(&lines[1]).unwrap();
239 assert_eq!(v1["event"], "page_write");
240 assert_eq!(v1["page_number"], 1);
241
242 let v2: serde_json::Value = serde_json::from_str(&lines[2]).unwrap();
243 assert_eq!(v2["event"], "session_end");
244 assert_eq!(v2["pages_written"], 1);
245 assert_eq!(v2["files_written"], 0);
246 }
247
248 #[test]
249 fn test_append_mode() {
250 let tmp = NamedTempFile::new().unwrap();
251 let path = tmp.path().to_str().unwrap().to_string();
252 drop(tmp);
253
254 {
256 let logger = AuditLogger::open(&path).unwrap();
257 logger.start_session(vec!["session1".into()]).unwrap();
258 }
259
260 {
262 let logger = AuditLogger::open(&path).unwrap();
263 logger.start_session(vec!["session2".into()]).unwrap();
264 }
265
266 let file = File::open(&path).unwrap();
267 let lines: Vec<String> = BufReader::new(file).lines().map(|l| l.unwrap()).collect();
268 assert_eq!(lines.len(), 2);
269 }
270
271 #[test]
272 fn test_session_counters() {
273 let (logger, path) = temp_logger();
274 logger.start_session(vec![]).unwrap();
275 logger
276 .log_page_write("a.ibd", 0, "repair", None, None)
277 .unwrap();
278 logger
279 .log_page_write("a.ibd", 1, "repair", None, None)
280 .unwrap();
281 logger.log_file_write("out.ibd", "defrag", 10).unwrap();
282 logger.end_session().unwrap();
283
284 let file = File::open(&path).unwrap();
285 let lines: Vec<String> = BufReader::new(file).lines().map(|l| l.unwrap()).collect();
286 let last: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap();
287 assert_eq!(last["pages_written"], 2);
288 assert_eq!(last["files_written"], 1);
289 }
290
291 #[test]
292 fn test_thread_safety() {
293 use std::sync::Arc;
294 use std::thread;
295
296 let (logger, path) = temp_logger();
297 let logger = Arc::new(logger);
298
299 let mut handles = Vec::new();
300 for i in 0..10 {
301 let lg = Arc::clone(&logger);
302 handles.push(thread::spawn(move || {
303 lg.log_page_write("test.ibd", i, "repair", None, None)
304 .unwrap();
305 }));
306 }
307
308 for h in handles {
309 h.join().unwrap();
310 }
311
312 let file = File::open(&path).unwrap();
313 let lines: Vec<String> = BufReader::new(file).lines().map(|l| l.unwrap()).collect();
314 assert_eq!(lines.len(), 10);
315
316 for line in &lines {
318 let _: serde_json::Value = serde_json::from_str(line).unwrap();
319 }
320 }
321
322 #[test]
323 fn test_backup_event() {
324 let (logger, path) = temp_logger();
325 logger.log_backup("test.ibd", "test.ibd.bak").unwrap();
326
327 let file = File::open(&path).unwrap();
328 let lines: Vec<String> = BufReader::new(file).lines().map(|l| l.unwrap()).collect();
329 assert_eq!(lines.len(), 1);
330
331 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
332 assert_eq!(v["event"], "backup_created");
333 assert_eq!(v["source"], "test.ibd");
334 }
335
336 #[test]
337 fn test_file_write_event() {
338 let (logger, path) = temp_logger();
339 logger.log_file_write("output.ibd", "defrag", 42).unwrap();
340
341 let file = File::open(&path).unwrap();
342 let lines: Vec<String> = BufReader::new(file).lines().map(|l| l.unwrap()).collect();
343 assert_eq!(lines.len(), 1);
344
345 let v: serde_json::Value = serde_json::from_str(&lines[0]).unwrap();
346 assert_eq!(v["event"], "file_write");
347 assert_eq!(v["pages_written"], 42);
348 }
349}