1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap};
5use std::fmt;
6use std::path::{Path, PathBuf};
7use std::sync::Arc;
8use tokio::fs::{File, OpenOptions};
9use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
10use tokio::sync::Mutex;
11use uuid::Uuid;
12
13use crate::error::{FlowError, Result};
14use crate::model::{FlowEvent, FlowEventEnvelope, WorkflowSpec};
15
16#[async_trait]
21pub trait FlowEventObserver: Send + Sync {
22 async fn observe(&self, envelope: FlowEventEnvelope);
23}
24
25#[derive(Debug, Default)]
27pub struct NoopFlowEventObserver;
28
29#[async_trait]
30impl FlowEventObserver for NoopFlowEventObserver {
31 async fn observe(&self, _envelope: FlowEventEnvelope) {}
32}
33
34#[derive(Clone, Default)]
36pub struct FanoutFlowEventObserver {
37 observers: Vec<Arc<dyn FlowEventObserver>>,
38}
39
40impl FanoutFlowEventObserver {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn from_observers(observers: Vec<Arc<dyn FlowEventObserver>>) -> Self {
46 Self { observers }
47 }
48
49 pub fn with_observer<O>(mut self, observer: Arc<O>) -> Self
50 where
51 O: FlowEventObserver + 'static,
52 {
53 self.observers.push(observer);
54 self
55 }
56
57 pub fn with_dyn_observer(mut self, observer: Arc<dyn FlowEventObserver>) -> Self {
58 self.observers.push(observer);
59 self
60 }
61
62 pub fn len(&self) -> usize {
63 self.observers.len()
64 }
65
66 pub fn is_empty(&self) -> bool {
67 self.observers.is_empty()
68 }
69}
70
71impl fmt::Debug for FanoutFlowEventObserver {
72 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73 formatter
74 .debug_struct("FanoutFlowEventObserver")
75 .field("observers", &self.observers.len())
76 .finish()
77 }
78}
79
80#[async_trait]
81impl FlowEventObserver for FanoutFlowEventObserver {
82 async fn observe(&self, envelope: FlowEventEnvelope) {
83 for observer in &self.observers {
84 observer.observe(envelope.clone()).await;
85 }
86 }
87}
88
89#[derive(Debug, Default)]
91pub struct InMemoryFlowEventObserver {
92 events: Mutex<Vec<FlowEventEnvelope>>,
93}
94
95impl InMemoryFlowEventObserver {
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub async fn events(&self) -> Vec<FlowEventEnvelope> {
101 self.events.lock().await.clone()
102 }
103
104 pub async fn event_keys(&self) -> Vec<&'static str> {
105 self.events
106 .lock()
107 .await
108 .iter()
109 .map(|event| event.event.event_key())
110 .collect()
111 }
112}
113
114#[async_trait]
115impl FlowEventObserver for InMemoryFlowEventObserver {
116 async fn observe(&self, envelope: FlowEventEnvelope) {
117 self.events.lock().await.push(envelope);
118 }
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
123pub struct FlowWorkflowIdentity {
124 pub name: String,
125 pub version: String,
126}
127
128impl From<&WorkflowSpec> for FlowWorkflowIdentity {
129 fn from(spec: &WorkflowSpec) -> Self {
130 Self {
131 name: spec.name.clone(),
132 version: spec.version.clone(),
133 }
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
139pub struct A3sFlowEventSubject {
140 pub kind: String,
141 pub id: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
150pub struct A3sFlowEvent {
151 pub key: String,
152 pub run_id: String,
153 pub sequence: u64,
154 pub event_id: Uuid,
155 pub timestamp: DateTime<Utc>,
156 pub workflow: Option<FlowWorkflowIdentity>,
157 pub status: Option<String>,
158 pub subject: Option<A3sFlowEventSubject>,
159}
160
161impl A3sFlowEvent {
162 pub fn from_envelope(
163 envelope: &FlowEventEnvelope,
164 workflow: Option<FlowWorkflowIdentity>,
165 ) -> Self {
166 Self {
167 key: envelope.event.event_key().to_string(),
168 run_id: envelope.run_id.clone(),
169 sequence: envelope.sequence,
170 event_id: envelope.event_id,
171 timestamp: envelope.timestamp,
172 workflow,
173 status: event_status(&envelope.event).map(str::to_string),
174 subject: event_subject(&envelope.event),
175 }
176 }
177
178 pub fn safe_metric_labels(&self) -> BTreeMap<String, String> {
179 let mut labels = BTreeMap::new();
180 labels.insert("event_key".to_string(), self.key.clone());
181 if let Some(workflow) = &self.workflow {
182 labels.insert("workflow_name".to_string(), workflow.name.clone());
183 labels.insert("workflow_version".to_string(), workflow.version.clone());
184 }
185 if let Some(status) = &self.status {
186 labels.insert("status".to_string(), status.clone());
187 }
188 labels
189 }
190}
191
192#[async_trait]
194pub trait A3sFlowEventSink: Send + Sync {
195 async fn emit(&self, event: A3sFlowEvent);
196}
197
198#[derive(Debug)]
200pub struct A3sFlowEventBridge<S> {
201 sink: Arc<S>,
202 workflows: Mutex<HashMap<String, FlowWorkflowIdentity>>,
203}
204
205impl<S> A3sFlowEventBridge<S>
206where
207 S: A3sFlowEventSink,
208{
209 pub fn new(sink: Arc<S>) -> Self {
210 Self {
211 sink,
212 workflows: Mutex::new(HashMap::new()),
213 }
214 }
215
216 pub fn sink(&self) -> Arc<S> {
217 Arc::clone(&self.sink)
218 }
219}
220
221#[async_trait]
222impl<S> FlowEventObserver for A3sFlowEventBridge<S>
223where
224 S: A3sFlowEventSink,
225{
226 async fn observe(&self, envelope: FlowEventEnvelope) {
227 let workflow = {
228 let mut workflows = self.workflows.lock().await;
229 if let FlowEvent::RunCreated { spec, .. } = &envelope.event {
230 workflows.insert(envelope.run_id.clone(), FlowWorkflowIdentity::from(spec));
231 }
232 workflows.get(&envelope.run_id).cloned()
233 };
234 self.sink
235 .emit(A3sFlowEvent::from_envelope(&envelope, workflow))
236 .await;
237 }
238}
239
240#[derive(Debug, Default)]
242pub struct InMemoryA3sFlowEventSink {
243 events: Mutex<Vec<A3sFlowEvent>>,
244}
245
246impl InMemoryA3sFlowEventSink {
247 pub fn new() -> Self {
248 Self::default()
249 }
250
251 pub async fn events(&self) -> Vec<A3sFlowEvent> {
252 self.events.lock().await.clone()
253 }
254}
255
256#[async_trait]
257impl A3sFlowEventSink for InMemoryA3sFlowEventSink {
258 async fn emit(&self, event: A3sFlowEvent) {
259 self.events.lock().await.push(event);
260 }
261}
262
263#[derive(Debug)]
269pub struct LocalFileA3sFlowEventSink {
270 path: PathBuf,
271 lock: Mutex<()>,
272 last_error: Mutex<Option<String>>,
273}
274
275impl LocalFileA3sFlowEventSink {
276 pub fn new(path: impl Into<PathBuf>) -> Self {
277 Self {
278 path: path.into(),
279 lock: Mutex::new(()),
280 last_error: Mutex::new(None),
281 }
282 }
283
284 pub fn path(&self) -> &Path {
285 &self.path
286 }
287
288 pub async fn last_error(&self) -> Option<String> {
289 self.last_error.lock().await.clone()
290 }
291
292 pub async fn events(&self) -> Result<Vec<A3sFlowEvent>> {
293 let _guard = self.lock.lock().await;
294 let file = match File::open(&self.path).await {
295 Ok(file) => file,
296 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
297 Err(err) => return Err(FlowError::Io(err)),
298 };
299 let mut lines = BufReader::new(file).lines();
300 let mut events = Vec::new();
301 while let Some(line) = lines.next_line().await? {
302 if line.trim().is_empty() {
303 continue;
304 }
305 events.push(serde_json::from_str(&line)?);
306 }
307 Ok(events)
308 }
309
310 async fn append_event(&self, event: &A3sFlowEvent) -> Result<()> {
311 if let Some(parent) = self
312 .path
313 .parent()
314 .filter(|path| !path.as_os_str().is_empty())
315 {
316 tokio::fs::create_dir_all(parent).await?;
317 }
318 let mut file = OpenOptions::new()
319 .create(true)
320 .append(true)
321 .open(&self.path)
322 .await?;
323 file.write_all(serde_json::to_string(event)?.as_bytes())
324 .await?;
325 file.write_all(b"\n").await?;
326 file.flush().await?;
327 file.sync_data().await?;
328 Ok(())
329 }
330}
331
332#[async_trait]
333impl A3sFlowEventSink for LocalFileA3sFlowEventSink {
334 async fn emit(&self, event: A3sFlowEvent) {
335 let _guard = self.lock.lock().await;
336 match self.append_event(&event).await {
337 Ok(()) => {
338 *self.last_error.lock().await = None;
339 }
340 Err(err) => {
341 let message = err.to_string();
342 tracing::warn!(
343 error = %message,
344 path = %self.path.display(),
345 "failed to emit flow audit event"
346 );
347 *self.last_error.lock().await = Some(message);
348 }
349 }
350 }
351}
352
353fn event_status(event: &FlowEvent) -> Option<&'static str> {
354 match event {
355 FlowEvent::RunCreated { .. } => Some("pending"),
356 FlowEvent::RunStarted => Some("running"),
357 FlowEvent::RunCompleted { .. } => Some("completed"),
358 FlowEvent::RunFailed { .. } => Some("failed"),
359 FlowEvent::RunCancelled { .. } => Some("cancelled"),
360 FlowEvent::StepCreated { .. } => Some("pending"),
361 FlowEvent::StepStarted { .. } => Some("running"),
362 FlowEvent::StepCompleted { .. } => Some("completed"),
363 FlowEvent::StepRetrying { .. } => Some("retrying"),
364 FlowEvent::StepFailed { .. } => Some("failed"),
365 FlowEvent::WaitCreated { .. } => Some("waiting"),
366 FlowEvent::WaitCompleted { .. } => Some("completed"),
367 FlowEvent::HookCreated { .. } => Some("active"),
368 FlowEvent::HookReceived { .. } => Some("received"),
369 FlowEvent::HookDisposed { .. } => Some("disposed"),
370 }
371}
372
373fn event_subject(event: &FlowEvent) -> Option<A3sFlowEventSubject> {
374 match event {
375 FlowEvent::StepCreated { step_id, .. }
376 | FlowEvent::StepStarted { step_id, .. }
377 | FlowEvent::StepCompleted { step_id, .. }
378 | FlowEvent::StepRetrying { step_id, .. }
379 | FlowEvent::StepFailed { step_id, .. } => Some(A3sFlowEventSubject {
380 kind: "step".to_string(),
381 id: step_id.clone(),
382 }),
383 FlowEvent::WaitCreated { wait_id, .. } | FlowEvent::WaitCompleted { wait_id } => {
384 Some(A3sFlowEventSubject {
385 kind: "wait".to_string(),
386 id: wait_id.clone(),
387 })
388 }
389 FlowEvent::HookCreated { hook_id, .. }
390 | FlowEvent::HookReceived { hook_id, .. }
391 | FlowEvent::HookDisposed { hook_id } => Some(A3sFlowEventSubject {
392 kind: "hook".to_string(),
393 id: hook_id.clone(),
394 }),
395 FlowEvent::RunCreated { .. }
396 | FlowEvent::RunStarted
397 | FlowEvent::RunCompleted { .. }
398 | FlowEvent::RunFailed { .. }
399 | FlowEvent::RunCancelled { .. } => None,
400 }
401}