use crate::hooks::Hook;
use crate::types::BoundedMeta;
use rustc_hash::FxHashMap;
use std::collections::VecDeque;
use std::sync::Mutex;
use std::time::SystemTime;
use tokio::sync::broadcast;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AgentEvent {
pub agent_id: String,
pub event_type: String,
pub severity: String,
#[serde(default)]
pub timestamp: u64,
#[serde(default)]
pub metadata: BoundedMeta,
}
pub struct EventLog {
ring: Mutex<VecDeque<AgentEvent>>,
tx: broadcast::Sender<AgentEvent>,
max_events: usize,
index: std::sync::atomic::AtomicU64,
}
impl EventLog {
pub fn new(max_events: usize) -> Self {
let (tx, _) = broadcast::channel(2048);
Self {
ring: Mutex::new(VecDeque::new()),
tx,
max_events,
index: std::sync::atomic::AtomicU64::new(0),
}
}
pub fn publish(&self, mut event: AgentEvent) {
if event.timestamp == 0 {
event.timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
}
let mut ring = self.ring.lock().unwrap_or_else(|e| e.into_inner());
if ring.len() >= self.max_events {
ring.pop_front();
}
ring.push_back(event.clone());
self.index
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let _ = self.tx.send(event);
}
pub fn all_since(&self, _since: usize) -> Vec<AgentEvent> {
let ring = self.ring.lock().unwrap_or_else(|e| e.into_inner());
ring.iter().cloned().collect()
}
pub fn subscribe(&self) -> broadcast::Receiver<AgentEvent> {
self.tx.subscribe()
}
pub fn recent(&self, n: usize) -> Vec<AgentEvent> {
let ring = self.ring.lock().unwrap_or_else(|e| e.into_inner());
ring.iter().rev().take(n).cloned().collect()
}
pub fn count(&self) -> usize {
self.ring.lock().unwrap_or_else(|e| e.into_inner()).len()
}
}
fn apply_event_hooks(event: &mut AgentEvent, hooks: &[Hook]) {
for hook in hooks {
let current = event.metadata.get("_hook").cloned().unwrap_or_default();
let new = format!("{}{}", current, hook.content);
let _ = event.metadata.insert("_hook".into(), new);
}
}
pub async fn handle_publish(
state: axum::extract::State<std::sync::Arc<crate::AppState>>,
axum::Json(mut event): axum::Json<AgentEvent>,
) -> impl axum::response::IntoResponse {
let hooks = state.hooks.match_event(&event.agent_id, &event.event_type);
if !hooks.is_empty() {
apply_event_hooks(&mut event, &hooks);
}
state.event_log.publish(event);
(axum::http::StatusCode::ACCEPTED, "accepted")
}
pub async fn handle_recent(
state: axum::extract::State<std::sync::Arc<crate::AppState>>,
axum::extract::Query(params): axum::extract::Query<FxHashMap<String, String>>,
) -> axum::Json<Vec<AgentEvent>> {
let n = params
.get("n")
.and_then(|v| v.parse().ok())
.unwrap_or(50)
.min(500);
axum::Json(state.event_log.recent(n))
}
pub async fn handle_stream(
state: axum::extract::State<std::sync::Arc<crate::AppState>>,
) -> axum::response::Sse<
impl futures::Stream<Item = Result<axum::response::sse::Event, std::convert::Infallible>>,
> {
use axum::response::sse::{Event, KeepAlive};
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
let rx = state.event_log.subscribe();
let stream = BroadcastStream::new(rx).filter_map(|r| match r {
Ok(event) => {
let data = serde_json::to_string(&event).unwrap_or_default();
Some(Ok(Event::default().data(data)))
}
Err(_) => None,
});
axum::response::Sse::new(stream).keep_alive(KeepAlive::default())
}
#[cfg(test)]
mod tests {
use super::*;
fn make_event(i: u64) -> AgentEvent {
AgentEvent {
agent_id: "test".into(),
event_type: "ping".into(),
severity: "info".into(),
timestamp: i,
metadata: BoundedMeta::default(),
}
}
#[test]
fn event_log_ring() {
let log = EventLog::new(3);
for i in 0..5 {
log.publish(make_event(i));
}
let recent = log.recent(10);
assert_eq!(recent.len(), 3);
assert_eq!(recent[0].timestamp, 4);
assert_eq!(recent[2].timestamp, 2);
}
#[test]
fn event_log_empty() {
let log = EventLog::new(100);
assert!(log.recent(10).is_empty());
}
#[test]
fn event_log_timestamp_auto() {
let log = EventLog::new(10);
log.publish(AgentEvent {
agent_id: "a".into(),
event_type: "start".into(),
severity: "info".into(),
timestamp: 0,
metadata: BoundedMeta::default(),
});
let recent = log.recent(1);
assert!(recent[0].timestamp > 0);
}
#[tokio::test]
async fn event_log_broadcast() {
let log = std::sync::Arc::new(EventLog::new(100));
let mut rx = log.subscribe();
log.publish(AgentEvent {
agent_id: "a".into(),
event_type: "test".into(),
severity: "info".into(),
timestamp: 1,
metadata: BoundedMeta::default(),
});
let ev = rx.recv().await.unwrap();
assert_eq!(ev.event_type, "test");
}
}