#![allow(dead_code)]
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AuditEventType {
WorkflowSubmitted,
StateChanged,
TaskStateChanged,
ParameterUpdated,
WorkflowCancelled,
ErrorOccurred,
WorkflowCompleted,
RetryAttempted,
}
impl AuditEventType {
#[must_use]
pub fn is_state_change(&self) -> bool {
matches!(
self,
Self::StateChanged
| Self::TaskStateChanged
| Self::WorkflowCancelled
| Self::WorkflowCompleted
)
}
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::WorkflowSubmitted => "workflow_submitted",
Self::StateChanged => "state_changed",
Self::TaskStateChanged => "task_state_changed",
Self::ParameterUpdated => "parameter_updated",
Self::WorkflowCancelled => "workflow_cancelled",
Self::ErrorOccurred => "error_occurred",
Self::WorkflowCompleted => "workflow_completed",
Self::RetryAttempted => "retry_attempted",
}
}
}
#[derive(Debug, Clone)]
pub struct WorkflowAuditEntry {
pub run_id: String,
pub event_type: AuditEventType,
pub timestamp_secs: u64,
pub actor: Option<String>,
pub context: Option<String>,
}
impl WorkflowAuditEntry {
#[must_use]
pub fn new(run_id: impl Into<String>, event_type: AuditEventType) -> Self {
let timestamp_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
Self {
run_id: run_id.into(),
event_type,
timestamp_secs,
actor: None,
context: None,
}
}
#[must_use]
pub fn with_timestamp(
run_id: impl Into<String>,
event_type: AuditEventType,
timestamp_secs: u64,
) -> Self {
Self {
run_id: run_id.into(),
event_type,
timestamp_secs,
actor: None,
context: None,
}
}
#[must_use]
pub fn with_actor(mut self, actor: impl Into<String>) -> Self {
self.actor = Some(actor.into());
self
}
#[must_use]
pub fn with_context(mut self, ctx: impl Into<String>) -> Self {
self.context = Some(ctx.into());
self
}
#[must_use]
pub fn description(&self) -> String {
format!(
"[{}] run={} event={}{}",
self.timestamp_secs,
self.run_id,
self.event_type.as_str(),
self.actor
.as_ref()
.map(|a| format!(" actor={a}"))
.unwrap_or_default(),
)
}
}
#[derive(Debug, Default)]
pub struct WorkflowAudit {
index: HashMap<String, Vec<usize>>,
all: Vec<WorkflowAuditEntry>,
}
impl WorkflowAudit {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, entry: WorkflowAuditEntry) {
let idx = self.all.len();
self.index
.entry(entry.run_id.clone())
.or_default()
.push(idx);
self.all.push(entry);
}
#[must_use]
pub fn entries_for_run(&self, run_id: &str) -> Vec<&WorkflowAuditEntry> {
self.index
.get(run_id)
.map(|idxs| idxs.iter().map(|&i| &self.all[i]).collect())
.unwrap_or_default()
}
#[must_use]
pub fn recent(&self, n: usize) -> Vec<&WorkflowAuditEntry> {
self.all.iter().rev().take(n).collect()
}
#[must_use]
pub fn total_entries(&self) -> usize {
self.all.len()
}
#[must_use]
pub fn all_entries(&self) -> &[WorkflowAuditEntry] {
&self.all
}
#[must_use]
pub fn by_event_type(&self, et: &AuditEventType) -> Vec<&WorkflowAuditEntry> {
self.all.iter().filter(|e| &e.event_type == et).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_event_type_is_state_change_true() {
assert!(AuditEventType::StateChanged.is_state_change());
assert!(AuditEventType::TaskStateChanged.is_state_change());
assert!(AuditEventType::WorkflowCancelled.is_state_change());
assert!(AuditEventType::WorkflowCompleted.is_state_change());
}
#[test]
fn test_event_type_is_state_change_false() {
assert!(!AuditEventType::WorkflowSubmitted.is_state_change());
assert!(!AuditEventType::ParameterUpdated.is_state_change());
assert!(!AuditEventType::ErrorOccurred.is_state_change());
assert!(!AuditEventType::RetryAttempted.is_state_change());
}
#[test]
fn test_event_type_as_str() {
assert_eq!(
AuditEventType::WorkflowSubmitted.as_str(),
"workflow_submitted"
);
assert_eq!(AuditEventType::StateChanged.as_str(), "state_changed");
assert_eq!(AuditEventType::ErrorOccurred.as_str(), "error_occurred");
}
#[test]
fn test_entry_description_no_actor() {
let e = WorkflowAuditEntry::with_timestamp("run1", AuditEventType::StateChanged, 1000);
let desc = e.description();
assert!(desc.contains("run1"));
assert!(desc.contains("state_changed"));
assert!(desc.contains("1000"));
}
#[test]
fn test_entry_description_with_actor() {
let e = WorkflowAuditEntry::with_timestamp("run2", AuditEventType::StateChanged, 2000)
.with_actor("operator");
let desc = e.description();
assert!(desc.contains("actor=operator"));
}
#[test]
fn test_entry_with_context() {
let e = WorkflowAuditEntry::new("r", AuditEventType::ParameterUpdated)
.with_context(r#"{"key":"val"}"#);
assert!(e.context.is_some());
}
#[test]
fn test_audit_record_and_total() {
let mut audit = WorkflowAudit::new();
audit.record(WorkflowAuditEntry::new(
"r1",
AuditEventType::WorkflowSubmitted,
));
audit.record(WorkflowAuditEntry::new("r1", AuditEventType::StateChanged));
audit.record(WorkflowAuditEntry::new(
"r2",
AuditEventType::WorkflowSubmitted,
));
assert_eq!(audit.total_entries(), 3);
}
#[test]
fn test_entries_for_run() {
let mut audit = WorkflowAudit::new();
audit.record(WorkflowAuditEntry::new(
"run1",
AuditEventType::WorkflowSubmitted,
));
audit.record(WorkflowAuditEntry::new(
"run2",
AuditEventType::WorkflowSubmitted,
));
audit.record(WorkflowAuditEntry::new(
"run1",
AuditEventType::WorkflowCompleted,
));
let entries = audit.entries_for_run("run1");
assert_eq!(entries.len(), 2);
}
#[test]
fn test_entries_for_unknown_run() {
let audit = WorkflowAudit::new();
assert!(audit.entries_for_run("ghost").is_empty());
}
#[test]
fn test_recent_entries() {
let mut audit = WorkflowAudit::new();
for i in 0..5u64 {
audit.record(WorkflowAuditEntry::with_timestamp(
format!("run{i}"),
AuditEventType::WorkflowSubmitted,
i,
));
}
let recent = audit.recent(3);
assert_eq!(recent.len(), 3);
assert_eq!(recent[0].timestamp_secs, 4);
}
#[test]
fn test_by_event_type() {
let mut audit = WorkflowAudit::new();
audit.record(WorkflowAuditEntry::new("r", AuditEventType::ErrorOccurred));
audit.record(WorkflowAuditEntry::new("r", AuditEventType::StateChanged));
audit.record(WorkflowAuditEntry::new("r", AuditEventType::ErrorOccurred));
let errors = audit.by_event_type(&AuditEventType::ErrorOccurred);
assert_eq!(errors.len(), 2);
}
#[test]
fn test_all_entries_insertion_order() {
let mut audit = WorkflowAudit::new();
audit.record(WorkflowAuditEntry::with_timestamp(
"r",
AuditEventType::WorkflowSubmitted,
1,
));
audit.record(WorkflowAuditEntry::with_timestamp(
"r",
AuditEventType::WorkflowCompleted,
2,
));
let all = audit.all_entries();
assert_eq!(all[0].timestamp_secs, 1);
assert_eq!(all[1].timestamp_secs, 2);
}
}