use echo_core::audit::{AuditEvent, AuditFilter, AuditLogger};
use echo_core::error::Result;
use futures::future::BoxFuture;
use std::io::Write;
use std::path::PathBuf;
use std::sync::Mutex;
pub struct FileAuditLogger {
path: PathBuf,
writer: Mutex<Option<std::io::BufWriter<std::fs::File>>>,
}
impl FileAuditLogger {
pub fn new(path: impl Into<PathBuf>) -> Result<Self> {
let path = path.into();
if let Some(parent) = path.parent() {
let parent = parent.to_path_buf();
tokio::task::block_in_place(|| std::fs::create_dir_all(&parent))?;
}
let file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
Ok(Self {
path,
writer: Mutex::new(Some(std::io::BufWriter::new(file))),
})
}
}
impl AuditLogger for FileAuditLogger {
fn log<'a>(&'a self, event: AuditEvent) -> BoxFuture<'a, Result<()>> {
Box::pin(async move {
let line = serde_json::to_string(&event)
.map_err(|e| echo_core::error::ReactError::Other(e.to_string()))?;
if let Ok(mut guard) = self.writer.lock()
&& let Some(writer) = guard.as_mut()
{
writeln!(writer, "{}", line)?;
writer.flush()?;
}
Ok(())
})
}
fn query<'a>(&'a self, filter: AuditFilter) -> BoxFuture<'a, Result<Vec<AuditEvent>>> {
Box::pin(async move {
let content = std::fs::read_to_string(&self.path).unwrap_or_default();
let mut events: Vec<AuditEvent> = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
if let Ok(event) = serde_json::from_str::<AuditEvent>(line) {
let mut keep = true;
if let Some(ref sid) = filter.session_id
&& event.session_id.as_deref() != Some(sid)
{
keep = false;
}
if let Some(ref name) = filter.agent_name
&& &event.agent_name != name
{
keep = false;
}
if let Some(ref from) = filter.from
&& event.timestamp < *from
{
keep = false;
}
if let Some(ref to) = filter.to
&& event.timestamp > *to
{
keep = false;
}
if keep {
events.push(event);
}
}
}
if let Some(limit) = filter.limit {
events.truncate(limit);
}
Ok(events)
})
}
}