use crate::models::AuditEvent;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fs::OpenOptions;
use std::io::{self, BufRead, BufReader, Write};
use std::path::{Path, PathBuf};
pub trait AuditSink: Send + Sync {
fn write_event(&self, event: &AuditEvent) -> io::Result<()>;
}
pub trait AuditReader {
fn read_events(&self, query: AuditQuery) -> io::Result<Vec<AuditEvent>>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuditQuery {
pub since: Option<DateTime<Utc>>,
pub limit: usize,
}
impl Default for AuditQuery {
fn default() -> Self {
Self {
since: None,
limit: 100,
}
}
}
#[derive(Debug, Clone)]
pub struct JsonlFileAuditSink {
path: PathBuf,
}
impl JsonlFileAuditSink {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl AuditSink for JsonlFileAuditSink {
fn write_event(&self, event: &AuditEvent) -> io::Result<()> {
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)?;
let line = serde_json::to_string(event).map_err(io::Error::other)?;
writeln!(file, "{line}")
}
}
impl AuditReader for JsonlFileAuditSink {
fn read_events(&self, query: AuditQuery) -> io::Result<Vec<AuditEvent>> {
if !self.path.exists() {
return Ok(Vec::new());
}
let file = OpenOptions::new().read(true).open(&self.path)?;
let reader = BufReader::new(file);
let mut events = Vec::new();
for line in reader.lines() {
let line = line?;
let event: AuditEvent = serde_json::from_str(&line).map_err(io::Error::other)?;
if let Some(since) = query.since
&& event.occurred_at < since
{
continue;
}
events.push(event);
if events.len() >= query.limit {
break;
}
}
Ok(events)
}
}
pub fn write_audit_event(sink: &dyn AuditSink, event: &AuditEvent) -> io::Result<()> {
sink.write_event(event)
}
pub fn read_audit_events(
reader: &dyn AuditReader,
query: AuditQuery,
) -> io::Result<Vec<AuditEvent>> {
reader.read_events(query)
}