1use async_trait::async_trait;
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4use std::collections::{BTreeMap, HashMap};
5use std::fmt;
6use std::sync::Arc;
7use tokio::sync::Mutex;
8use uuid::Uuid;
9
10use crate::model::{FlowEvent, FlowEventEnvelope, WorkflowSpec};
11
12mod local_file;
13
14pub use local_file::LocalFileA3sFlowEventSink;
15
16#[async_trait]
21pub trait FlowEventObserver: Send + Sync {
22 async fn observe(&self, envelope: FlowEventEnvelope);
24}
25
26#[derive(Debug, Default)]
28pub struct NoopFlowEventObserver;
29
30#[async_trait]
31impl FlowEventObserver for NoopFlowEventObserver {
32 async fn observe(&self, _envelope: FlowEventEnvelope) {}
33}
34
35#[derive(Clone, Default)]
37pub struct FanoutFlowEventObserver {
38 observers: Vec<Arc<dyn FlowEventObserver>>,
39}
40
41impl FanoutFlowEventObserver {
42 pub fn new() -> Self {
44 Self::default()
45 }
46
47 pub fn from_observers(observers: Vec<Arc<dyn FlowEventObserver>>) -> Self {
49 Self { observers }
50 }
51
52 pub fn with_observer<O>(mut self, observer: Arc<O>) -> Self
54 where
55 O: FlowEventObserver + 'static,
56 {
57 self.observers.push(observer);
58 self
59 }
60
61 pub fn with_dyn_observer(mut self, observer: Arc<dyn FlowEventObserver>) -> Self {
63 self.observers.push(observer);
64 self
65 }
66
67 pub fn len(&self) -> usize {
69 self.observers.len()
70 }
71
72 pub fn is_empty(&self) -> bool {
74 self.observers.is_empty()
75 }
76}
77
78impl fmt::Debug for FanoutFlowEventObserver {
79 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80 formatter
81 .debug_struct("FanoutFlowEventObserver")
82 .field("observers", &self.observers.len())
83 .finish()
84 }
85}
86
87#[async_trait]
88impl FlowEventObserver for FanoutFlowEventObserver {
89 async fn observe(&self, envelope: FlowEventEnvelope) {
90 for observer in &self.observers {
91 observer.observe(envelope.clone()).await;
92 }
93 }
94}
95
96#[derive(Debug, Default)]
98pub struct InMemoryFlowEventObserver {
99 events: Mutex<Vec<FlowEventEnvelope>>,
100}
101
102impl InMemoryFlowEventObserver {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 pub async fn events(&self) -> Vec<FlowEventEnvelope> {
110 self.events.lock().await.clone()
111 }
112
113 pub async fn event_keys(&self) -> Vec<&'static str> {
115 self.events
116 .lock()
117 .await
118 .iter()
119 .map(|event| event.event.event_key())
120 .collect()
121 }
122}
123
124#[async_trait]
125impl FlowEventObserver for InMemoryFlowEventObserver {
126 async fn observe(&self, envelope: FlowEventEnvelope) {
127 self.events.lock().await.push(envelope);
128 }
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
133#[non_exhaustive]
134pub struct FlowWorkflowIdentity {
135 pub name: String,
137 pub version: String,
139}
140
141impl FlowWorkflowIdentity {
142 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
144 Self {
145 name: name.into(),
146 version: version.into(),
147 }
148 }
149}
150
151impl From<&WorkflowSpec> for FlowWorkflowIdentity {
152 fn from(spec: &WorkflowSpec) -> Self {
153 Self::new(spec.name.clone(), spec.version.clone())
154 }
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
159#[non_exhaustive]
160pub struct A3sFlowEventSubject {
161 pub kind: String,
163 pub id: String,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
173#[non_exhaustive]
174pub struct A3sFlowEvent {
175 pub key: String,
177 pub run_id: String,
179 pub sequence: u64,
181 pub event_id: Uuid,
183 pub timestamp: DateTime<Utc>,
185 pub workflow: Option<FlowWorkflowIdentity>,
187 pub status: Option<String>,
189 pub subject: Option<A3sFlowEventSubject>,
191}
192
193impl A3sFlowEvent {
194 pub fn from_envelope(
196 envelope: &FlowEventEnvelope,
197 workflow: Option<FlowWorkflowIdentity>,
198 ) -> Self {
199 Self {
200 key: envelope.event.event_key().to_string(),
201 run_id: envelope.run_id.clone(),
202 sequence: envelope.sequence,
203 event_id: envelope.event_id,
204 timestamp: envelope.timestamp,
205 workflow,
206 status: event_status(&envelope.event).map(str::to_string),
207 subject: event_subject(&envelope.event),
208 }
209 }
210
211 pub fn safe_metric_labels(&self) -> BTreeMap<String, String> {
213 let mut labels = BTreeMap::new();
214 labels.insert("event_key".to_string(), self.key.clone());
215 if let Some(workflow) = &self.workflow {
216 labels.insert("workflow_name".to_string(), workflow.name.clone());
217 labels.insert("workflow_version".to_string(), workflow.version.clone());
218 }
219 if let Some(status) = &self.status {
220 labels.insert("status".to_string(), status.clone());
221 }
222 labels
223 }
224}
225
226#[cfg(feature = "a3s-event")]
227pub struct A3sEventBusFlowEventSink {
234 bus: Arc<a3s_event::EventBus>,
235 category: String,
236 source: String,
237 last_error: Mutex<Option<String>>,
238}
239
240#[cfg(feature = "a3s-event")]
241impl A3sEventBusFlowEventSink {
242 pub fn new(bus: Arc<a3s_event::EventBus>) -> Self {
244 Self {
245 bus,
246 category: "flow".to_string(),
247 source: "a3s-flow".to_string(),
248 last_error: Mutex::new(None),
249 }
250 }
251
252 pub fn with_category(mut self, category: impl Into<String>) -> Self {
254 self.category = category.into();
255 self
256 }
257
258 pub fn with_source(mut self, source: impl Into<String>) -> Self {
260 self.source = source.into();
261 self
262 }
263
264 pub fn bus(&self) -> Arc<a3s_event::EventBus> {
266 Arc::clone(&self.bus)
267 }
268
269 pub fn category(&self) -> &str {
271 &self.category
272 }
273
274 pub fn source(&self) -> &str {
276 &self.source
277 }
278
279 pub async fn last_error(&self) -> Option<String> {
281 self.last_error.lock().await.clone()
282 }
283
284 pub fn to_a3s_event(
286 &self,
287 event: &A3sFlowEvent,
288 ) -> std::result::Result<a3s_event::Event, serde_json::Error> {
289 let topic = flow_event_topic(&event.key);
290 let subject = self.bus.provider_arc().build_subject(&self.category, topic);
291 let timestamp = event.timestamp.timestamp_millis();
292 let mut metadata = HashMap::new();
293 metadata.insert("flow.event_key".to_string(), event.key.clone());
294 metadata.insert("flow.run_id".to_string(), event.run_id.clone());
295 metadata.insert("flow.sequence".to_string(), event.sequence.to_string());
296 metadata.insert("flow.event_id".to_string(), event.event_id.to_string());
297 if let Some(status) = &event.status {
298 metadata.insert("flow.status".to_string(), status.clone());
299 }
300 if let Some(workflow) = &event.workflow {
301 metadata.insert("flow.workflow_name".to_string(), workflow.name.clone());
302 metadata.insert(
303 "flow.workflow_version".to_string(),
304 workflow.version.clone(),
305 );
306 }
307 if let Some(subject) = &event.subject {
308 metadata.insert("flow.subject_kind".to_string(), subject.kind.clone());
309 metadata.insert("flow.subject_id".to_string(), subject.id.clone());
310 }
311
312 Ok(a3s_event::Event {
313 id: format!("evt-{}", event.event_id),
314 subject,
315 category: self.category.clone(),
316 event_type: event.key.clone(),
317 version: 1,
318 payload: serde_json::to_value(event)?,
319 summary: format!("{} for run {}", event.key, event.run_id),
320 source: self.source.clone(),
321 timestamp: timestamp.max(0) as u64,
322 metadata,
323 })
324 }
325}
326
327#[cfg(feature = "a3s-event")]
328impl fmt::Debug for A3sEventBusFlowEventSink {
329 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
330 formatter
331 .debug_struct("A3sEventBusFlowEventSink")
332 .field("category", &self.category)
333 .field("source", &self.source)
334 .finish_non_exhaustive()
335 }
336}
337
338#[cfg(feature = "a3s-event")]
339#[async_trait]
340impl A3sFlowEventSink for A3sEventBusFlowEventSink {
341 async fn emit(&self, event: A3sFlowEvent) {
342 let a3s_event = match self.to_a3s_event(&event) {
343 Ok(event) => event,
344 Err(err) => {
345 let message = err.to_string();
346 tracing::warn!(
347 error = %message,
348 event_key = %event.key,
349 run_id = %event.run_id,
350 "failed to convert flow event for A3S Event"
351 );
352 *self.last_error.lock().await = Some(message);
353 return;
354 }
355 };
356
357 match self.bus.publish_event(&a3s_event).await {
358 Ok(_) => {
359 *self.last_error.lock().await = None;
360 }
361 Err(err) => {
362 let message = err.to_string();
363 tracing::warn!(
364 error = %message,
365 subject = %a3s_event.subject,
366 event_type = %a3s_event.event_type,
367 "failed to publish flow event to A3S Event"
368 );
369 *self.last_error.lock().await = Some(message);
370 }
371 }
372 }
373}
374
375#[async_trait]
377pub trait A3sFlowEventSink: Send + Sync {
378 async fn emit(&self, event: A3sFlowEvent);
380}
381
382#[derive(Debug)]
384pub struct A3sFlowEventBridge<S> {
385 sink: Arc<S>,
386 workflows: Mutex<HashMap<String, FlowWorkflowIdentity>>,
387}
388
389impl<S> A3sFlowEventBridge<S>
390where
391 S: A3sFlowEventSink,
392{
393 pub fn new(sink: Arc<S>) -> Self {
395 Self {
396 sink,
397 workflows: Mutex::new(HashMap::new()),
398 }
399 }
400
401 pub fn sink(&self) -> Arc<S> {
403 Arc::clone(&self.sink)
404 }
405}
406
407#[async_trait]
408impl<S> FlowEventObserver for A3sFlowEventBridge<S>
409where
410 S: A3sFlowEventSink,
411{
412 async fn observe(&self, envelope: FlowEventEnvelope) {
413 let workflow = {
414 let mut workflows = self.workflows.lock().await;
415 if let FlowEvent::RunCreated { spec, .. } = &envelope.event {
416 workflows.insert(envelope.run_id.clone(), FlowWorkflowIdentity::from(spec));
417 }
418 workflows.get(&envelope.run_id).cloned()
419 };
420 self.sink
421 .emit(A3sFlowEvent::from_envelope(&envelope, workflow))
422 .await;
423 }
424}
425
426#[derive(Debug, Default)]
428pub struct InMemoryA3sFlowEventSink {
429 events: Mutex<Vec<A3sFlowEvent>>,
430}
431
432impl InMemoryA3sFlowEventSink {
433 pub fn new() -> Self {
435 Self::default()
436 }
437
438 pub async fn events(&self) -> Vec<A3sFlowEvent> {
440 self.events.lock().await.clone()
441 }
442}
443
444#[async_trait]
445impl A3sFlowEventSink for InMemoryA3sFlowEventSink {
446 async fn emit(&self, event: A3sFlowEvent) {
447 self.events.lock().await.push(event);
448 }
449}
450
451#[cfg(feature = "a3s-event")]
452fn flow_event_topic(key: &str) -> &str {
453 key.strip_prefix("flow.").unwrap_or(key)
454}
455
456fn event_status(event: &FlowEvent) -> Option<&'static str> {
457 match event {
458 FlowEvent::RunCreated { .. } => Some("pending"),
459 FlowEvent::RunStarted => Some("running"),
460 FlowEvent::RunCompleted { .. } => Some("completed"),
461 FlowEvent::RunFailed { .. } => Some("failed"),
462 FlowEvent::RunCancellationRequested { .. } => Some("cancelling"),
463 FlowEvent::RunCancelled { .. } => Some("cancelled"),
464 FlowEvent::RunTimedOut { .. } => Some("timed_out"),
465 FlowEvent::RunRetryExhausted { .. } => Some("retry_exhausted"),
466 FlowEvent::RunHostShutdown { .. } => Some("host_shutdown"),
467 FlowEvent::RunContinuedAsNew { .. } => Some("continued_as_new"),
468 FlowEvent::RunProgressRecorded { .. } => Some("recorded"),
469 FlowEvent::ChildOperationLinked { .. } => Some("linked"),
470 FlowEvent::ChildWorkflowRequested { .. } => Some("requested"),
471 FlowEvent::ChildWorkflowResolved { .. } => Some("resolved"),
472 FlowEvent::SignalReceived { .. } => Some("received"),
473 FlowEvent::SignalWaitCreated { .. } => Some("waiting"),
474 FlowEvent::SignalWaitCompleted { .. } => Some("completed"),
475 FlowEvent::StepCreated { .. } => Some("pending"),
476 FlowEvent::StepStarted { .. } => Some("running"),
477 FlowEvent::StepCompleted { .. } => Some("completed"),
478 FlowEvent::StepRetrying { .. } => Some("retrying"),
479 FlowEvent::StepFailed { .. } => Some("failed"),
480 FlowEvent::WaitCreated { .. } => Some("waiting"),
481 FlowEvent::WaitCompleted { .. } => Some("completed"),
482 FlowEvent::HookCreated { .. } => Some("active"),
483 FlowEvent::HookReceived { .. } => Some("received"),
484 FlowEvent::HookDisposed { .. } => Some("disposed"),
485 }
486}
487
488fn event_subject(event: &FlowEvent) -> Option<A3sFlowEventSubject> {
489 match event {
490 FlowEvent::StepCreated { step_id, .. }
491 | FlowEvent::StepStarted { step_id, .. }
492 | FlowEvent::StepCompleted { step_id, .. }
493 | FlowEvent::StepRetrying { step_id, .. }
494 | FlowEvent::StepFailed { step_id, .. }
495 | FlowEvent::RunRetryExhausted { step_id, .. } => Some(A3sFlowEventSubject {
496 kind: "step".to_string(),
497 id: step_id.clone(),
498 }),
499 FlowEvent::RunProgressRecorded { progress } => Some(A3sFlowEventSubject {
500 kind: "progress".to_string(),
501 id: progress.progress_id.clone(),
502 }),
503 FlowEvent::ChildOperationLinked { child } => Some(A3sFlowEventSubject {
504 kind: "child_operation".to_string(),
505 id: child.reference_id.clone(),
506 }),
507 FlowEvent::ChildWorkflowRequested { child_id, .. }
508 | FlowEvent::ChildWorkflowResolved { child_id, .. } => Some(A3sFlowEventSubject {
509 kind: "child_workflow".to_string(),
510 id: child_id.clone(),
511 }),
512 FlowEvent::SignalReceived { signal } => Some(A3sFlowEventSubject {
513 kind: "signal".to_string(),
514 id: signal.signal_id.clone(),
515 }),
516 FlowEvent::SignalWaitCreated { wait_id, .. }
517 | FlowEvent::SignalWaitCompleted { wait_id, .. } => Some(A3sFlowEventSubject {
518 kind: "signal_wait".to_string(),
519 id: wait_id.clone(),
520 }),
521 FlowEvent::WaitCreated { wait_id, .. } | FlowEvent::WaitCompleted { wait_id } => {
522 Some(A3sFlowEventSubject {
523 kind: "wait".to_string(),
524 id: wait_id.clone(),
525 })
526 }
527 FlowEvent::HookCreated { hook_id, .. }
528 | FlowEvent::HookReceived { hook_id, .. }
529 | FlowEvent::HookDisposed { hook_id } => Some(A3sFlowEventSubject {
530 kind: "hook".to_string(),
531 id: hook_id.clone(),
532 }),
533 FlowEvent::RunCreated { .. }
534 | FlowEvent::RunStarted
535 | FlowEvent::RunCompleted { .. }
536 | FlowEvent::RunFailed { .. }
537 | FlowEvent::RunCancellationRequested { .. }
538 | FlowEvent::RunCancelled { .. }
539 | FlowEvent::RunContinuedAsNew { .. } => None,
540 FlowEvent::RunTimedOut { .. } | FlowEvent::RunHostShutdown { .. } => None,
541 }
542}