Skip to main content

orbit_runtime/
metrics.rs

1//! Prometheus-style counters derived from committed events.
2
3use crate::model::Event;
4
5#[derive(Debug, Default, Clone)]
6pub struct Metrics {
7    pub executions_started: u64,
8    pub executions_completed: u64,
9    pub executions_failed: u64,
10    pub executions_cancelled: u64,
11    pub executions_timed_out: u64,
12    pub tasks_dispatched: u64,
13    pub tasks_succeeded: u64,
14    pub task_attempts_failed: u64,
15    pub task_attempts_uncertain: u64,
16    pub retries_scheduled: u64,
17    pub steps_skipped: u64,
18    pub stale_results: u64,
19    pub compensations_started: u64,
20    pub compensations_failed: u64,
21    pub events_committed: u64,
22    pub executions_continued_as_new: u64,
23    /// Schedule occurrences that started an execution (incl. triggers).
24    pub schedules_fired: u64,
25    /// Occurrences skipped because the previous run was still running.
26    pub schedules_skipped_overlap: u64,
27    /// Occurrences skipped because the server was down at the time.
28    pub schedules_skipped_missed: u64,
29}
30
31impl Metrics {
32    pub fn observe(&mut self, e: &Event) {
33        self.events_committed += 1;
34        match e {
35            // A new run (continue-as-new) is not a new execution.
36            Event::WorkflowCreated { run: None, .. } => self.executions_started += 1,
37            Event::WorkflowContinuedAsNew { .. } => self.executions_continued_as_new += 1,
38            Event::WorkflowCompleted { .. } => self.executions_completed += 1,
39            Event::WorkflowFailed { .. } => self.executions_failed += 1,
40            Event::WorkflowCancelled { .. } => self.executions_cancelled += 1,
41            Event::WorkflowTimedOut { .. } => self.executions_timed_out += 1,
42            Event::StepDispatched { .. } => self.tasks_dispatched += 1,
43            Event::StepSucceeded { .. } => self.tasks_succeeded += 1,
44            Event::StepAttemptFailed { .. } => self.task_attempts_failed += 1,
45            Event::StepAttemptUncertain { .. } => self.task_attempts_uncertain += 1,
46            Event::RetryScheduled { .. } => self.retries_scheduled += 1,
47            Event::StepSkipped { .. } => self.steps_skipped += 1,
48            Event::StaleResultIgnored { .. } => self.stale_results += 1,
49            Event::CompensationStarted { .. } => self.compensations_started += 1,
50            Event::CompensationFailed { .. } => self.compensations_failed += 1,
51            _ => {}
52        }
53    }
54
55    pub fn render(&self, active: usize, sessions: usize) -> String {
56        let counters = [
57            (
58                "orbit_executions_started_total",
59                "Executions started",
60                self.executions_started,
61            ),
62            (
63                "orbit_executions_completed_total",
64                "Executions completed",
65                self.executions_completed,
66            ),
67            (
68                "orbit_executions_failed_total",
69                "Executions failed",
70                self.executions_failed,
71            ),
72            (
73                "orbit_executions_cancelled_total",
74                "Executions cancelled",
75                self.executions_cancelled,
76            ),
77            (
78                "orbit_executions_timed_out_total",
79                "Executions timed out",
80                self.executions_timed_out,
81            ),
82            (
83                "orbit_tasks_dispatched_total",
84                "Task attempts dispatched",
85                self.tasks_dispatched,
86            ),
87            (
88                "orbit_tasks_succeeded_total",
89                "Task attempts succeeded",
90                self.tasks_succeeded,
91            ),
92            (
93                "orbit_task_attempts_failed_total",
94                "Task attempts failed",
95                self.task_attempts_failed,
96            ),
97            (
98                "orbit_task_attempts_uncertain_total",
99                "Task attempts with unknown outcome",
100                self.task_attempts_uncertain,
101            ),
102            (
103                "orbit_retries_scheduled_total",
104                "Retries scheduled",
105                self.retries_scheduled,
106            ),
107            (
108                "orbit_steps_skipped_total",
109                "Steps skipped by conditions",
110                self.steps_skipped,
111            ),
112            (
113                "orbit_stale_results_total",
114                "Duplicate or late results ignored",
115                self.stale_results,
116            ),
117            (
118                "orbit_compensations_started_total",
119                "Compensations started",
120                self.compensations_started,
121            ),
122            (
123                "orbit_compensations_failed_total",
124                "Compensations failed",
125                self.compensations_failed,
126            ),
127            (
128                "orbit_events_committed_total",
129                "History events committed",
130                self.events_committed,
131            ),
132            (
133                "orbit_executions_continued_as_new_total",
134                "Runs ended by continue_as_new",
135                self.executions_continued_as_new,
136            ),
137            (
138                "orbit_schedules_fired_total",
139                "Schedule occurrences that started an execution",
140                self.schedules_fired,
141            ),
142        ];
143        let mut out = String::new();
144        for (name, help, value) in counters {
145            out.push_str(&format!(
146                "# HELP {name} {help}\n# TYPE {name} counter\n{name} {value}\n"
147            ));
148        }
149        out.push_str(&format!(
150            "# HELP orbit_schedules_skipped_total Schedule occurrences that started nothing\n# TYPE orbit_schedules_skipped_total counter\norbit_schedules_skipped_total{{reason=\"overlap\"}} {}\norbit_schedules_skipped_total{{reason=\"missed\"}} {}\n",
151            self.schedules_skipped_overlap, self.schedules_skipped_missed
152        ));
153        out.push_str(&format!(
154            "# HELP orbit_active_executions Executions not yet finished\n# TYPE orbit_active_executions gauge\norbit_active_executions {active}\n"
155        ));
156        out.push_str(&format!(
157            "# HELP orbit_worker_sessions Registered worker sessions\n# TYPE orbit_worker_sessions gauge\norbit_worker_sessions {sessions}\n"
158        ));
159        out
160    }
161}