Skip to main content

oxirs_core/audit/
logger.rs

1//! Audit logger sinks: in-memory, NDJSON file, and composite fan-out.
2
3use std::io::Write;
4use std::sync::{Arc, Mutex, RwLock};
5
6use super::event::AuditEvent;
7
8/// Error type for audit logging operations.
9#[derive(Debug, thiserror::Error)]
10pub enum AuditLogError {
11    /// The in-memory logger has reached its maximum capacity.
12    #[error("logger capacity exceeded ({0} events)")]
13    CapacityExceeded(usize),
14    /// An underlying I/O error occurred (e.g., disk write failure).
15    #[error("IO error: {0}")]
16    Io(#[from] std::io::Error),
17    /// JSON serialization of the event failed.
18    #[error("serialization error: {0}")]
19    Serialization(String),
20}
21
22/// Trait for audit event sinks.
23///
24/// Implementors are required to be `Send + Sync` so they can be used behind
25/// `Arc` in multi-threaded services.
26pub trait AuditLogger: Send + Sync {
27    /// Record a single audit event.
28    ///
29    /// Implementors must not panic; all errors must be returned as
30    /// [`AuditLogError`] variants.
31    fn log(&self, event: AuditEvent) -> Result<(), AuditLogError>;
32
33    /// Flush any buffered events to the underlying sink.
34    ///
35    /// The default implementation is a no-op, suitable for loggers that
36    /// write synchronously.
37    fn flush(&self) -> Result<(), AuditLogError> {
38        Ok(())
39    }
40}
41
42// ─────────────────────────────────────────────
43// InMemoryAuditLogger
44// ─────────────────────────────────────────────
45
46/// In-memory audit logger, backed by a `Vec<AuditEvent>`.
47///
48/// Suitable for tests and short-lived processes. When capacity is exceeded,
49/// [`log`](AuditLogger::log) returns [`AuditLogError::CapacityExceeded`] rather
50/// than evicting events — preserving audit trail completeness.
51pub struct InMemoryAuditLogger {
52    events: Arc<RwLock<Vec<AuditEvent>>>,
53    max_capacity: usize,
54}
55
56impl Default for InMemoryAuditLogger {
57    fn default() -> Self {
58        Self::new()
59    }
60}
61
62impl InMemoryAuditLogger {
63    /// Create a new logger with the default capacity of 10,000 events.
64    pub fn new() -> Self {
65        Self::with_capacity(10_000)
66    }
67
68    /// Create a new logger with a custom maximum capacity.
69    pub fn with_capacity(max_capacity: usize) -> Self {
70        Self {
71            events: Arc::new(RwLock::new(Vec::new())),
72            max_capacity,
73        }
74    }
75
76    /// Return a snapshot of all logged events.
77    ///
78    /// Acquires a read lock; panics (poison recovery) if the lock is poisoned.
79    pub fn events(&self) -> Vec<AuditEvent> {
80        self.events
81            .read()
82            .unwrap_or_else(|p| p.into_inner())
83            .clone()
84    }
85
86    /// Return the number of events currently stored.
87    pub fn len(&self) -> usize {
88        self.events.read().unwrap_or_else(|p| p.into_inner()).len()
89    }
90
91    /// Return `true` if no events are stored.
92    pub fn is_empty(&self) -> bool {
93        self.len() == 0
94    }
95
96    /// Remove all stored events.
97    pub fn clear(&self) {
98        self.events
99            .write()
100            .unwrap_or_else(|p| p.into_inner())
101            .clear();
102    }
103}
104
105impl AuditLogger for InMemoryAuditLogger {
106    fn log(&self, event: AuditEvent) -> Result<(), AuditLogError> {
107        let mut guard = self.events.write().unwrap_or_else(|p| p.into_inner());
108        if guard.len() >= self.max_capacity {
109            return Err(AuditLogError::CapacityExceeded(self.max_capacity));
110        }
111        guard.push(event);
112        Ok(())
113    }
114}
115
116// ─────────────────────────────────────────────
117// JsonLineAuditLogger
118// ─────────────────────────────────────────────
119
120/// NDJSON (newline-delimited JSON) audit logger.
121///
122/// Writes one compact JSON object per line to an underlying `Write` sink.
123/// Thread-safe: concurrent calls to [`log`](AuditLogger::log) are serialised
124/// via an internal `Mutex`. Suitable for log aggregators that tail files
125/// (Fluentd, Filebeat, etc.).
126pub struct JsonLineAuditLogger {
127    writer: Arc<Mutex<Box<dyn Write + Send>>>,
128}
129
130impl JsonLineAuditLogger {
131    /// Create a logger that writes to any `Write` sink.
132    pub fn new(writer: impl Write + Send + 'static) -> Self {
133        Self {
134            writer: Arc::new(Mutex::new(Box::new(writer))),
135        }
136    }
137
138    /// Create a logger that appends to a file at the given path.
139    ///
140    /// The file is created if it does not exist. Existing content is preserved
141    /// (append semantics) to maintain audit trail integrity.
142    pub fn to_file(path: &std::path::Path) -> Result<Self, AuditLogError> {
143        let file = std::fs::OpenOptions::new()
144            .create(true)
145            .append(true)
146            .open(path)?;
147        Ok(Self::new(file))
148    }
149
150    /// Create a logger that writes to standard error.
151    pub fn to_stderr() -> Self {
152        Self::new(std::io::stderr())
153    }
154}
155
156impl AuditLogger for JsonLineAuditLogger {
157    fn log(&self, event: AuditEvent) -> Result<(), AuditLogError> {
158        let line = serde_json::to_string(&event)
159            .map_err(|e| AuditLogError::Serialization(e.to_string()))?;
160        let mut guard = self.writer.lock().unwrap_or_else(|p| p.into_inner());
161        guard.write_all(line.as_bytes())?;
162        guard.write_all(b"\n")?;
163        Ok(())
164    }
165
166    fn flush(&self) -> Result<(), AuditLogError> {
167        let mut guard = self.writer.lock().unwrap_or_else(|p| p.into_inner());
168        guard.flush()?;
169        Ok(())
170    }
171}
172
173// ─────────────────────────────────────────────
174// CompositeAuditLogger
175// ─────────────────────────────────────────────
176
177/// Fan-out audit logger that dispatches every event to multiple child loggers.
178///
179/// All loggers receive every event. If one or more loggers return errors, the
180/// composite logger still delivers to all remaining loggers before returning
181/// the first error encountered. This ensures maximum coverage even in partial
182/// failure scenarios.
183pub struct CompositeAuditLogger {
184    loggers: Vec<Arc<dyn AuditLogger>>,
185}
186
187impl CompositeAuditLogger {
188    /// Create a composite logger from a list of child logger arcs.
189    pub fn new(loggers: Vec<Arc<dyn AuditLogger>>) -> Self {
190        Self { loggers }
191    }
192}
193
194impl AuditLogger for CompositeAuditLogger {
195    fn log(&self, event: AuditEvent) -> Result<(), AuditLogError> {
196        // Fan out to all loggers, collecting the first error while still
197        // delivering to all remaining loggers (fail-partial semantics).
198        let mut first_err: Option<AuditLogError> = None;
199        for logger in &self.loggers {
200            if let Err(e) = logger.log(event.clone()) {
201                if first_err.is_none() {
202                    first_err = Some(e);
203                }
204            }
205        }
206        match first_err {
207            Some(e) => Err(e),
208            None => Ok(()),
209        }
210    }
211
212    fn flush(&self) -> Result<(), AuditLogError> {
213        let mut first_err: Option<AuditLogError> = None;
214        for logger in &self.loggers {
215            if let Err(e) = logger.flush() {
216                if first_err.is_none() {
217                    first_err = Some(e);
218                }
219            }
220        }
221        match first_err {
222            Some(e) => Err(e),
223            None => Ok(()),
224        }
225    }
226}