1use serde::{Deserialize, Serialize};
7use std::fs::{File, OpenOptions};
8use std::io::{BufWriter, Write};
9use std::path::PathBuf;
10use std::sync::{Arc, Mutex};
11use std::time::{SystemTime, UNIX_EPOCH};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "event_type")]
16pub enum AuditEvent {
17 CompileStart {
19 source_hash: String,
20 user: Option<String>,
21 },
22 CompileComplete {
24 source_hash: String,
25 node_count: usize,
26 duration_ms: u64,
27 cache_hit: bool,
28 },
29 CompileFailed {
31 source_hash: String,
32 error: String,
33 },
34 BacktestStart {
36 signal_hash: String,
37 start_date: String,
38 end_date: String,
39 parameters: std::collections::HashMap<String, f64>,
40 },
41 BacktestComplete {
43 signal_hash: String,
44 total_return: f64,
45 sharpe_ratio: f64,
46 max_drawdown: f64,
47 duration_ms: u64,
48 },
49 BacktestFailed {
51 signal_hash: String,
52 error: String,
53 },
54 DataLoaded {
56 source: String,
57 rows: usize,
58 columns: usize,
59 },
60 CacheOperation {
62 operation: String,
63 key: String,
64 hit: bool,
65 },
66 Optimization {
68 signal_hash: String,
69 parameter_count: usize,
70 best_sharpe: f64,
71 combinations_tested: usize,
72 },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct AuditEntry {
78 pub timestamp: u64,
79 pub session_id: String,
80 pub event: AuditEvent,
81}
82
83pub struct AuditLogger {
85 session_id: String,
86 writer: Option<Arc<Mutex<BufWriter<File>>>>,
87 enabled: bool,
88}
89
90impl AuditLogger {
91 pub fn new() -> Self {
93 let session_id = format!("{:x}", SystemTime::now()
94 .duration_since(UNIX_EPOCH)
95 .map(|d| d.as_nanos())
96 .unwrap_or(0));
97
98 AuditLogger {
99 session_id,
100 writer: None,
101 enabled: false,
102 }
103 }
104
105 pub fn with_file(path: PathBuf) -> std::io::Result<Self> {
107 let file = OpenOptions::new()
108 .create(true)
109 .append(true)
110 .open(&path)?;
111
112 let session_id = format!("{:x}", SystemTime::now()
113 .duration_since(UNIX_EPOCH)
114 .map(|d| d.as_nanos())
115 .unwrap_or(0));
116
117 Ok(AuditLogger {
118 session_id,
119 writer: Some(Arc::new(Mutex::new(BufWriter::new(file)))),
120 enabled: true,
121 })
122 }
123
124 pub fn set_enabled(&mut self, enabled: bool) {
126 self.enabled = enabled;
127 }
128
129 pub fn log(&self, event: AuditEvent) {
131 if !self.enabled {
132 return;
133 }
134
135 let entry = AuditEntry {
136 timestamp: SystemTime::now()
137 .duration_since(UNIX_EPOCH)
138 .map(|d| d.as_secs())
139 .unwrap_or(0),
140 session_id: self.session_id.clone(),
141 event: event.clone(),
142 };
143
144 match &event {
146 AuditEvent::CompileStart { source_hash, .. } => {
147 tracing::info!(target: "audit", event = "compile_start", source_hash = %source_hash);
148 }
149 AuditEvent::CompileComplete { source_hash, node_count, duration_ms, cache_hit } => {
150 tracing::info!(target: "audit", event = "compile_complete",
151 source_hash = %source_hash, nodes = node_count,
152 duration_ms = duration_ms, cache_hit = cache_hit);
153 }
154 AuditEvent::CompileFailed { source_hash, error } => {
155 tracing::error!(target: "audit", event = "compile_failed",
156 source_hash = %source_hash, error = %error);
157 }
158 AuditEvent::BacktestStart { signal_hash, start_date, end_date, .. } => {
159 tracing::info!(target: "audit", event = "backtest_start",
160 signal_hash = %signal_hash, start = %start_date, end = %end_date);
161 }
162 AuditEvent::BacktestComplete { signal_hash, total_return, sharpe_ratio, .. } => {
163 tracing::info!(target: "audit", event = "backtest_complete",
164 signal_hash = %signal_hash,
165 return_pct = total_return * 100.0,
166 sharpe = sharpe_ratio);
167 }
168 AuditEvent::BacktestFailed { signal_hash, error } => {
169 tracing::error!(target: "audit", event = "backtest_failed",
170 signal_hash = %signal_hash, error = %error);
171 }
172 AuditEvent::DataLoaded { source, rows, columns } => {
173 tracing::debug!(target: "audit", event = "data_loaded",
174 source = %source, rows = rows, columns = columns);
175 }
176 AuditEvent::CacheOperation { operation, key, hit } => {
177 tracing::debug!(target: "audit", event = "cache_op",
178 op = %operation, key = %key, hit = hit);
179 }
180 AuditEvent::Optimization { signal_hash, combinations_tested, best_sharpe, .. } => {
181 tracing::info!(target: "audit", event = "optimization",
182 signal_hash = %signal_hash,
183 combinations = combinations_tested,
184 best_sharpe = best_sharpe);
185 }
186 }
187
188 if let Some(ref writer) = self.writer {
190 if let Ok(mut w) = writer.lock() {
191 if let Ok(json) = serde_json::to_string(&entry) {
192 let _ = writeln!(w, "{}", json);
193 let _ = w.flush();
194 }
195 }
196 }
197 }
198
199 pub fn session_id(&self) -> &str {
201 &self.session_id
202 }
203}
204
205impl Default for AuditLogger {
206 fn default() -> Self {
207 Self::new()
208 }
209}
210
211static AUDIT_LOGGER: std::sync::OnceLock<Mutex<AuditLogger>> = std::sync::OnceLock::new();
213
214pub fn init_audit_logger(path: Option<PathBuf>) -> std::io::Result<()> {
216 let logger = if let Some(p) = path {
217 AuditLogger::with_file(p)?
218 } else {
219 let mut l = AuditLogger::new();
220 l.set_enabled(true);
221 l
222 };
223
224 let _ = AUDIT_LOGGER.set(Mutex::new(logger));
225 Ok(())
226}
227
228pub fn audit_log(event: AuditEvent) {
230 if let Some(logger) = AUDIT_LOGGER.get() {
231 if let Ok(l) = logger.lock() {
232 l.log(event);
233 }
234 }
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240 use std::collections::HashMap;
241
242 #[test]
243 fn test_audit_logger_creation() {
244 let logger = AuditLogger::new();
245 assert!(!logger.session_id.is_empty());
246 }
247
248 #[test]
249 fn test_audit_event_serialization() {
250 let event = AuditEvent::BacktestComplete {
251 signal_hash: "abc123".to_string(),
252 total_return: 0.15,
253 sharpe_ratio: 1.5,
254 max_drawdown: 0.10,
255 duration_ms: 1000,
256 };
257
258 let json = serde_json::to_string(&event).unwrap();
259 assert!(json.contains("BacktestComplete"));
260 assert!(json.contains("1.5"));
261 }
262
263 #[test]
264 fn test_audit_entry_serialization() {
265 let entry = AuditEntry {
266 timestamp: 1234567890,
267 session_id: "test123".to_string(),
268 event: AuditEvent::CompileStart {
269 source_hash: "hash123".to_string(),
270 user: Some("testuser".to_string()),
271 },
272 };
273
274 let json = serde_json::to_string(&entry).unwrap();
275 assert!(json.contains("1234567890"));
276 assert!(json.contains("test123"));
277 }
278
279 #[test]
280 fn test_disabled_logger() {
281 let logger = AuditLogger::new();
282 logger.log(AuditEvent::DataLoaded {
284 source: "test.csv".to_string(),
285 rows: 100,
286 columns: 5,
287 });
288 }
289}