photoncloud-monorepo/iam/crates/iam-audit/src/logger.rs
centra 8f94aee1fa Fix R8: Convert submodule gitlinks to regular directories
- 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>
2025-12-09 16:51:20 +09:00

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
}
}
}