Skip to main content

metrics/
metrics.rs

1//! Observability hooks.
2//!
3//! Wraps `Runner` to capture lifecycle metrics (transition counts,
4//! per-mode entry counts, time in each mode) without any external
5//! dependency. Adapt this pattern for `tracing`, `prometheus`, etc.
6
7#![allow(clippy::print_stdout, clippy::enum_glob_use)]
8
9use std::collections::HashMap;
10use std::time::Instant;
11
12use ready_active_safe::prelude::*;
13
14#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15enum Mode {
16    Ready,
17    Active,
18    Safe,
19}
20
21impl core::fmt::Display for Mode {
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        write!(f, "{self:?}")
24    }
25}
26
27#[derive(Debug)]
28enum Event {
29    Start,
30    Fault,
31    Recover,
32}
33
34#[derive(Debug)]
35enum Command {
36    Initialize,
37    EmergencyStop,
38    Reset,
39}
40
41struct System;
42
43impl Machine for System {
44    type Mode = Mode;
45    type Event = Event;
46    type Command = Command;
47
48    fn initial_mode(&self) -> Mode {
49        Mode::Ready
50    }
51
52    fn on_event(&self, mode: &Mode, event: &Event) -> Decision<Mode, Command> {
53        use Command::*;
54        use Event::*;
55        use Mode::*;
56
57        match (mode, event) {
58            (Ready, Start) => transition(Active).emit(Initialize),
59            (Active, Fault) => transition(Safe).emit(EmergencyStop),
60            (Safe, Recover) => transition(Ready).emit(Reset),
61            _ => ignore(),
62        }
63    }
64}
65
66/// Wraps a `Runner` to record lifecycle metrics.
67struct MetricsRunner<'a, M: Machine> {
68    runner: Runner<'a, M>,
69    transition_count: u64,
70    commands_dispatched: u64,
71    mode_entries: HashMap<Mode, u64>,
72    mode_entered_at: Instant,
73    mode_durations: HashMap<Mode, std::time::Duration>,
74}
75
76impl<'a> MetricsRunner<'a, System> {
77    fn new(runner: Runner<'a, System>) -> Self {
78        let initial = runner.mode().clone();
79        let mut mode_entries = HashMap::new();
80        *mode_entries.entry(initial).or_insert(0) += 1;
81
82        Self {
83            runner,
84            transition_count: 0,
85            commands_dispatched: 0,
86            mode_entries,
87            mode_entered_at: Instant::now(),
88            mode_durations: HashMap::new(),
89        }
90    }
91
92    fn feed(&mut self, event: &Event) {
93        let before = self.runner.mode().clone();
94        let commands = self.runner.feed(event);
95
96        let after = self.runner.mode().clone();
97
98        self.commands_dispatched += commands.len() as u64;
99
100        if before != after {
101            self.transition_count += 1;
102
103            let elapsed = self.mode_entered_at.elapsed();
104            *self.mode_durations.entry(before).or_default() += elapsed;
105            self.mode_entered_at = Instant::now();
106
107            *self.mode_entries.entry(after.clone()).or_insert(0) += 1;
108            println!("  [metrics] transition to {after:?}");
109        }
110
111        for cmd in &commands {
112            println!("  [metrics] dispatch: {cmd:?}");
113        }
114    }
115
116    fn print_summary(&self) {
117        println!("\n=== Metrics Summary ===");
118        println!("Transitions:        {}", self.transition_count);
119        println!("Commands dispatched: {}", self.commands_dispatched);
120
121        println!("Mode entries:");
122        for (mode, count) in &self.mode_entries {
123            println!("  {mode:?}: {count}");
124        }
125
126        println!("Time in mode:");
127        for (mode, dur) in &self.mode_durations {
128            println!("  {mode:?}: {dur:?}");
129        }
130
131        println!("Current mode: {:?}", self.runner.mode());
132    }
133}
134
135fn main() {
136    let system = System;
137    let runner = Runner::new(&system);
138    let mut metrics = MetricsRunner::new(runner);
139
140    let events = [
141        Event::Start,
142        Event::Fault,
143        Event::Recover,
144        Event::Start,
145        Event::Fault,
146        Event::Recover,
147    ];
148
149    for event in &events {
150        println!("Event: {event:?}");
151        metrics.feed(event);
152    }
153
154    metrics.print_summary();
155
156    assert_eq!(metrics.transition_count, 6);
157    assert_eq!(metrics.commands_dispatched, 6);
158}