use crate::backend::AuditBackend;
use chrono::{Duration, Utc};
use std::sync::Arc;
use tokio::time::interval;
#[derive(Debug, Clone)]
pub struct RetentionPolicy {
pub max_age: Duration,
pub cleanup_interval: std::time::Duration,
}
impl RetentionPolicy {
pub fn new(max_age: Duration) -> Self {
Self {
max_age,
cleanup_interval: std::time::Duration::from_secs(24 * 3600), }
}
pub fn cleanup_interval(mut self, interval: std::time::Duration) -> Self {
self.cleanup_interval = interval;
self
}
pub fn days(days: i64) -> Self {
Self::new(Duration::days(days))
}
pub fn hours(hours: i64) -> Self {
Self::new(Duration::hours(hours))
}
pub fn minutes(minutes: i64) -> Self {
Self::new(Duration::minutes(minutes))
}
}
pub struct RetentionManager {
backend: Arc<dyn AuditBackend>,
policy: RetentionPolicy,
running: Arc<tokio::sync::Mutex<bool>>,
}
impl RetentionManager {
pub fn new(backend: Arc<dyn AuditBackend>, policy: RetentionPolicy) -> Self {
Self {
backend,
policy,
running: Arc::new(tokio::sync::Mutex::new(false)),
}
}
pub async fn cleanup(&self) -> Result<usize, crate::backend::AuditBackendError> {
let cutoff = Utc::now() - self.policy.max_age;
tracing::info!("Running audit log cleanup (cutoff: {})", cutoff);
let deleted = self.backend.delete_before(cutoff).await?;
if deleted > 0 {
tracing::info!("Deleted {} old audit logs", deleted);
}
Ok(deleted)
}
pub async fn start(self: Arc<Self>) {
let mut running = self.running.lock().await;
if *running {
tracing::warn!("Retention manager already running");
return;
}
*running = true;
drop(running);
let manager = self.clone();
tokio::spawn(async move {
let mut ticker = interval(manager.policy.cleanup_interval);
loop {
ticker.tick().await;
let running = manager.running.lock().await;
if !*running {
break;
}
drop(running);
match manager.cleanup().await {
Ok(deleted) => {
if deleted > 0 {
tracing::info!("Retention cleanup: deleted {} logs", deleted);
}
}
Err(e) => {
tracing::error!("Retention cleanup failed: {}", e);
}
}
}
tracing::info!("Retention manager stopped");
});
}
pub async fn stop(&self) {
let mut running = self.running.lock().await;
*running = false;
}
pub async fn is_running(&self) -> bool {
*self.running.lock().await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{AuditEvent, MemoryBackend};
#[test]
fn test_retention_policy_days() {
let policy = RetentionPolicy::days(90);
assert_eq!(policy.max_age.num_days(), 90);
}
#[test]
fn test_retention_policy_hours() {
let policy = RetentionPolicy::hours(24);
assert_eq!(policy.max_age.num_hours(), 24);
}
#[tokio::test]
async fn test_retention_manager_cleanup() {
let backend = Arc::new(MemoryBackend::new());
let backend_clone = backend.clone();
let old_event = AuditEvent::new("old");
backend.write(&old_event).await.unwrap();
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let new_event = AuditEvent::new("new");
backend.write(&new_event).await.unwrap();
let policy = RetentionPolicy::new(Duration::milliseconds(5));
let manager = RetentionManager::new(backend_clone, policy);
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
let deleted = manager.cleanup().await.unwrap();
assert!(deleted > 0);
}
#[tokio::test]
async fn test_retention_manager_start_stop() {
let backend = Arc::new(MemoryBackend::new());
let policy = RetentionPolicy::minutes(1);
let manager = Arc::new(RetentionManager::new(backend, policy));
assert!(!manager.is_running().await);
manager.clone().start().await;
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
assert!(manager.is_running().await);
manager.stop().await;
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
assert!(!manager.is_running().await);
}
}