Skip to main content

confium_coordinator/coordinator/
metrics.rs

1//! Coordinator metrics — Prometheus-compatible counters and gauges.
2//!
3//! Tracks session lifecycle events and operational health. Exposed via
4//! the TCP protocol (`MetricsQuery` / `MetricsResponse`) and renderable
5//! in Prometheus text format.
6
7use std::sync::atomic::{AtomicU64, Ordering};
8
9/// Coordinator-level metrics. All fields are atomic for thread-safe
10/// increments from the coordinator's connection handler threads.
11#[derive(Debug, Default)]
12pub struct CoordinatorMetrics {
13    /// Total sessions created since coordinator start.
14    sessions_created: AtomicU64,
15    /// Total sessions completed (aggregated successfully).
16    sessions_completed: AtomicU64,
17    /// Total sessions expired (unlock window elapsed).
18    sessions_expired: AtomicU64,
19    /// Total sessions aborted.
20    sessions_aborted: AtomicU64,
21    /// Total signature aggregations attempted.
22    aggregations_attempted: AtomicU64,
23    /// Total signature aggregations that failed.
24    aggregations_failed: AtomicU64,
25    /// Currently active sessions.
26    active_sessions: AtomicU64,
27    /// Currently registered signers.
28    registered_signers: AtomicU64,
29    /// Total bytes of messages processed.
30    bytes_processed: AtomicU64,
31}
32
33impl CoordinatorMetrics {
34    /// Record a session creation.
35    pub fn record_session_created(&self) {
36        self.sessions_created.fetch_add(1, Ordering::Relaxed);
37        self.active_sessions.fetch_add(1, Ordering::Relaxed);
38    }
39
40    /// Record a session completion.
41    pub fn record_session_completed(&self) {
42        self.sessions_completed.fetch_add(1, Ordering::Relaxed);
43        self.active_sessions.fetch_sub(1, Ordering::Relaxed);
44    }
45
46    /// Record a session expiration.
47    pub fn record_session_expired(&self) {
48        self.sessions_expired.fetch_add(1, Ordering::Relaxed);
49        self.active_sessions.fetch_sub(1, Ordering::Relaxed);
50    }
51
52    /// Record a session abort.
53    pub fn record_session_aborted(&self) {
54        self.sessions_aborted.fetch_add(1, Ordering::Relaxed);
55        self.active_sessions.fetch_sub(1, Ordering::Relaxed);
56    }
57
58    /// Record an aggregation attempt.
59    pub fn record_aggregation_attempted(&self) {
60        self.aggregations_attempted.fetch_add(1, Ordering::Relaxed);
61    }
62
63    /// Record a failed aggregation.
64    pub fn record_aggregation_failed(&self) {
65        self.aggregations_failed.fetch_add(1, Ordering::Relaxed);
66    }
67
68    /// Set the registered signer count.
69    pub fn set_registered_signers(&self, count: u64) {
70        self.registered_signers.store(count, Ordering::Relaxed);
71    }
72
73    /// Record bytes processed.
74    pub fn record_bytes(&self, bytes: u64) {
75        self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
76    }
77
78    /// Render metrics in Prometheus text exposition format.
79    pub fn render_prometheus(&self) -> String {
80        let mut out = String::new();
81        out.push_str("# HELP confium_sessions_created_total Total sessions created.\n");
82        out.push_str("# TYPE confium_sessions_created_total counter\n");
83        out.push_str(&format!(
84            "confium_sessions_created_total {}\n",
85            self.sessions_created.load(Ordering::Relaxed)
86        ));
87
88        out.push_str("# HELP confium_sessions_completed_total Total sessions completed.\n");
89        out.push_str("# TYPE confium_sessions_completed_total counter\n");
90        out.push_str(&format!(
91            "confium_sessions_completed_total {}\n",
92            self.sessions_completed.load(Ordering::Relaxed)
93        ));
94
95        out.push_str("# HELP confium_sessions_expired_total Total sessions expired.\n");
96        out.push_str("# TYPE confium_sessions_expired_total counter\n");
97        out.push_str(&format!(
98            "confium_sessions_expired_total {}\n",
99            self.sessions_expired.load(Ordering::Relaxed)
100        ));
101
102        out.push_str("# HELP confium_sessions_aborted_total Total sessions aborted.\n");
103        out.push_str("# TYPE confium_sessions_aborted_total counter\n");
104        out.push_str(&format!(
105            "confium_sessions_aborted_total {}\n",
106            self.sessions_aborted.load(Ordering::Relaxed)
107        ));
108
109        out.push_str("# HELP confium_aggregations_attempted_total Total aggregation attempts.\n");
110        out.push_str("# TYPE confium_aggregations_attempted_total counter\n");
111        out.push_str(&format!(
112            "confium_aggregations_attempted_total {}\n",
113            self.aggregations_attempted.load(Ordering::Relaxed)
114        ));
115
116        out.push_str("# HELP confium_aggregations_failed_total Total failed aggregations.\n");
117        out.push_str("# TYPE confium_aggregations_failed_total counter\n");
118        out.push_str(&format!(
119            "confium_aggregations_failed_total {}\n",
120            self.aggregations_failed.load(Ordering::Relaxed)
121        ));
122
123        out.push_str("# HELP confium_active_sessions Currently active sessions.\n");
124        out.push_str("# TYPE confium_active_sessions gauge\n");
125        out.push_str(&format!(
126            "confium_active_sessions {}\n",
127            self.active_sessions.load(Ordering::Relaxed)
128        ));
129
130        out.push_str("# HELP confium_registered_signers Currently registered signers.\n");
131        out.push_str("# TYPE confium_registered_signers gauge\n");
132        out.push_str(&format!(
133            "confium_registered_signers {}\n",
134            self.registered_signers.load(Ordering::Relaxed)
135        ));
136
137        out.push_str("# HELP confium_bytes_processed_total Total bytes processed.\n");
138        out.push_str("# TYPE confium_bytes_processed_total counter\n");
139        out.push_str(&format!(
140            "confium_bytes_processed_total {}\n",
141            self.bytes_processed.load(Ordering::Relaxed)
142        ));
143
144        out
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn empty_metrics_render_correctly() {
154        let m = CoordinatorMetrics::default();
155        let text = m.render_prometheus();
156        assert!(text.contains("confium_sessions_created_total 0"));
157        assert!(text.contains("confium_active_sessions 0"));
158    }
159
160    #[test]
161    fn session_lifecycle_increments() {
162        let m = CoordinatorMetrics::default();
163        m.record_session_created();
164        m.record_session_created();
165        m.record_session_completed();
166        m.record_session_expired();
167
168        let text = m.render_prometheus();
169        assert!(text.contains("confium_sessions_created_total 2"));
170        assert!(text.contains("confium_sessions_completed_total 1"));
171        assert!(text.contains("confium_sessions_expired_total 1"));
172        assert!(text.contains("confium_active_sessions 0"));
173    }
174
175    #[test]
176    fn aggregation_metrics() {
177        let m = CoordinatorMetrics::default();
178        m.record_aggregation_attempted();
179        m.record_aggregation_attempted();
180        m.record_aggregation_failed();
181
182        let text = m.render_prometheus();
183        assert!(text.contains("confium_aggregations_attempted_total 2"));
184        assert!(text.contains("confium_aggregations_failed_total 1"));
185    }
186
187    #[test]
188    fn prometheus_format_has_help_and_type() {
189        let m = CoordinatorMetrics::default();
190        let text = m.render_prometheus();
191        assert!(text.contains("# HELP"));
192        assert!(text.contains("# TYPE"));
193        assert!(text.contains("counter"));
194        assert!(text.contains("gauge"));
195    }
196
197    #[test]
198    fn registered_signers_gauge() {
199        let m = CoordinatorMetrics::default();
200        m.set_registered_signers(5);
201        let text = m.render_prometheus();
202        assert!(text.contains("confium_registered_signers 5"));
203    }
204
205    #[test]
206    fn bytes_processed_accumulates() {
207        let m = CoordinatorMetrics::default();
208        m.record_bytes(100);
209        m.record_bytes(250);
210        let text = m.render_prometheus();
211        assert!(text.contains("confium_bytes_processed_total 350"));
212    }
213}