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}