Skip to main content

oximedia_workflow/
monitoring.rs

1//! Workflow monitoring and metrics.
2
3use crate::task::{TaskId, TaskState};
4use crate::workflow::{WorkflowId, WorkflowState};
5use chrono::{DateTime, Utc};
6use dashmap::DashMap;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use std::collections::HashMap;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::sync::Arc;
11use std::time::Duration;
12
13// ---------------------------------------------------------------------------
14// Serde helpers for Arc<AtomicU64>
15// ---------------------------------------------------------------------------
16
17fn serialize_atomic<S: Serializer>(val: &Arc<AtomicU64>, ser: S) -> Result<S::Ok, S::Error> {
18    ser.serialize_u64(val.load(Ordering::Relaxed))
19}
20
21fn deserialize_atomic<'de, D: Deserializer<'de>>(de: D) -> Result<Arc<AtomicU64>, D::Error> {
22    let n = u64::deserialize(de)?;
23    Ok(Arc::new(AtomicU64::new(n)))
24}
25
26fn default_atomic() -> Arc<AtomicU64> {
27    Arc::new(AtomicU64::new(0))
28}
29
30/// Task execution metrics.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TaskMetrics {
33    /// Task identifier.
34    pub task_id: TaskId,
35    /// Task name.
36    pub task_name: String,
37    /// Current state.
38    pub state: TaskState,
39    /// Start time.
40    pub start_time: Option<DateTime<Utc>>,
41    /// End time.
42    pub end_time: Option<DateTime<Utc>>,
43    /// Execution duration.
44    pub duration: Option<Duration>,
45    /// Number of retry attempts.
46    pub retry_count: u32,
47    /// Error message if failed.
48    pub error: Option<String>,
49}
50
51impl TaskMetrics {
52    /// Create new task metrics.
53    #[must_use]
54    pub fn new(task_id: TaskId, task_name: String) -> Self {
55        Self {
56            task_id,
57            task_name,
58            state: TaskState::Pending,
59            start_time: None,
60            end_time: None,
61            duration: None,
62            retry_count: 0,
63            error: None,
64        }
65    }
66
67    /// Mark task as started.
68    pub fn mark_started(&mut self) {
69        self.state = TaskState::Running;
70        self.start_time = Some(Utc::now());
71    }
72
73    /// Mark task as completed.
74    pub fn mark_completed(&mut self) {
75        self.state = TaskState::Completed;
76        self.end_time = Some(Utc::now());
77        if let Some(start) = self.start_time {
78            self.duration = Some(Duration::from_millis(
79                u64::try_from((Utc::now() - start).num_milliseconds()).unwrap_or(0),
80            ));
81        }
82    }
83
84    /// Mark task as failed.
85    pub fn mark_failed(&mut self, error: String) {
86        self.state = TaskState::Failed;
87        self.end_time = Some(Utc::now());
88        self.error = Some(error);
89        if let Some(start) = self.start_time {
90            self.duration = Some(Duration::from_millis(
91                u64::try_from((Utc::now() - start).num_milliseconds()).unwrap_or(0),
92            ));
93        }
94    }
95
96    /// Increment retry count.
97    pub fn increment_retry(&mut self) {
98        self.retry_count += 1;
99    }
100}
101
102/// Workflow execution metrics.
103///
104/// The `completed_tasks`, `failed_tasks`, and `running_tasks` counters are
105/// backed by `Arc<AtomicU64>` so they can be incremented/decremented from
106/// any thread without a mutable borrow. The `Arc` also allows sharing a
107/// counter across cheap clones (each clone shares the same atomic).
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct WorkflowMetrics {
110    /// Workflow identifier.
111    pub workflow_id: WorkflowId,
112    /// Workflow name.
113    pub workflow_name: String,
114    /// Current state.
115    pub state: WorkflowState,
116    /// Start time.
117    pub start_time: Option<DateTime<Utc>>,
118    /// End time.
119    pub end_time: Option<DateTime<Utc>>,
120    /// Total duration.
121    pub duration: Option<Duration>,
122    /// Task metrics.
123    pub tasks: HashMap<TaskId, TaskMetrics>,
124    /// Total task count.
125    pub total_tasks: usize,
126    /// Completed tasks count (atomic for lock-free concurrent updates).
127    #[serde(
128        serialize_with = "serialize_atomic",
129        deserialize_with = "deserialize_atomic",
130        default = "default_atomic"
131    )]
132    pub completed_tasks: Arc<AtomicU64>,
133    /// Failed tasks count (atomic for lock-free concurrent updates).
134    #[serde(
135        serialize_with = "serialize_atomic",
136        deserialize_with = "deserialize_atomic",
137        default = "default_atomic"
138    )]
139    pub failed_tasks: Arc<AtomicU64>,
140    /// Running tasks count (atomic for lock-free concurrent updates).
141    #[serde(
142        serialize_with = "serialize_atomic",
143        deserialize_with = "deserialize_atomic",
144        default = "default_atomic"
145    )]
146    pub running_tasks: Arc<AtomicU64>,
147}
148
149impl WorkflowMetrics {
150    /// Create new workflow metrics.
151    #[must_use]
152    pub fn new(workflow_id: WorkflowId, workflow_name: String, total_tasks: usize) -> Self {
153        Self {
154            workflow_id,
155            workflow_name,
156            state: WorkflowState::Created,
157            start_time: None,
158            end_time: None,
159            duration: None,
160            tasks: HashMap::new(),
161            total_tasks,
162            completed_tasks: Arc::new(AtomicU64::new(0)),
163            failed_tasks: Arc::new(AtomicU64::new(0)),
164            running_tasks: Arc::new(AtomicU64::new(0)),
165        }
166    }
167
168    /// Read the completed tasks count.
169    #[must_use]
170    pub fn completed_tasks_count(&self) -> u64 {
171        self.completed_tasks.load(Ordering::Relaxed)
172    }
173
174    /// Read the failed tasks count.
175    #[must_use]
176    pub fn failed_tasks_count(&self) -> u64 {
177        self.failed_tasks.load(Ordering::Relaxed)
178    }
179
180    /// Read the running tasks count.
181    #[must_use]
182    pub fn running_tasks_count(&self) -> u64 {
183        self.running_tasks.load(Ordering::Relaxed)
184    }
185
186    /// Mark workflow as started.
187    pub fn mark_started(&mut self) {
188        self.state = WorkflowState::Running;
189        self.start_time = Some(Utc::now());
190    }
191
192    /// Mark workflow as completed.
193    pub fn mark_completed(&mut self) {
194        self.state = WorkflowState::Completed;
195        self.end_time = Some(Utc::now());
196        if let Some(start) = self.start_time {
197            self.duration = Some(Duration::from_millis(
198                u64::try_from((Utc::now() - start).num_milliseconds()).unwrap_or(0),
199            ));
200        }
201    }
202
203    /// Mark workflow as failed.
204    pub fn mark_failed(&mut self) {
205        self.state = WorkflowState::Failed;
206        self.end_time = Some(Utc::now());
207        if let Some(start) = self.start_time {
208            self.duration = Some(Duration::from_millis(
209                u64::try_from((Utc::now() - start).num_milliseconds()).unwrap_or(0),
210            ));
211        }
212    }
213
214    /// Update task metrics.
215    pub fn update_task(&mut self, task_metrics: TaskMetrics) {
216        // Update counters based on state change
217        if let Some(old_metrics) = self.tasks.get(&task_metrics.task_id) {
218            self.update_counters(old_metrics.state, false);
219        }
220        self.update_counters(task_metrics.state, true);
221
222        self.tasks.insert(task_metrics.task_id, task_metrics);
223    }
224
225    fn update_counters(&self, state: TaskState, increment: bool) {
226        match state {
227            TaskState::Completed => {
228                if increment {
229                    self.completed_tasks.fetch_add(1, Ordering::Relaxed);
230                } else {
231                    // Saturating decrement — avoid underflow.
232                    let _ = self.completed_tasks.fetch_update(
233                        Ordering::Relaxed,
234                        Ordering::Relaxed,
235                        |v| if v > 0 { Some(v - 1) } else { Some(0) },
236                    );
237                }
238            }
239            TaskState::Failed => {
240                if increment {
241                    self.failed_tasks.fetch_add(1, Ordering::Relaxed);
242                } else {
243                    let _ =
244                        self.failed_tasks
245                            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
246                                if v > 0 {
247                                    Some(v - 1)
248                                } else {
249                                    Some(0)
250                                }
251                            });
252                }
253            }
254            TaskState::Running | TaskState::Retrying => {
255                if increment {
256                    self.running_tasks.fetch_add(1, Ordering::Relaxed);
257                } else {
258                    let _ = self.running_tasks.fetch_update(
259                        Ordering::Relaxed,
260                        Ordering::Relaxed,
261                        |v| if v > 0 { Some(v - 1) } else { Some(0) },
262                    );
263                }
264            }
265            _ => {}
266        }
267    }
268
269    /// Get progress percentage.
270    #[must_use]
271    pub fn progress_percentage(&self) -> f64 {
272        if self.total_tasks == 0 {
273            return 100.0;
274        }
275        self.completed_tasks.load(Ordering::Relaxed) as f64 / self.total_tasks as f64 * 100.0
276    }
277
278    /// Get average task duration.
279    #[must_use]
280    pub fn average_task_duration(&self) -> Option<Duration> {
281        let durations: Vec<_> = self.tasks.values().filter_map(|m| m.duration).collect();
282
283        if durations.is_empty() {
284            return None;
285        }
286
287        let total_ms: u64 = durations.iter().map(|d| d.as_millis() as u64).sum();
288        Some(Duration::from_millis(total_ms / durations.len() as u64))
289    }
290
291    /// Get throughput (tasks per second).
292    #[must_use]
293    pub fn throughput(&self) -> f64 {
294        let completed = self.completed_tasks.load(Ordering::Relaxed) as f64;
295        if let (Some(start), Some(end)) = (self.start_time, self.end_time) {
296            let duration_secs = (end - start).num_seconds().max(1);
297            completed / duration_secs as f64
298        } else if let Some(start) = self.start_time {
299            let duration_secs = (Utc::now() - start).num_seconds().max(1);
300            completed / duration_secs as f64
301        } else {
302            0.0
303        }
304    }
305}
306
307/// Monitoring service for tracking workflow execution.
308pub struct MonitoringService {
309    /// Active workflow metrics.
310    workflows: Arc<DashMap<WorkflowId, WorkflowMetrics>>,
311    /// Historical metrics (completed workflows).
312    history: Arc<DashMap<WorkflowId, WorkflowMetrics>>,
313}
314
315impl MonitoringService {
316    /// Create a new monitoring service.
317    #[must_use]
318    pub fn new() -> Self {
319        Self {
320            workflows: Arc::new(DashMap::new()),
321            history: Arc::new(DashMap::new()),
322        }
323    }
324
325    /// Start tracking a workflow.
326    pub fn start_workflow(
327        &self,
328        workflow_id: WorkflowId,
329        workflow_name: String,
330        total_tasks: usize,
331    ) {
332        let mut metrics = WorkflowMetrics::new(workflow_id, workflow_name, total_tasks);
333        metrics.mark_started();
334        self.workflows.insert(workflow_id, metrics);
335    }
336
337    /// Update task progress.
338    pub fn update_task(
339        &self,
340        workflow_id: WorkflowId,
341        task_id: TaskId,
342        task_name: String,
343        state: TaskState,
344        error: Option<String>,
345    ) {
346        if let Some(mut workflow_metrics) = self.workflows.get_mut(&workflow_id) {
347            let mut task_metrics = workflow_metrics
348                .tasks
349                .get(&task_id)
350                .cloned()
351                .unwrap_or_else(|| TaskMetrics::new(task_id, task_name.clone()));
352
353            match state {
354                TaskState::Running => task_metrics.mark_started(),
355                TaskState::Completed => task_metrics.mark_completed(),
356                TaskState::Failed => {
357                    if let Some(err) = error {
358                        task_metrics.mark_failed(err);
359                    } else {
360                        task_metrics.mark_failed("Unknown error".to_string());
361                    }
362                }
363                TaskState::Retrying => task_metrics.increment_retry(),
364                _ => task_metrics.state = state,
365            }
366
367            workflow_metrics.update_task(task_metrics);
368        }
369    }
370
371    /// Complete workflow tracking.
372    pub fn complete_workflow(&self, workflow_id: WorkflowId, success: bool) {
373        if let Some((_, mut metrics)) = self.workflows.remove(&workflow_id) {
374            if success {
375                metrics.mark_completed();
376            } else {
377                metrics.mark_failed();
378            }
379            self.history.insert(workflow_id, metrics);
380        }
381    }
382
383    /// Get workflow metrics.
384    #[must_use]
385    pub fn get_workflow_metrics(&self, workflow_id: &WorkflowId) -> Option<WorkflowMetrics> {
386        self.workflows
387            .get(workflow_id)
388            .map(|m| m.clone())
389            .or_else(|| self.history.get(workflow_id).map(|m| m.clone()))
390    }
391
392    /// Get all active workflow metrics.
393    #[must_use]
394    pub fn get_active_workflows(&self) -> Vec<WorkflowMetrics> {
395        self.workflows
396            .iter()
397            .map(|entry| entry.value().clone())
398            .collect()
399    }
400
401    /// Get workflow history.
402    #[must_use]
403    pub fn get_history(&self, limit: Option<usize>) -> Vec<WorkflowMetrics> {
404        let mut history: Vec<_> = self
405            .history
406            .iter()
407            .map(|entry| entry.value().clone())
408            .collect();
409
410        // Sort by start time descending
411        history.sort_by(|a, b| b.start_time.cmp(&a.start_time));
412
413        if let Some(limit) = limit {
414            history.truncate(limit);
415        }
416
417        history
418    }
419
420    /// Clear old history entries.
421    pub fn clear_history(&self, older_than: DateTime<Utc>) {
422        self.history.retain(|_, metrics| {
423            if let Some(end_time) = metrics.end_time {
424                end_time > older_than
425            } else {
426                true
427            }
428        });
429    }
430
431    /// Get system-wide statistics.
432    #[must_use]
433    pub fn get_statistics(&self) -> SystemStatistics {
434        let active_workflows = self.workflows.len();
435        let total_workflows = self.history.len() + active_workflows;
436
437        let completed_workflows = self
438            .history
439            .iter()
440            .filter(|entry| matches!(entry.value().state, WorkflowState::Completed))
441            .count();
442
443        let failed_workflows = self
444            .history
445            .iter()
446            .filter(|entry| matches!(entry.value().state, WorkflowState::Failed))
447            .count();
448
449        let total_tasks_completed: usize = (self
450            .workflows
451            .iter()
452            .map(|entry| entry.value().completed_tasks.load(Ordering::Relaxed))
453            .sum::<u64>()
454            + self
455                .history
456                .iter()
457                .map(|entry| entry.value().completed_tasks.load(Ordering::Relaxed))
458                .sum::<u64>()) as usize;
459
460        let total_tasks_failed: usize = (self
461            .workflows
462            .iter()
463            .map(|entry| entry.value().failed_tasks.load(Ordering::Relaxed))
464            .sum::<u64>()
465            + self
466                .history
467                .iter()
468                .map(|entry| entry.value().failed_tasks.load(Ordering::Relaxed))
469                .sum::<u64>()) as usize;
470
471        SystemStatistics {
472            active_workflows,
473            total_workflows,
474            completed_workflows,
475            failed_workflows,
476            total_tasks_completed,
477            total_tasks_failed,
478        }
479    }
480}
481
482impl Default for MonitoringService {
483    fn default() -> Self {
484        Self::new()
485    }
486}
487
488/// System-wide statistics.
489#[derive(Debug, Clone, Serialize, Deserialize)]
490pub struct SystemStatistics {
491    /// Number of active workflows.
492    pub active_workflows: usize,
493    /// Total workflows (active + historical).
494    pub total_workflows: usize,
495    /// Completed workflows.
496    pub completed_workflows: usize,
497    /// Failed workflows.
498    pub failed_workflows: usize,
499    /// Total tasks completed.
500    pub total_tasks_completed: usize,
501    /// Total tasks failed.
502    pub total_tasks_failed: usize,
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn test_task_metrics_creation() {
511        let task_id = TaskId::new();
512        let metrics = TaskMetrics::new(task_id, "test-task".to_string());
513        assert_eq!(metrics.task_id, task_id);
514        assert_eq!(metrics.task_name, "test-task");
515        assert_eq!(metrics.state, TaskState::Pending);
516    }
517
518    #[test]
519    fn test_task_metrics_lifecycle() {
520        let task_id = TaskId::new();
521        let mut metrics = TaskMetrics::new(task_id, "test-task".to_string());
522
523        metrics.mark_started();
524        assert_eq!(metrics.state, TaskState::Running);
525        assert!(metrics.start_time.is_some());
526
527        metrics.mark_completed();
528        assert_eq!(metrics.state, TaskState::Completed);
529        assert!(metrics.end_time.is_some());
530        assert!(metrics.duration.is_some());
531    }
532
533    #[test]
534    fn test_task_metrics_failure() {
535        let task_id = TaskId::new();
536        let mut metrics = TaskMetrics::new(task_id, "test-task".to_string());
537
538        metrics.mark_started();
539        metrics.mark_failed("Test error".to_string());
540
541        assert_eq!(metrics.state, TaskState::Failed);
542        assert_eq!(metrics.error, Some("Test error".to_string()));
543    }
544
545    #[test]
546    fn test_workflow_metrics_creation() {
547        let workflow_id = WorkflowId::new();
548        let metrics = WorkflowMetrics::new(workflow_id, "test-workflow".to_string(), 5);
549
550        assert_eq!(metrics.workflow_id, workflow_id);
551        assert_eq!(metrics.total_tasks, 5);
552        assert_eq!(metrics.completed_tasks_count(), 0);
553    }
554
555    #[test]
556    fn test_workflow_metrics_progress() {
557        let workflow_id = WorkflowId::new();
558        let metrics = WorkflowMetrics::new(workflow_id, "test-workflow".to_string(), 10);
559
560        assert_eq!(metrics.progress_percentage(), 0.0);
561
562        metrics.completed_tasks.store(5, Ordering::Relaxed);
563        assert_eq!(metrics.progress_percentage(), 50.0);
564
565        metrics.completed_tasks.store(10, Ordering::Relaxed);
566        assert_eq!(metrics.progress_percentage(), 100.0);
567    }
568
569    #[test]
570    fn test_monitoring_service_creation() {
571        let service = MonitoringService::new();
572        assert_eq!(service.get_active_workflows().len(), 0);
573    }
574
575    #[test]
576    fn test_monitoring_service_workflow_tracking() {
577        let service = MonitoringService::new();
578        let workflow_id = WorkflowId::new();
579
580        service.start_workflow(workflow_id, "test-workflow".to_string(), 3);
581
582        let metrics = service.get_workflow_metrics(&workflow_id);
583        assert!(metrics.is_some());
584        assert_eq!(
585            metrics.expect("should succeed in test").state,
586            WorkflowState::Running
587        );
588    }
589
590    #[test]
591    fn test_monitoring_service_task_updates() {
592        let service = MonitoringService::new();
593        let workflow_id = WorkflowId::new();
594        let task_id = TaskId::new();
595
596        service.start_workflow(workflow_id, "test-workflow".to_string(), 1);
597
598        service.update_task(
599            workflow_id,
600            task_id,
601            "task-1".to_string(),
602            TaskState::Running,
603            None,
604        );
605
606        let metrics = service
607            .get_workflow_metrics(&workflow_id)
608            .expect("should succeed in test");
609        assert_eq!(metrics.running_tasks_count(), 1);
610
611        service.update_task(
612            workflow_id,
613            task_id,
614            "task-1".to_string(),
615            TaskState::Completed,
616            None,
617        );
618
619        let metrics = service
620            .get_workflow_metrics(&workflow_id)
621            .expect("should succeed in test");
622        assert_eq!(metrics.completed_tasks_count(), 1);
623        assert_eq!(metrics.running_tasks_count(), 0);
624    }
625
626    #[test]
627    fn test_monitoring_service_completion() {
628        let service = MonitoringService::new();
629        let workflow_id = WorkflowId::new();
630
631        service.start_workflow(workflow_id, "test-workflow".to_string(), 1);
632        assert_eq!(service.get_active_workflows().len(), 1);
633
634        service.complete_workflow(workflow_id, true);
635        assert_eq!(service.get_active_workflows().len(), 0);
636
637        let history = service.get_history(None);
638        assert_eq!(history.len(), 1);
639        assert_eq!(history[0].state, WorkflowState::Completed);
640    }
641
642    #[test]
643    fn test_statistics() {
644        let service = MonitoringService::new();
645
646        let wf1 = WorkflowId::new();
647        let wf2 = WorkflowId::new();
648
649        service.start_workflow(wf1, "workflow1".to_string(), 2);
650        service.start_workflow(wf2, "workflow2".to_string(), 3);
651
652        service.complete_workflow(wf1, true);
653        service.complete_workflow(wf2, false);
654
655        let stats = service.get_statistics();
656        assert_eq!(stats.active_workflows, 0);
657        assert_eq!(stats.total_workflows, 2);
658        assert_eq!(stats.completed_workflows, 1);
659        assert_eq!(stats.failed_workflows, 1);
660    }
661}