use std::{collections::VecDeque, sync::Mutex, time::Duration};
const MAX_RECORDS: usize = 256;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HookOutcome {
Continued,
Denied {
reason: String,
},
Observed,
Failed {
reason: String,
},
Dropped,
}
impl HookOutcome {
pub fn label(&self) -> &'static str {
match self {
Self::Continued => "continued",
Self::Denied { .. } => "denied",
Self::Observed => "observed",
Self::Failed { .. } => "failed",
Self::Dropped => "dropped",
}
}
pub fn detail(&self) -> Option<&str> {
match self {
Self::Denied { reason } | Self::Failed { reason } => Some(reason),
Self::Continued | Self::Observed | Self::Dropped => None,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HookActivity {
pub hook_id: String,
pub event: &'static str,
pub outcome: HookOutcome,
pub duration: Option<Duration>,
pub truncated: bool,
}
impl std::fmt::Display for HookActivity {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"{} {} {}",
self.hook_id,
self.event,
self.outcome.label()
)?;
if let Some(duration) = self.duration {
write!(formatter, " in {}ms", duration.as_millis())?;
}
if self.truncated {
formatter.write_str(" (output truncated)")?;
}
if let Some(detail) = self.outcome.detail() {
write!(formatter, ": {detail}")?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct HookActivityLog {
records: Mutex<VecDeque<HookActivity>>,
}
impl HookActivityLog {
pub fn record(&self, activity: HookActivity) {
tracing::debug!(
hook = %activity.hook_id,
event = activity.event,
outcome = activity.outcome.label(),
"hook finished"
);
let mut records = self
.records
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
if records.len() == MAX_RECORDS {
records.pop_front();
}
records.push_back(activity);
}
pub fn snapshot(&self) -> Vec<HookActivity> {
self.records
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.iter()
.cloned()
.collect()
}
}
#[cfg(test)]
#[path = "activity_tests.rs"]
mod tests;