use std::collections::{HashMap, VecDeque};
use std::time::{SystemTime, UNIX_EPOCH};
use tower_rules::{MeshIdent, RuleId, Trigger};
use crate::event::{Event, EventScope, Level};
use crate::supervisor::{scope_id, FiredEvent};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriggerKind {
AgentDispatch,
Notification,
YubabaAction,
}
impl From<&Trigger> for TriggerKind {
fn from(t: &Trigger) -> Self {
match t {
Trigger::AgentDispatch { .. } => TriggerKind::AgentDispatch,
Trigger::Notification { .. } => TriggerKind::Notification,
Trigger::YubabaAction { .. } => TriggerKind::YubabaAction,
}
}
}
#[derive(Debug, Clone)]
pub struct DispatchContext {
pub rule_id: RuleId,
pub rule: tower_rules::TowerRule,
pub matched_event: Event,
pub context_events: Vec<Event>,
pub peer: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DispatchOutcome {
Dispatched,
Failed { cause: String },
}
#[derive(Debug, Clone)]
pub struct DispatchAuditEvent {
pub rule_id: RuleId,
pub trigger_kind: TriggerKind,
pub outcome: DispatchOutcome,
pub matched_seq: u64,
pub matched_scope: String,
pub context_event_count: usize,
pub peer: Option<String>,
pub ts_ms: u64,
}
impl DispatchAuditEvent {
pub fn to_scryer_event(&self, seq: u64) -> Event {
let (target, level) = match &self.outcome {
DispatchOutcome::Dispatched => ("tower.dispatch", Level::Info),
DispatchOutcome::Failed { .. } => ("tower.dispatch.failed", Level::Error),
};
let mut fields: std::collections::HashMap<String, serde_json::Value> = HashMap::new();
fields.insert("rule_id".into(), serde_json::json!(self.rule_id.0));
fields.insert(
"trigger_kind".into(),
serde_json::json!(trigger_kind_name(self.trigger_kind)),
);
fields.insert("matched_seq".into(), serde_json::json!(self.matched_seq));
fields.insert("matched_scope".into(), serde_json::json!(&self.matched_scope));
fields.insert(
"context_event_count".into(),
serde_json::json!(self.context_event_count),
);
if let Some(peer) = &self.peer {
fields.insert("peer".into(), serde_json::json!(peer));
}
if let DispatchOutcome::Failed { cause } = &self.outcome {
fields.insert("cause".into(), serde_json::json!(cause));
}
let msg = match &self.outcome {
DispatchOutcome::Dispatched => {
format!("rule {} dispatched ({})", self.rule_id.0, trigger_kind_name(self.trigger_kind))
}
DispatchOutcome::Failed { cause } => {
format!("rule {} dispatch failed: {}", self.rule_id.0, cause)
}
};
Event {
scope: EventScope::Service(MeshIdent("tower.local".into())),
level,
target: target.into(),
msg,
fields,
seq,
}
}
}
fn trigger_kind_name(kind: TriggerKind) -> &'static str {
match kind {
TriggerKind::AgentDispatch => "agent_dispatch",
TriggerKind::Notification => "notification",
TriggerKind::YubabaAction => "yubaba_action",
}
}
#[derive(Debug, thiserror::Error)]
pub enum DispatchError {
#[error("{0}")]
Failed(String),
}
pub trait TriggerHandler: Send + Sync {
fn handle(&self, ctx: &DispatchContext) -> Result<(), DispatchError>;
}
pub trait DispatchRecorder: Send + Sync {
fn record(&self, event: DispatchAuditEvent);
}
pub struct NoopTriggerHandler;
impl TriggerHandler for NoopTriggerHandler {
fn handle(&self, _ctx: &DispatchContext) -> Result<(), DispatchError> {
Ok(())
}
}
struct ContextRing {
capacity: usize,
events: VecDeque<Event>,
}
impl ContextRing {
fn new(capacity: usize) -> Self {
Self { capacity, events: VecDeque::with_capacity(capacity) }
}
fn snapshot_and_insert(&mut self, event: &Event) -> Vec<Event> {
let snapshot: Vec<Event> = self.events.iter().cloned().collect();
if self.events.len() >= self.capacity {
self.events.pop_front();
}
self.events.push_back(event.clone());
snapshot
}
}
pub struct DispatchEngine {
context_rings: HashMap<RuleId, ContextRing>,
context_window: usize,
trigger_handler: Box<dyn TriggerHandler>,
recorder: Box<dyn DispatchRecorder>,
}
impl DispatchEngine {
pub fn new(
context_window: usize,
trigger_handler: Box<dyn TriggerHandler>,
recorder: Box<dyn DispatchRecorder>,
) -> Self {
Self {
context_rings: HashMap::new(),
context_window,
trigger_handler,
recorder,
}
}
pub fn process(&mut self, fired: Vec<FiredEvent>) {
for fe in fired {
let ring = self
.context_rings
.entry(fe.rule_id.clone())
.or_insert_with(|| ContextRing::new(self.context_window));
let context_events = ring.snapshot_and_insert(&fe.event);
let context_count = context_events.len();
let trigger_kind = TriggerKind::from(&fe.rule.trigger);
let matched_scope = scope_id(&fe.event.scope);
self.recorder.record(DispatchAuditEvent {
rule_id: fe.rule_id.clone(),
trigger_kind,
outcome: DispatchOutcome::Dispatched,
matched_seq: fe.event.seq,
matched_scope: matched_scope.clone(),
context_event_count: context_count,
peer: fe.peer.clone(),
ts_ms: now_ms(),
});
let ctx = DispatchContext {
rule_id: fe.rule_id.clone(),
rule: fe.rule,
matched_event: fe.event,
context_events,
peer: fe.peer,
};
if let Err(e) = self.trigger_handler.handle(&ctx) {
self.recorder.record(DispatchAuditEvent {
rule_id: fe.rule_id,
trigger_kind,
outcome: DispatchOutcome::Failed { cause: e.to_string() },
matched_seq: ctx.matched_event.seq,
matched_scope,
context_event_count: context_count,
peer: ctx.peer,
ts_ms: now_ms(),
});
}
}
}
}
fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}