Skip to main content

confium_coordinator/coordinator/
connection_stats.rs

1//! Connection statistics — per-signer operational telemetry.
2//!
3//! Tracks connect time, message counts, bytes transferred, last
4//! activity, and error count for each connected signer. Queryable for
5//! monitoring dashboards and debugging.
6
7use chrono::{DateTime, Utc};
8use std::collections::HashMap;
9use std::sync::Mutex;
10
11/// Statistics for a single signer connection.
12#[derive(Debug, Clone)]
13pub struct ConnectionStats {
14    /// Signer identity.
15    pub signer_id: String,
16    /// When the connection was established.
17    pub connected_at: DateTime<Utc>,
18    /// Total messages received from this signer.
19    pub messages_received: u64,
20    /// Total messages sent to this signer.
21    pub messages_sent: u64,
22    /// Total bytes received.
23    pub bytes_received: u64,
24    /// Total bytes sent.
25    pub bytes_sent: u64,
26    /// Number of errors encountered.
27    pub error_count: u64,
28    /// Last activity timestamp.
29    pub last_activity: DateTime<Utc>,
30}
31
32impl ConnectionStats {
33    /// Create a new stats entry for a signer, connected now.
34    pub fn new(signer_id: &str) -> Self {
35        let now = Utc::now();
36        Self {
37            signer_id: signer_id.into(),
38            connected_at: now,
39            messages_received: 0,
40            messages_sent: 0,
41            bytes_received: 0,
42            bytes_sent: 0,
43            error_count: 0,
44            last_activity: now,
45        }
46    }
47
48    /// Record a received message.
49    pub fn record_received(&mut self, bytes: u64) {
50        self.messages_received += 1;
51        self.bytes_received += bytes;
52        self.last_activity = Utc::now();
53    }
54
55    /// Record a sent message.
56    pub fn record_sent(&mut self, bytes: u64) {
57        self.messages_sent += 1;
58        self.bytes_sent += bytes;
59        self.last_activity = Utc::now();
60    }
61
62    /// Record an error.
63    pub fn record_error(&mut self) {
64        self.error_count += 1;
65        self.last_activity = Utc::now();
66    }
67
68    /// Connection duration in seconds.
69    pub fn connection_duration_secs(&self) -> i64 {
70        (Utc::now() - self.connected_at).num_seconds()
71    }
72
73    /// Total messages (sent + received).
74    pub fn total_messages(&self) -> u64 {
75        self.messages_received + self.messages_sent
76    }
77
78    /// Total bytes transferred.
79    pub fn total_bytes(&self) -> u64 {
80        self.bytes_received + self.bytes_sent
81    }
82}
83
84/// Registry of per-signer connection statistics. Thread-safe.
85#[derive(Default)]
86pub struct ConnectionStatsRegistry {
87    entries: Mutex<HashMap<String, ConnectionStats>>,
88}
89
90impl ConnectionStatsRegistry {
91    /// Create an empty registry.
92    pub fn new() -> Self {
93        Self::default()
94    }
95
96    /// Register a new signer connection. Overwrites any existing
97    /// stats for this signer_id.
98    pub fn register(&self, signer_id: &str) {
99        self.entries
100            .lock()
101            .unwrap()
102            .insert(signer_id.into(), ConnectionStats::new(signer_id));
103    }
104
105    /// Record a received message for a signer.
106    pub fn record_received(&self, signer_id: &str, bytes: u64) {
107        if let Some(stats) = self.entries.lock().unwrap().get_mut(signer_id) {
108            stats.record_received(bytes);
109        }
110    }
111
112    /// Record a sent message for a signer.
113    pub fn record_sent(&self, signer_id: &str, bytes: u64) {
114        if let Some(stats) = self.entries.lock().unwrap().get_mut(signer_id) {
115            stats.record_sent(bytes);
116        }
117    }
118
119    /// Record an error for a signer.
120    pub fn record_error(&self, signer_id: &str) {
121        if let Some(stats) = self.entries.lock().unwrap().get_mut(signer_id) {
122            stats.record_error();
123        }
124    }
125
126    /// Get stats for a specific signer.
127    pub fn get(&self, signer_id: &str) -> Option<ConnectionStats> {
128        self.entries.lock().unwrap().get(signer_id).cloned()
129    }
130
131    /// Get stats for all signers.
132    pub fn all(&self) -> Vec<ConnectionStats> {
133        self.entries.lock().unwrap().values().cloned().collect()
134    }
135
136    /// Remove a signer's stats (on disconnect).
137    pub fn remove(&self, signer_id: &str) {
138        self.entries.lock().unwrap().remove(signer_id);
139    }
140
141    /// Number of tracked signers.
142    pub fn count(&self) -> usize {
143        self.entries.lock().unwrap().len()
144    }
145
146    /// Clear all stats.
147    pub fn clear(&self) {
148        self.entries.lock().unwrap().clear();
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn new_stats_starts_at_zero() {
158        let stats = ConnectionStats::new("s1");
159        assert_eq!(stats.signer_id, "s1");
160        assert_eq!(stats.messages_received, 0);
161        assert_eq!(stats.messages_sent, 0);
162        assert_eq!(stats.bytes_received, 0);
163        assert_eq!(stats.bytes_sent, 0);
164        assert_eq!(stats.error_count, 0);
165    }
166
167    #[test]
168    fn record_received_increments() {
169        let mut stats = ConnectionStats::new("s1");
170        stats.record_received(100);
171        stats.record_received(50);
172        assert_eq!(stats.messages_received, 2);
173        assert_eq!(stats.bytes_received, 150);
174    }
175
176    #[test]
177    fn record_sent_increments() {
178        let mut stats = ConnectionStats::new("s1");
179        stats.record_sent(200);
180        assert_eq!(stats.messages_sent, 1);
181        assert_eq!(stats.bytes_sent, 200);
182    }
183
184    #[test]
185    fn record_error_increments() {
186        let mut stats = ConnectionStats::new("s1");
187        stats.record_error();
188        stats.record_error();
189        assert_eq!(stats.error_count, 2);
190    }
191
192    #[test]
193    fn total_messages_and_bytes() {
194        let mut stats = ConnectionStats::new("s1");
195        stats.record_received(100);
196        stats.record_sent(200);
197        assert_eq!(stats.total_messages(), 2);
198        assert_eq!(stats.total_bytes(), 300);
199    }
200
201    #[test]
202    fn connection_duration_positive() {
203        let stats = ConnectionStats::new("s1");
204        std::thread::sleep(std::time::Duration::from_millis(100));
205        assert!(stats.connection_duration_secs() >= 0);
206    }
207
208    #[test]
209    fn registry_register_and_get() {
210        let reg = ConnectionStatsRegistry::new();
211        reg.register("s1");
212        let stats = reg.get("s1").unwrap();
213        assert_eq!(stats.signer_id, "s1");
214        assert_eq!(stats.messages_received, 0);
215    }
216
217    #[test]
218    fn registry_record_received() {
219        let reg = ConnectionStatsRegistry::new();
220        reg.register("s1");
221        reg.record_received("s1", 500);
222        let stats = reg.get("s1").unwrap();
223        assert_eq!(stats.messages_received, 1);
224        assert_eq!(stats.bytes_received, 500);
225    }
226
227    #[test]
228    fn registry_record_sent() {
229        let reg = ConnectionStatsRegistry::new();
230        reg.register("s1");
231        reg.record_sent("s1", 1000);
232        assert_eq!(reg.get("s1").unwrap().bytes_sent, 1000);
233    }
234
235    #[test]
236    fn registry_record_error() {
237        let reg = ConnectionStatsRegistry::new();
238        reg.register("s1");
239        reg.record_error("s1");
240        assert_eq!(reg.get("s1").unwrap().error_count, 1);
241    }
242
243    #[test]
244    fn registry_all_returns_all_signers() {
245        let reg = ConnectionStatsRegistry::new();
246        reg.register("s1");
247        reg.register("s2");
248        reg.register("s3");
249        assert_eq!(reg.all().len(), 3);
250    }
251
252    #[test]
253    fn registry_remove() {
254        let reg = ConnectionStatsRegistry::new();
255        reg.register("s1");
256        reg.remove("s1");
257        assert!(reg.get("s1").is_none());
258        assert_eq!(reg.count(), 0);
259    }
260
261    #[test]
262    fn registry_unknown_signer_no_op() {
263        let reg = ConnectionStatsRegistry::new();
264        reg.record_received("unknown", 100);
265        assert_eq!(reg.count(), 0);
266    }
267}