use async_trait::async_trait;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::fs::{File, OpenOptions};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::sync::Mutex;
use uuid::Uuid;
use crate::error::{FlowError, Result};
use crate::model::{FlowEvent, FlowEventEnvelope, WorkflowSpec};
#[async_trait]
pub trait FlowEventObserver: Send + Sync {
async fn observe(&self, envelope: FlowEventEnvelope);
}
#[derive(Debug, Default)]
pub struct NoopFlowEventObserver;
#[async_trait]
impl FlowEventObserver for NoopFlowEventObserver {
async fn observe(&self, _envelope: FlowEventEnvelope) {}
}
#[derive(Clone, Default)]
pub struct FanoutFlowEventObserver {
observers: Vec<Arc<dyn FlowEventObserver>>,
}
impl FanoutFlowEventObserver {
pub fn new() -> Self {
Self::default()
}
pub fn from_observers(observers: Vec<Arc<dyn FlowEventObserver>>) -> Self {
Self { observers }
}
pub fn with_observer<O>(mut self, observer: Arc<O>) -> Self
where
O: FlowEventObserver + 'static,
{
self.observers.push(observer);
self
}
pub fn with_dyn_observer(mut self, observer: Arc<dyn FlowEventObserver>) -> Self {
self.observers.push(observer);
self
}
pub fn len(&self) -> usize {
self.observers.len()
}
pub fn is_empty(&self) -> bool {
self.observers.is_empty()
}
}
impl fmt::Debug for FanoutFlowEventObserver {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("FanoutFlowEventObserver")
.field("observers", &self.observers.len())
.finish()
}
}
#[async_trait]
impl FlowEventObserver for FanoutFlowEventObserver {
async fn observe(&self, envelope: FlowEventEnvelope) {
for observer in &self.observers {
observer.observe(envelope.clone()).await;
}
}
}
#[derive(Debug, Default)]
pub struct InMemoryFlowEventObserver {
events: Mutex<Vec<FlowEventEnvelope>>,
}
impl InMemoryFlowEventObserver {
pub fn new() -> Self {
Self::default()
}
pub async fn events(&self) -> Vec<FlowEventEnvelope> {
self.events.lock().await.clone()
}
pub async fn event_keys(&self) -> Vec<&'static str> {
self.events
.lock()
.await
.iter()
.map(|event| event.event.event_key())
.collect()
}
}
#[async_trait]
impl FlowEventObserver for InMemoryFlowEventObserver {
async fn observe(&self, envelope: FlowEventEnvelope) {
self.events.lock().await.push(envelope);
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct FlowWorkflowIdentity {
pub name: String,
pub version: String,
}
impl From<&WorkflowSpec> for FlowWorkflowIdentity {
fn from(spec: &WorkflowSpec) -> Self {
Self {
name: spec.name.clone(),
version: spec.version.clone(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct A3sFlowEventSubject {
pub kind: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct A3sFlowEvent {
pub key: String,
pub run_id: String,
pub sequence: u64,
pub event_id: Uuid,
pub timestamp: DateTime<Utc>,
pub workflow: Option<FlowWorkflowIdentity>,
pub status: Option<String>,
pub subject: Option<A3sFlowEventSubject>,
}
impl A3sFlowEvent {
pub fn from_envelope(
envelope: &FlowEventEnvelope,
workflow: Option<FlowWorkflowIdentity>,
) -> Self {
Self {
key: envelope.event.event_key().to_string(),
run_id: envelope.run_id.clone(),
sequence: envelope.sequence,
event_id: envelope.event_id,
timestamp: envelope.timestamp,
workflow,
status: event_status(&envelope.event).map(str::to_string),
subject: event_subject(&envelope.event),
}
}
pub fn safe_metric_labels(&self) -> BTreeMap<String, String> {
let mut labels = BTreeMap::new();
labels.insert("event_key".to_string(), self.key.clone());
if let Some(workflow) = &self.workflow {
labels.insert("workflow_name".to_string(), workflow.name.clone());
labels.insert("workflow_version".to_string(), workflow.version.clone());
}
if let Some(status) = &self.status {
labels.insert("status".to_string(), status.clone());
}
labels
}
}
#[async_trait]
pub trait A3sFlowEventSink: Send + Sync {
async fn emit(&self, event: A3sFlowEvent);
}
#[derive(Debug)]
pub struct A3sFlowEventBridge<S> {
sink: Arc<S>,
workflows: Mutex<HashMap<String, FlowWorkflowIdentity>>,
}
impl<S> A3sFlowEventBridge<S>
where
S: A3sFlowEventSink,
{
pub fn new(sink: Arc<S>) -> Self {
Self {
sink,
workflows: Mutex::new(HashMap::new()),
}
}
pub fn sink(&self) -> Arc<S> {
Arc::clone(&self.sink)
}
}
#[async_trait]
impl<S> FlowEventObserver for A3sFlowEventBridge<S>
where
S: A3sFlowEventSink,
{
async fn observe(&self, envelope: FlowEventEnvelope) {
let workflow = {
let mut workflows = self.workflows.lock().await;
if let FlowEvent::RunCreated { spec, .. } = &envelope.event {
workflows.insert(envelope.run_id.clone(), FlowWorkflowIdentity::from(spec));
}
workflows.get(&envelope.run_id).cloned()
};
self.sink
.emit(A3sFlowEvent::from_envelope(&envelope, workflow))
.await;
}
}
#[derive(Debug, Default)]
pub struct InMemoryA3sFlowEventSink {
events: Mutex<Vec<A3sFlowEvent>>,
}
impl InMemoryA3sFlowEventSink {
pub fn new() -> Self {
Self::default()
}
pub async fn events(&self) -> Vec<A3sFlowEvent> {
self.events.lock().await.clone()
}
}
#[async_trait]
impl A3sFlowEventSink for InMemoryA3sFlowEventSink {
async fn emit(&self, event: A3sFlowEvent) {
self.events.lock().await.push(event);
}
}
#[derive(Debug)]
pub struct LocalFileA3sFlowEventSink {
path: PathBuf,
lock: Mutex<()>,
last_error: Mutex<Option<String>>,
}
impl LocalFileA3sFlowEventSink {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self {
path: path.into(),
lock: Mutex::new(()),
last_error: Mutex::new(None),
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub async fn last_error(&self) -> Option<String> {
self.last_error.lock().await.clone()
}
pub async fn events(&self) -> Result<Vec<A3sFlowEvent>> {
let _guard = self.lock.lock().await;
let file = match File::open(&self.path).await {
Ok(file) => file,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(err) => return Err(FlowError::Io(err)),
};
let mut lines = BufReader::new(file).lines();
let mut events = Vec::new();
while let Some(line) = lines.next_line().await? {
if line.trim().is_empty() {
continue;
}
events.push(serde_json::from_str(&line)?);
}
Ok(events)
}
async fn append_event(&self, event: &A3sFlowEvent) -> Result<()> {
if let Some(parent) = self
.path
.parent()
.filter(|path| !path.as_os_str().is_empty())
{
tokio::fs::create_dir_all(parent).await?;
}
let mut file = OpenOptions::new()
.create(true)
.append(true)
.open(&self.path)
.await?;
file.write_all(serde_json::to_string(event)?.as_bytes())
.await?;
file.write_all(b"\n").await?;
file.flush().await?;
file.sync_data().await?;
Ok(())
}
}
#[async_trait]
impl A3sFlowEventSink for LocalFileA3sFlowEventSink {
async fn emit(&self, event: A3sFlowEvent) {
let _guard = self.lock.lock().await;
match self.append_event(&event).await {
Ok(()) => {
*self.last_error.lock().await = None;
}
Err(err) => {
let message = err.to_string();
tracing::warn!(
error = %message,
path = %self.path.display(),
"failed to emit flow audit event"
);
*self.last_error.lock().await = Some(message);
}
}
}
}
fn event_status(event: &FlowEvent) -> Option<&'static str> {
match event {
FlowEvent::RunCreated { .. } => Some("pending"),
FlowEvent::RunStarted => Some("running"),
FlowEvent::RunCompleted { .. } => Some("completed"),
FlowEvent::RunFailed { .. } => Some("failed"),
FlowEvent::RunCancelled { .. } => Some("cancelled"),
FlowEvent::StepCreated { .. } => Some("pending"),
FlowEvent::StepStarted { .. } => Some("running"),
FlowEvent::StepCompleted { .. } => Some("completed"),
FlowEvent::StepRetrying { .. } => Some("retrying"),
FlowEvent::StepFailed { .. } => Some("failed"),
FlowEvent::WaitCreated { .. } => Some("waiting"),
FlowEvent::WaitCompleted { .. } => Some("completed"),
FlowEvent::HookCreated { .. } => Some("active"),
FlowEvent::HookReceived { .. } => Some("received"),
FlowEvent::HookDisposed { .. } => Some("disposed"),
}
}
fn event_subject(event: &FlowEvent) -> Option<A3sFlowEventSubject> {
match event {
FlowEvent::StepCreated { step_id, .. }
| FlowEvent::StepStarted { step_id, .. }
| FlowEvent::StepCompleted { step_id, .. }
| FlowEvent::StepRetrying { step_id, .. }
| FlowEvent::StepFailed { step_id, .. } => Some(A3sFlowEventSubject {
kind: "step".to_string(),
id: step_id.clone(),
}),
FlowEvent::WaitCreated { wait_id, .. } | FlowEvent::WaitCompleted { wait_id } => {
Some(A3sFlowEventSubject {
kind: "wait".to_string(),
id: wait_id.clone(),
})
}
FlowEvent::HookCreated { hook_id, .. }
| FlowEvent::HookReceived { hook_id, .. }
| FlowEvent::HookDisposed { hook_id } => Some(A3sFlowEventSubject {
kind: "hook".to_string(),
id: hook_id.clone(),
}),
FlowEvent::RunCreated { .. }
| FlowEvent::RunStarted
| FlowEvent::RunCompleted { .. }
| FlowEvent::RunFailed { .. }
| FlowEvent::RunCancelled { .. } => None,
}
}