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#[cfg(feature = "a3s-event")]
193pub struct A3sEventBusFlowEventSink {
200 bus: Arc<a3s_event::EventBus>,
201 category: String,
202 source: String,
203 last_error: Mutex<Option<String>>,
204}
205
206#[cfg(feature = "a3s-event")]
207impl A3sEventBusFlowEventSink {
208 pub fn new(bus: Arc<a3s_event::EventBus>) -> Self {
209 Self {
210 bus,
211 category: "flow".to_string(),
212 source: "a3s-flow".to_string(),
213 last_error: Mutex::new(None),
214 }
215 }
216
217 pub fn with_category(mut self, category: impl Into<String>) -> Self {
218 self.category = category.into();
219 self
220 }
221
222 pub fn with_source(mut self, source: impl Into<String>) -> Self {
223 self.source = source.into();
224 self
225 }
226
227 pub fn bus(&self) -> Arc<a3s_event::EventBus> {
228 Arc::clone(&self.bus)
229 }
230
231 pub fn category(&self) -> &str {
232 &self.category
233 }
234
235 pub fn source(&self) -> &str {
236 &self.source
237 }
238
239 pub async fn last_error(&self) -> Option<String> {
240 self.last_error.lock().await.clone()
241 }
242
243 pub fn to_a3s_event(
244 &self,
245 event: &A3sFlowEvent,
246 ) -> std::result::Result<a3s_event::Event, serde_json::Error> {
247 let topic = flow_event_topic(&event.key);
248 let subject = self.bus.provider_arc().build_subject(&self.category, topic);
249 let timestamp = event.timestamp.timestamp_millis();
250 let mut metadata = HashMap::new();
251 metadata.insert("flow.event_key".to_string(), event.key.clone());
252 metadata.insert("flow.run_id".to_string(), event.run_id.clone());
253 metadata.insert("flow.sequence".to_string(), event.sequence.to_string());
254 metadata.insert("flow.event_id".to_string(), event.event_id.to_string());
255 if let Some(status) = &event.status {
256 metadata.insert("flow.status".to_string(), status.clone());
257 }
258 if let Some(workflow) = &event.workflow {
259 metadata.insert("flow.workflow_name".to_string(), workflow.name.clone());
260 metadata.insert(
261 "flow.workflow_version".to_string(),
262 workflow.version.clone(),
263 );
264 }
265 if let Some(subject) = &event.subject {
266 metadata.insert("flow.subject_kind".to_string(), subject.kind.clone());
267 metadata.insert("flow.subject_id".to_string(), subject.id.clone());
268 }
269
270 Ok(a3s_event::Event {
271 id: format!("evt-{}", event.event_id),
272 subject,
273 category: self.category.clone(),
274 event_type: event.key.clone(),
275 version: 1,
276 payload: serde_json::to_value(event)?,
277 summary: format!("{} for run {}", event.key, event.run_id),
278 source: self.source.clone(),
279 timestamp: timestamp.max(0) as u64,
280 metadata,
281 })
282 }
283}
284
285#[cfg(feature = "a3s-event")]
286impl fmt::Debug for A3sEventBusFlowEventSink {
287 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
288 formatter
289 .debug_struct("A3sEventBusFlowEventSink")
290 .field("category", &self.category)
291 .field("source", &self.source)
292 .finish_non_exhaustive()
293 }
294}
295
296#[cfg(feature = "a3s-event")]
297#[async_trait]
298impl A3sFlowEventSink for A3sEventBusFlowEventSink {
299 async fn emit(&self, event: A3sFlowEvent) {
300 let a3s_event = match self.to_a3s_event(&event) {
301 Ok(event) => event,
302 Err(err) => {
303 let message = err.to_string();
304 tracing::warn!(
305 error = %message,
306 event_key = %event.key,
307 run_id = %event.run_id,
308 "failed to convert flow event for A3S Event"
309 );
310 *self.last_error.lock().await = Some(message);
311 return;
312 }
313 };
314
315 match self.bus.publish_event(&a3s_event).await {
316 Ok(_) => {
317 *self.last_error.lock().await = None;
318 }
319 Err(err) => {
320 let message = err.to_string();
321 tracing::warn!(
322 error = %message,
323 subject = %a3s_event.subject,
324 event_type = %a3s_event.event_type,
325 "failed to publish flow event to A3S Event"
326 );
327 *self.last_error.lock().await = Some(message);
328 }
329 }
330 }
331}
332
333#[async_trait]
335pub trait A3sFlowEventSink: Send + Sync {
336 async fn emit(&self, event: A3sFlowEvent);
337}
338
339#[derive(Debug)]
341pub struct A3sFlowEventBridge<S> {
342 sink: Arc<S>,
343 workflows: Mutex<HashMap<String, FlowWorkflowIdentity>>,
344}
345
346impl<S> A3sFlowEventBridge<S>
347where
348 S: A3sFlowEventSink,
349{
350 pub fn new(sink: Arc<S>) -> Self {
351 Self {
352 sink,
353 workflows: Mutex::new(HashMap::new()),
354 }
355 }
356
357 pub fn sink(&self) -> Arc<S> {
358 Arc::clone(&self.sink)
359 }
360}
361
362#[async_trait]
363impl<S> FlowEventObserver for A3sFlowEventBridge<S>
364where
365 S: A3sFlowEventSink,
366{
367 async fn observe(&self, envelope: FlowEventEnvelope) {
368 let workflow = {
369 let mut workflows = self.workflows.lock().await;
370 if let FlowEvent::RunCreated { spec, .. } = &envelope.event {
371 workflows.insert(envelope.run_id.clone(), FlowWorkflowIdentity::from(spec));
372 }
373 workflows.get(&envelope.run_id).cloned()
374 };
375 self.sink
376 .emit(A3sFlowEvent::from_envelope(&envelope, workflow))
377 .await;
378 }
379}
380
381#[derive(Debug, Default)]
383pub struct InMemoryA3sFlowEventSink {
384 events: Mutex<Vec<A3sFlowEvent>>,
385}
386
387impl InMemoryA3sFlowEventSink {
388 pub fn new() -> Self {
389 Self::default()
390 }
391
392 pub async fn events(&self) -> Vec<A3sFlowEvent> {
393 self.events.lock().await.clone()
394 }
395}
396
397#[async_trait]
398impl A3sFlowEventSink for InMemoryA3sFlowEventSink {
399 async fn emit(&self, event: A3sFlowEvent) {
400 self.events.lock().await.push(event);
401 }
402}
403
404#[derive(Debug)]
410pub struct LocalFileA3sFlowEventSink {
411 path: PathBuf,
412 lock: Mutex<()>,
413 last_error: Mutex<Option<String>>,
414}
415
416impl LocalFileA3sFlowEventSink {
417 pub fn new(path: impl Into<PathBuf>) -> Self {
418 Self {
419 path: path.into(),
420 lock: Mutex::new(()),
421 last_error: Mutex::new(None),
422 }
423 }
424
425 pub fn path(&self) -> &Path {
426 &self.path
427 }
428
429 pub async fn last_error(&self) -> Option<String> {
430 self.last_error.lock().await.clone()
431 }
432
433 pub async fn events(&self) -> Result<Vec<A3sFlowEvent>> {
434 let _guard = self.lock.lock().await;
435 let file = match File::open(&self.path).await {
436 Ok(file) => file,
437 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
438 Err(err) => return Err(FlowError::Io(err)),
439 };
440 let mut lines = BufReader::new(file).lines();
441 let mut events = Vec::new();
442 while let Some(line) = lines.next_line().await? {
443 if line.trim().is_empty() {
444 continue;
445 }
446 events.push(serde_json::from_str(&line)?);
447 }
448 Ok(events)
449 }
450
451 async fn append_event(&self, event: &A3sFlowEvent) -> Result<()> {
452 if let Some(parent) = self
453 .path
454 .parent()
455 .filter(|path| !path.as_os_str().is_empty())
456 {
457 tokio::fs::create_dir_all(parent).await?;
458 }
459 let mut file = OpenOptions::new()
460 .create(true)
461 .append(true)
462 .open(&self.path)
463 .await?;
464 file.write_all(serde_json::to_string(event)?.as_bytes())
465 .await?;
466 file.write_all(b"\n").await?;
467 file.flush().await?;
468 file.sync_data().await?;
469 Ok(())
470 }
471}
472
473#[async_trait]
474impl A3sFlowEventSink for LocalFileA3sFlowEventSink {
475 async fn emit(&self, event: A3sFlowEvent) {
476 let _guard = self.lock.lock().await;
477 match self.append_event(&event).await {
478 Ok(()) => {
479 *self.last_error.lock().await = None;
480 }
481 Err(err) => {
482 let message = err.to_string();
483 tracing::warn!(
484 error = %message,
485 path = %self.path.display(),
486 "failed to emit flow audit event"
487 );
488 *self.last_error.lock().await = Some(message);
489 }
490 }
491 }
492}
493
494#[cfg(feature = "a3s-event")]
495fn flow_event_topic(key: &str) -> &str {
496 key.strip_prefix("flow.").unwrap_or(key)
497}
498
499fn event_status(event: &FlowEvent) -> Option<&'static str> {
500 match event {
501 FlowEvent::RunCreated { .. } => Some("pending"),
502 FlowEvent::RunStarted => Some("running"),
503 FlowEvent::RunCompleted { .. } => Some("completed"),
504 FlowEvent::RunFailed { .. } => Some("failed"),
505 FlowEvent::RunCancelled { .. } => Some("cancelled"),
506 FlowEvent::StepCreated { .. } => Some("pending"),
507 FlowEvent::StepStarted { .. } => Some("running"),
508 FlowEvent::StepCompleted { .. } => Some("completed"),
509 FlowEvent::StepRetrying { .. } => Some("retrying"),
510 FlowEvent::StepFailed { .. } => Some("failed"),
511 FlowEvent::WaitCreated { .. } => Some("waiting"),
512 FlowEvent::WaitCompleted { .. } => Some("completed"),
513 FlowEvent::HookCreated { .. } => Some("active"),
514 FlowEvent::HookReceived { .. } => Some("received"),
515 FlowEvent::HookDisposed { .. } => Some("disposed"),
516 }
517}
518
519fn event_subject(event: &FlowEvent) -> Option<A3sFlowEventSubject> {
520 match event {
521 FlowEvent::StepCreated { step_id, .. }
522 | FlowEvent::StepStarted { step_id, .. }
523 | FlowEvent::StepCompleted { step_id, .. }
524 | FlowEvent::StepRetrying { step_id, .. }
525 | FlowEvent::StepFailed { step_id, .. } => Some(A3sFlowEventSubject {
526 kind: "step".to_string(),
527 id: step_id.clone(),
528 }),
529 FlowEvent::WaitCreated { wait_id, .. } | FlowEvent::WaitCompleted { wait_id } => {
530 Some(A3sFlowEventSubject {
531 kind: "wait".to_string(),
532 id: wait_id.clone(),
533 })
534 }
535 FlowEvent::HookCreated { hook_id, .. }
536 | FlowEvent::HookReceived { hook_id, .. }
537 | FlowEvent::HookDisposed { hook_id } => Some(A3sFlowEventSubject {
538 kind: "hook".to_string(),
539 id: hook_id.clone(),
540 }),
541 FlowEvent::RunCreated { .. }
542 | FlowEvent::RunStarted
543 | FlowEvent::RunCompleted { .. }
544 | FlowEvent::RunFailed { .. }
545 | FlowEvent::RunCancelled { .. } => None,
546 }
547}