- Remove gitlinks (160000 mode) for chainfire, flaredb, iam - Add workspace contents as regular tracked files - Update flake.nix to use simple paths instead of builtins.fetchGit This resolves the nix build failure where submodule directories appeared empty in the nix store. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
281 lines
7.5 KiB
Rust
281 lines
7.5 KiB
Rust
//! Audit logger
|
|
//!
|
|
//! Main entry point for audit logging.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use tokio::sync::mpsc;
|
|
use tracing::{debug, error, warn};
|
|
|
|
use crate::event::AuditEvent;
|
|
use crate::sink::{AuditSink, Result};
|
|
|
|
/// Configuration for the audit logger
|
|
#[derive(Debug, Clone)]
|
|
pub struct AuditLoggerConfig {
|
|
/// Buffer size for async logging
|
|
pub buffer_size: usize,
|
|
|
|
/// Whether to block when buffer is full
|
|
pub block_on_full: bool,
|
|
|
|
/// Whether to log errors to tracing
|
|
pub log_errors: bool,
|
|
}
|
|
|
|
impl Default for AuditLoggerConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
buffer_size: 1000,
|
|
block_on_full: false,
|
|
log_errors: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Audit logger
|
|
///
|
|
/// Handles audit event logging with async processing.
|
|
pub struct AuditLogger {
|
|
sink: Arc<dyn AuditSink>,
|
|
config: AuditLoggerConfig,
|
|
}
|
|
|
|
impl AuditLogger {
|
|
/// Create a new audit logger with a sink
|
|
pub fn new(sink: impl AuditSink + 'static) -> Self {
|
|
Self {
|
|
sink: Arc::new(sink),
|
|
config: AuditLoggerConfig::default(),
|
|
}
|
|
}
|
|
|
|
/// Create with configuration
|
|
pub fn with_config(sink: impl AuditSink + 'static, config: AuditLoggerConfig) -> Self {
|
|
Self {
|
|
sink: Arc::new(sink),
|
|
config,
|
|
}
|
|
}
|
|
|
|
/// Create from an Arc'd sink
|
|
pub fn from_arc(sink: Arc<dyn AuditSink>) -> Self {
|
|
Self {
|
|
sink,
|
|
config: AuditLoggerConfig::default(),
|
|
}
|
|
}
|
|
|
|
/// Log an audit event
|
|
pub async fn log(&self, event: AuditEvent) -> Result<()> {
|
|
if let Err(e) = self.sink.write(&event).await {
|
|
if self.config.log_errors {
|
|
error!(
|
|
event_id = %event.id,
|
|
error = %e,
|
|
"Failed to write audit event"
|
|
);
|
|
}
|
|
return Err(e);
|
|
}
|
|
|
|
debug!(
|
|
event_id = %event.id,
|
|
kind = ?std::mem::discriminant(&event.kind),
|
|
"Audit event logged"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Log an event without waiting for completion
|
|
///
|
|
/// Returns immediately. Errors are logged via tracing.
|
|
pub fn log_async(&self, event: AuditEvent) {
|
|
let sink = self.sink.clone();
|
|
let log_errors = self.config.log_errors;
|
|
|
|
tokio::spawn(async move {
|
|
if let Err(e) = sink.write(&event).await {
|
|
if log_errors {
|
|
error!(
|
|
event_id = %event.id,
|
|
error = %e,
|
|
"Failed to write audit event (async)"
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/// Flush the sink
|
|
pub async fn flush(&self) -> Result<()> {
|
|
self.sink.flush().await
|
|
}
|
|
|
|
/// Close the logger
|
|
pub async fn close(&self) -> Result<()> {
|
|
self.sink.close().await
|
|
}
|
|
|
|
/// Get the underlying sink
|
|
pub fn sink(&self) -> &Arc<dyn AuditSink> {
|
|
&self.sink
|
|
}
|
|
}
|
|
|
|
/// Buffered audit logger with async processing
|
|
///
|
|
/// Uses a channel to buffer events and process them in the background.
|
|
pub struct BufferedAuditLogger {
|
|
tx: mpsc::Sender<AuditEvent>,
|
|
config: AuditLoggerConfig,
|
|
}
|
|
|
|
impl BufferedAuditLogger {
|
|
/// Create a new buffered logger
|
|
///
|
|
/// Spawns a background task to process events.
|
|
pub fn new(sink: impl AuditSink + 'static, config: AuditLoggerConfig) -> Self {
|
|
let (tx, rx) = mpsc::channel(config.buffer_size);
|
|
let sink = Arc::new(sink);
|
|
|
|
// Spawn background processor
|
|
tokio::spawn(Self::process_events(rx, sink, config.log_errors));
|
|
|
|
Self { tx, config }
|
|
}
|
|
|
|
/// Create with default config
|
|
pub fn with_defaults(sink: impl AuditSink + 'static) -> Self {
|
|
Self::new(sink, AuditLoggerConfig::default())
|
|
}
|
|
|
|
/// Log an audit event
|
|
///
|
|
/// If buffer is full, behavior depends on config.block_on_full
|
|
pub async fn log(&self, event: AuditEvent) -> bool {
|
|
if self.config.block_on_full {
|
|
self.tx.send(event).await.is_ok()
|
|
} else {
|
|
match self.tx.try_send(event) {
|
|
Ok(()) => true,
|
|
Err(mpsc::error::TrySendError::Full(event)) => {
|
|
if self.config.log_errors {
|
|
warn!(
|
|
event_id = %event.id,
|
|
"Audit buffer full, event dropped"
|
|
);
|
|
}
|
|
false
|
|
}
|
|
Err(mpsc::error::TrySendError::Closed(_)) => {
|
|
error!("Audit logger channel closed");
|
|
false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Process events from the channel
|
|
async fn process_events(
|
|
mut rx: mpsc::Receiver<AuditEvent>,
|
|
sink: Arc<dyn AuditSink>,
|
|
log_errors: bool,
|
|
) {
|
|
while let Some(event) = rx.recv().await {
|
|
if let Err(e) = sink.write(&event).await {
|
|
if log_errors {
|
|
error!(
|
|
event_id = %event.id,
|
|
error = %e,
|
|
"Failed to write audit event"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Channel closed, flush and close sink
|
|
if let Err(e) = sink.flush().await {
|
|
if log_errors {
|
|
error!(error = %e, "Failed to flush audit sink on close");
|
|
}
|
|
}
|
|
|
|
if let Err(e) = sink.close().await {
|
|
if log_errors {
|
|
error!(error = %e, "Failed to close audit sink");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::sink::MemorySink;
|
|
|
|
#[tokio::test]
|
|
async fn test_audit_logger() {
|
|
let sink = Arc::new(MemorySink::new(100));
|
|
let logger = AuditLogger::from_arc(sink.clone());
|
|
|
|
let event = AuditEvent::authn_success("alice", "jwt");
|
|
logger.log(event).await.unwrap();
|
|
|
|
assert_eq!(sink.count().await, 1);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_audit_logger_async() {
|
|
let sink = Arc::new(MemorySink::new(100));
|
|
let logger = AuditLogger::from_arc(sink.clone());
|
|
|
|
for i in 0..10 {
|
|
let event = AuditEvent::authn_success(&format!("user-{}", i), "jwt");
|
|
logger.log_async(event);
|
|
}
|
|
|
|
// Give async tasks time to complete
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
assert_eq!(sink.count().await, 10);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_buffered_logger() {
|
|
let sink = Arc::new(MemorySink::new(100));
|
|
let logger = BufferedAuditLogger::new(
|
|
MemorySinkWrapper(sink.clone()),
|
|
AuditLoggerConfig::default(),
|
|
);
|
|
|
|
for i in 0..10 {
|
|
let event = AuditEvent::authn_success(&format!("user-{}", i), "jwt");
|
|
logger.log(event).await;
|
|
}
|
|
|
|
// Give background task time to process
|
|
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
|
|
|
assert_eq!(sink.count().await, 10);
|
|
}
|
|
|
|
// Wrapper to allow Arc<MemorySink> to be used as AuditSink
|
|
struct MemorySinkWrapper(Arc<MemorySink>);
|
|
|
|
#[async_trait::async_trait]
|
|
impl AuditSink for MemorySinkWrapper {
|
|
async fn write(&self, event: &AuditEvent) -> Result<()> {
|
|
self.0.write(event).await
|
|
}
|
|
|
|
async fn flush(&self) -> Result<()> {
|
|
self.0.flush().await
|
|
}
|
|
|
|
async fn close(&self) -> Result<()> {
|
|
self.0.close().await
|
|
}
|
|
}
|
|
}
|