Skip to main content

sig_runtime/
audit.rs

1//! Audit logging for compliance and governance
2//!
3//! Provides structured logging of all operations for regulatory compliance,
4//! model governance, and debugging.
5
6use 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/// Audit event types
14#[derive(Debug, Clone, Serialize, Deserialize)]
15#[serde(tag = "event_type")]
16pub enum AuditEvent {
17    /// Signal compilation started
18    CompileStart {
19        source_hash: String,
20        user: Option<String>,
21    },
22    /// Signal compilation completed
23    CompileComplete {
24        source_hash: String,
25        node_count: usize,
26        duration_ms: u64,
27        cache_hit: bool,
28    },
29    /// Signal compilation failed
30    CompileFailed {
31        source_hash: String,
32        error: String,
33    },
34    /// Backtest started
35    BacktestStart {
36        signal_hash: String,
37        start_date: String,
38        end_date: String,
39        parameters: std::collections::HashMap<String, f64>,
40    },
41    /// Backtest completed
42    BacktestComplete {
43        signal_hash: String,
44        total_return: f64,
45        sharpe_ratio: f64,
46        max_drawdown: f64,
47        duration_ms: u64,
48    },
49    /// Backtest failed
50    BacktestFailed {
51        signal_hash: String,
52        error: String,
53    },
54    /// Data loaded
55    DataLoaded {
56        source: String,
57        rows: usize,
58        columns: usize,
59    },
60    /// Cache operation
61    CacheOperation {
62        operation: String,
63        key: String,
64        hit: bool,
65    },
66    /// Parameter optimization
67    Optimization {
68        signal_hash: String,
69        parameter_count: usize,
70        best_sharpe: f64,
71        combinations_tested: usize,
72    },
73}
74
75/// Audit log entry with metadata
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct AuditEntry {
78    pub timestamp: u64,
79    pub session_id: String,
80    pub event: AuditEvent,
81}
82
83/// Audit logger for tracking operations
84pub struct AuditLogger {
85    session_id: String,
86    writer: Option<Arc<Mutex<BufWriter<File>>>>,
87    enabled: bool,
88}
89
90impl AuditLogger {
91    /// Create a new audit logger
92    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    /// Create a logger that writes to a file
106    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    /// Enable or disable audit logging
125    pub fn set_enabled(&mut self, enabled: bool) {
126        self.enabled = enabled;
127    }
128
129    /// Log an audit event
130    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        // Log to tracing
145        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        // Write to file if configured
189        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    /// Get session ID
200    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
211/// Global audit logger instance
212static AUDIT_LOGGER: std::sync::OnceLock<Mutex<AuditLogger>> = std::sync::OnceLock::new();
213
214/// Initialize the global audit logger
215pub 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
228/// Log an audit event to the global logger
229pub 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        // Should not panic even though logging is disabled
283        logger.log(AuditEvent::DataLoaded {
284            source: "test.csv".to_string(),
285            rows: 100,
286            columns: 5,
287        });
288    }
289}