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