Skip to main content

irontide_session/
stats.rs

1//! Session statistics metric registry and atomic counter array.
2//!
3//! Provides 70 atomic metrics across 7 categories (network, disk, DHT, peers,
4//! protocol, bandwidth, session). [`MetricKind`] distinguishes monotonic
5//! counters from point-in-time gauges. [`SessionStatsMetric`] provides static
6//! metadata for each metric, and [`SessionCounters`] holds the atomic values.
7
8use std::sync::atomic::{AtomicI64, Ordering};
9use std::time::Instant;
10
11// ---------------------------------------------------------------------------
12// MetricKind
13// ---------------------------------------------------------------------------
14
15/// Whether a metric is a monotonically increasing counter or a point-in-time gauge.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum MetricKind {
18    /// Monotonically increasing (bytes sent, pieces downloaded, etc.).
19    Counter,
20    /// Current value that can go up or down (connections, DHT nodes, etc.).
21    Gauge,
22}
23
24// ---------------------------------------------------------------------------
25// SessionStatsMetric
26// ---------------------------------------------------------------------------
27
28/// Static metadata for a single session metric.
29#[derive(Debug, Clone, Copy)]
30pub struct SessionStatsMetric {
31    /// Human-readable dotted name (e.g. `"net.bytes_sent"`).
32    pub name: &'static str,
33    /// Whether this metric is a counter or gauge.
34    pub kind: MetricKind,
35}
36
37// ---------------------------------------------------------------------------
38// Metric index constants (70 total)
39// ---------------------------------------------------------------------------
40
41// -- Network (0..11) --
42
43/// Metric index: total bytes sent across all connections (counter).
44pub const NET_BYTES_SENT: usize = 0;
45/// Metric index: total bytes received across all connections (counter).
46pub const NET_BYTES_RECV: usize = 1;
47/// Metric index: current number of established connections (gauge).
48pub const NET_NUM_CONNECTIONS: usize = 2;
49/// Metric index: current number of half-open (connecting) sockets (gauge).
50pub const NET_NUM_HALF_OPEN: usize = 3;
51/// Metric index: current number of TCP peers (gauge).
52pub const NET_NUM_TCP_PEERS: usize = 4;
53/// Metric index: current number of uTP peers (gauge).
54pub const NET_NUM_UTP_PEERS: usize = 5;
55/// Metric index: current number of TCP connections (gauge).
56pub const NET_NUM_TCP_CONNECTIONS: usize = 6;
57/// Metric index: current number of uTP connections (gauge).
58pub const NET_NUM_UTP_CONNECTIONS: usize = 7;
59/// Metric index: total bytes sent over TCP (counter).
60pub const NET_TCP_BYTES_SENT: usize = 8;
61/// Metric index: total bytes received over TCP (counter).
62pub const NET_TCP_BYTES_RECV: usize = 9;
63/// Metric index: total bytes sent over uTP (counter).
64pub const NET_UTP_BYTES_SENT: usize = 10;
65/// Metric index: total bytes received over uTP (counter).
66pub const NET_UTP_BYTES_RECV: usize = 11;
67
68// -- Disk (12..21) --
69
70/// Metric index: total disk read operations (counter).
71pub const DISK_READ_COUNT: usize = 12;
72/// Metric index: total disk write operations (counter).
73pub const DISK_WRITE_COUNT: usize = 13;
74/// Metric index: total bytes read from disk (counter).
75pub const DISK_READ_BYTES: usize = 14;
76/// Metric index: total bytes written to disk (counter).
77pub const DISK_WRITE_BYTES: usize = 15;
78/// Metric index: total disk cache hits (counter).
79pub const DISK_CACHE_HITS: usize = 16;
80/// Metric index: total disk cache misses (counter).
81pub const DISK_CACHE_MISSES: usize = 17;
82/// Metric index: current disk job queue depth (gauge).
83pub const DISK_QUEUE_DEPTH: usize = 18;
84/// Metric index: cumulative disk job time in microseconds (counter).
85pub const DISK_JOB_TIME_US: usize = 19;
86/// Metric index: current write buffer size in bytes (gauge).
87pub const DISK_WRITE_BUFFER_BYTES: usize = 20;
88/// Metric index: total piece hash operations (counter).
89pub const DISK_HASH_COUNT: usize = 21;
90
91// -- DHT (22..28) --
92
93/// Metric index: current number of DHT routing table nodes (gauge).
94pub const DHT_NODES: usize = 22;
95/// Metric index: total DHT lookup operations (counter).
96pub const DHT_LOOKUPS: usize = 23;
97/// Metric index: total bytes received via DHT (counter).
98pub const DHT_BYTES_IN: usize = 24;
99/// Metric index: total bytes sent via DHT (counter).
100pub const DHT_BYTES_OUT: usize = 25;
101/// Metric index: current number of IPv4 DHT nodes (gauge).
102pub const DHT_NODES_V4: usize = 26;
103/// Metric index: current number of IPv6 DHT nodes (gauge).
104pub const DHT_NODES_V6: usize = 27;
105/// Metric index: total DHT announce operations (counter).
106pub const DHT_ANNOUNCE_COUNT: usize = 28;
107
108// -- Peers (29..40) --
109
110/// Metric index: current number of unchoked peers (gauge).
111pub const PEER_NUM_UNCHOKED: usize = 29;
112/// Metric index: current number of interested peers (gauge).
113pub const PEER_NUM_INTERESTED: usize = 30;
114/// Metric index: current number of peers we are uploading to (gauge).
115pub const PEER_NUM_UPLOADING: usize = 31;
116/// Metric index: current number of peers we are downloading from (gauge).
117pub const PEER_NUM_DOWNLOADING: usize = 32;
118/// Metric index: current number of torrents in seeding state (gauge).
119pub const PEER_NUM_SEEDING_TORRENTS: usize = 33;
120/// Metric index: current number of torrents in downloading state (gauge).
121pub const PEER_NUM_DOWNLOADING_TORRENTS: usize = 34;
122/// Metric index: current number of torrents being checked (gauge).
123pub const PEER_NUM_CHECKING_TORRENTS: usize = 35;
124/// Metric index: current number of paused torrents (gauge).
125pub const PEER_NUM_PAUSED_TORRENTS: usize = 36;
126/// Metric index: current total number of connected peers (gauge).
127pub const PEER_PEERS_CONNECTED: usize = 37;
128/// Metric index: current total number of known available peers (gauge).
129pub const PEER_PEERS_AVAILABLE: usize = 38;
130/// Metric index: current number of active web seeds (gauge).
131pub const PEER_NUM_WEB_SEEDS: usize = 39;
132/// Metric index: current number of banned peers (gauge).
133pub const PEER_NUM_BANNED: usize = 40;
134
135// -- Protocol (41..54) --
136
137/// Metric index: total pieces downloaded across all torrents (counter).
138pub const PROTO_PIECES_DOWNLOADED: usize = 41;
139/// Metric index: total pieces uploaded across all torrents (counter).
140pub const PROTO_PIECES_UPLOADED: usize = 42;
141/// Metric index: total piece hash verification failures (counter).
142pub const PROTO_HASHFAILS: usize = 43;
143/// Metric index: total wasted bytes (duplicate/rejected data) (counter).
144pub const PROTO_WASTE_BYTES: usize = 44;
145/// Metric index: total piece request messages sent (counter).
146pub const PROTO_PIECE_REQUESTS: usize = 45;
147/// Metric index: total piece reject messages received (counter).
148pub const PROTO_PIECE_REJECTS: usize = 46;
149/// Metric index: total incoming handshakes received (counter).
150pub const PROTO_HANDSHAKES_IN: usize = 47;
151/// Metric index: total outgoing handshakes sent (counter).
152pub const PROTO_HANDSHAKES_OUT: usize = 48;
153/// Metric index: total PEX messages received (counter).
154pub const PROTO_PEX_MESSAGES_IN: usize = 49;
155/// Metric index: total PEX messages sent (counter).
156pub const PROTO_PEX_MESSAGES_OUT: usize = 50;
157/// Metric index: total tracker announce requests (counter).
158pub const PROTO_TRACKER_ANNOUNCES: usize = 51;
159/// Metric index: total tracker announce errors (counter).
160pub const PROTO_TRACKER_ERRORS: usize = 52;
161/// Metric index: total BEP 9 metadata requests sent (counter).
162pub const PROTO_METADATA_REQUESTS: usize = 53;
163/// Metric index: total BEP 9 metadata pieces received (counter).
164pub const PROTO_METADATA_RECEIVES: usize = 54;
165
166// -- Bandwidth (55..64) --
167
168/// Metric index: current aggregate upload rate in bytes/sec (gauge).
169pub const BW_UPLOAD_RATE: usize = 55;
170/// Metric index: current aggregate download rate in bytes/sec (gauge).
171pub const BW_DOWNLOAD_RATE: usize = 56;
172/// Metric index: current TCP upload rate in bytes/sec (gauge).
173pub const BW_UPLOAD_RATE_TCP: usize = 57;
174/// Metric index: current TCP download rate in bytes/sec (gauge).
175pub const BW_DOWNLOAD_RATE_TCP: usize = 58;
176/// Metric index: current uTP upload rate in bytes/sec (gauge).
177pub const BW_UPLOAD_RATE_UTP: usize = 59;
178/// Metric index: current uTP download rate in bytes/sec (gauge).
179pub const BW_DOWNLOAD_RATE_UTP: usize = 60;
180/// Metric index: current payload-only upload rate in bytes/sec (gauge).
181pub const BW_PAYLOAD_UPLOAD_RATE: usize = 61;
182/// Metric index: current payload-only download rate in bytes/sec (gauge).
183pub const BW_PAYLOAD_DOWNLOAD_RATE: usize = 62;
184/// Metric index: total bytes uploaded since session start (counter).
185pub const BW_TOTAL_UPLOADED: usize = 63;
186/// Metric index: total bytes downloaded since session start (counter).
187pub const BW_TOTAL_DOWNLOADED: usize = 64;
188
189// -- Session (65..69) --
190
191/// Metric index: current number of active (non-paused) torrents (gauge).
192pub const SES_ACTIVE_TORRENTS: usize = 65;
193/// Metric index: total number of torrents in the session (gauge).
194pub const SES_NUM_TORRENTS: usize = 66;
195/// Metric index: session uptime in seconds (gauge).
196pub const SES_UPTIME_SECS: usize = 67;
197/// Metric index: total connections blocked by the IP filter (counter).
198pub const SES_IP_FILTER_BLOCKED: usize = 68;
199/// Metric index: total torrents paused by auto-management (counter).
200pub const SES_QUEUE_PAUSED_BY_AUTO: usize = 69;
201
202/// Total number of metrics tracked by the session.
203pub const NUM_METRICS: usize = 70;
204
205// ---------------------------------------------------------------------------
206// session_stats_metrics()
207// ---------------------------------------------------------------------------
208
209/// Return static metadata for all session metrics.
210///
211/// The returned slice is indexed by metric constant (e.g. [`NET_BYTES_SENT`]).
212pub fn session_stats_metrics() -> &'static [SessionStatsMetric] {
213    use MetricKind::*;
214    static METRICS: [SessionStatsMetric; NUM_METRICS] = [
215        // Network (0..11)
216        SessionStatsMetric {
217            name: "net.bytes_sent",
218            kind: Counter,
219        },
220        SessionStatsMetric {
221            name: "net.bytes_recv",
222            kind: Counter,
223        },
224        SessionStatsMetric {
225            name: "net.num_connections",
226            kind: Gauge,
227        },
228        SessionStatsMetric {
229            name: "net.num_half_open",
230            kind: Gauge,
231        },
232        SessionStatsMetric {
233            name: "net.num_tcp_peers",
234            kind: Gauge,
235        },
236        SessionStatsMetric {
237            name: "net.num_utp_peers",
238            kind: Gauge,
239        },
240        SessionStatsMetric {
241            name: "net.num_tcp_connections",
242            kind: Gauge,
243        },
244        SessionStatsMetric {
245            name: "net.num_utp_connections",
246            kind: Gauge,
247        },
248        SessionStatsMetric {
249            name: "net.tcp_bytes_sent",
250            kind: Counter,
251        },
252        SessionStatsMetric {
253            name: "net.tcp_bytes_recv",
254            kind: Counter,
255        },
256        SessionStatsMetric {
257            name: "net.utp_bytes_sent",
258            kind: Counter,
259        },
260        SessionStatsMetric {
261            name: "net.utp_bytes_recv",
262            kind: Counter,
263        },
264        // Disk (12..21)
265        SessionStatsMetric {
266            name: "disk.read_count",
267            kind: Counter,
268        },
269        SessionStatsMetric {
270            name: "disk.write_count",
271            kind: Counter,
272        },
273        SessionStatsMetric {
274            name: "disk.read_bytes",
275            kind: Counter,
276        },
277        SessionStatsMetric {
278            name: "disk.write_bytes",
279            kind: Counter,
280        },
281        SessionStatsMetric {
282            name: "disk.cache_hits",
283            kind: Counter,
284        },
285        SessionStatsMetric {
286            name: "disk.cache_misses",
287            kind: Counter,
288        },
289        SessionStatsMetric {
290            name: "disk.queue_depth",
291            kind: Gauge,
292        },
293        SessionStatsMetric {
294            name: "disk.job_time_us",
295            kind: Counter,
296        },
297        SessionStatsMetric {
298            name: "disk.write_buffer_bytes",
299            kind: Gauge,
300        },
301        SessionStatsMetric {
302            name: "disk.hash_count",
303            kind: Counter,
304        },
305        // DHT (22..28)
306        SessionStatsMetric {
307            name: "dht.nodes",
308            kind: Gauge,
309        },
310        SessionStatsMetric {
311            name: "dht.lookups",
312            kind: Counter,
313        },
314        SessionStatsMetric {
315            name: "dht.bytes_in",
316            kind: Counter,
317        },
318        SessionStatsMetric {
319            name: "dht.bytes_out",
320            kind: Counter,
321        },
322        SessionStatsMetric {
323            name: "dht.nodes_v4",
324            kind: Gauge,
325        },
326        SessionStatsMetric {
327            name: "dht.nodes_v6",
328            kind: Gauge,
329        },
330        SessionStatsMetric {
331            name: "dht.announce_count",
332            kind: Counter,
333        },
334        // Peers (29..40)
335        SessionStatsMetric {
336            name: "peer.num_unchoked",
337            kind: Gauge,
338        },
339        SessionStatsMetric {
340            name: "peer.num_interested",
341            kind: Gauge,
342        },
343        SessionStatsMetric {
344            name: "peer.num_uploading",
345            kind: Gauge,
346        },
347        SessionStatsMetric {
348            name: "peer.num_downloading",
349            kind: Gauge,
350        },
351        SessionStatsMetric {
352            name: "peer.num_seeding_torrents",
353            kind: Gauge,
354        },
355        SessionStatsMetric {
356            name: "peer.num_downloading_torrents",
357            kind: Gauge,
358        },
359        SessionStatsMetric {
360            name: "peer.num_checking_torrents",
361            kind: Gauge,
362        },
363        SessionStatsMetric {
364            name: "peer.num_paused_torrents",
365            kind: Gauge,
366        },
367        SessionStatsMetric {
368            name: "peer.peers_connected",
369            kind: Gauge,
370        },
371        SessionStatsMetric {
372            name: "peer.peers_available",
373            kind: Gauge,
374        },
375        SessionStatsMetric {
376            name: "peer.num_web_seeds",
377            kind: Gauge,
378        },
379        SessionStatsMetric {
380            name: "peer.num_banned",
381            kind: Gauge,
382        },
383        // Protocol (41..54)
384        SessionStatsMetric {
385            name: "proto.pieces_downloaded",
386            kind: Counter,
387        },
388        SessionStatsMetric {
389            name: "proto.pieces_uploaded",
390            kind: Counter,
391        },
392        SessionStatsMetric {
393            name: "proto.hashfails",
394            kind: Counter,
395        },
396        SessionStatsMetric {
397            name: "proto.waste_bytes",
398            kind: Counter,
399        },
400        SessionStatsMetric {
401            name: "proto.piece_requests",
402            kind: Counter,
403        },
404        SessionStatsMetric {
405            name: "proto.piece_rejects",
406            kind: Counter,
407        },
408        SessionStatsMetric {
409            name: "proto.handshakes_in",
410            kind: Counter,
411        },
412        SessionStatsMetric {
413            name: "proto.handshakes_out",
414            kind: Counter,
415        },
416        SessionStatsMetric {
417            name: "proto.pex_messages_in",
418            kind: Counter,
419        },
420        SessionStatsMetric {
421            name: "proto.pex_messages_out",
422            kind: Counter,
423        },
424        SessionStatsMetric {
425            name: "proto.tracker_announces",
426            kind: Counter,
427        },
428        SessionStatsMetric {
429            name: "proto.tracker_errors",
430            kind: Counter,
431        },
432        SessionStatsMetric {
433            name: "proto.metadata_requests",
434            kind: Counter,
435        },
436        SessionStatsMetric {
437            name: "proto.metadata_receives",
438            kind: Counter,
439        },
440        // Bandwidth (55..64)
441        SessionStatsMetric {
442            name: "bw.upload_rate",
443            kind: Gauge,
444        },
445        SessionStatsMetric {
446            name: "bw.download_rate",
447            kind: Gauge,
448        },
449        SessionStatsMetric {
450            name: "bw.upload_rate_tcp",
451            kind: Gauge,
452        },
453        SessionStatsMetric {
454            name: "bw.download_rate_tcp",
455            kind: Gauge,
456        },
457        SessionStatsMetric {
458            name: "bw.upload_rate_utp",
459            kind: Gauge,
460        },
461        SessionStatsMetric {
462            name: "bw.download_rate_utp",
463            kind: Gauge,
464        },
465        SessionStatsMetric {
466            name: "bw.payload_upload_rate",
467            kind: Gauge,
468        },
469        SessionStatsMetric {
470            name: "bw.payload_download_rate",
471            kind: Gauge,
472        },
473        SessionStatsMetric {
474            name: "bw.total_uploaded",
475            kind: Counter,
476        },
477        SessionStatsMetric {
478            name: "bw.total_downloaded",
479            kind: Counter,
480        },
481        // Session (65..69)
482        SessionStatsMetric {
483            name: "ses.active_torrents",
484            kind: Gauge,
485        },
486        SessionStatsMetric {
487            name: "ses.num_torrents",
488            kind: Gauge,
489        },
490        SessionStatsMetric {
491            name: "ses.uptime_secs",
492            kind: Gauge,
493        },
494        SessionStatsMetric {
495            name: "ses.ip_filter_blocked",
496            kind: Counter,
497        },
498        SessionStatsMetric {
499            name: "ses.queue_paused_by_auto",
500            kind: Counter,
501        },
502    ];
503    &METRICS
504}
505
506// ---------------------------------------------------------------------------
507// SessionCounters
508// ---------------------------------------------------------------------------
509
510/// Atomic counter array shared between session and torrent actors.
511///
512/// All values are [`AtomicI64`] — counters are incremented, gauges are set.
513/// The struct is `Send + Sync` (auto-derived from `AtomicI64`).
514pub struct SessionCounters {
515    values: [AtomicI64; NUM_METRICS],
516    started_at: Instant,
517    prev_bytes_sent: AtomicI64,
518    prev_bytes_recv: AtomicI64,
519}
520
521impl SessionCounters {
522    /// Create a new counter array with all values initialised to zero.
523    pub fn new() -> Self {
524        Self {
525            values: std::array::from_fn(|_| AtomicI64::new(0)),
526            started_at: Instant::now(),
527            prev_bytes_sent: AtomicI64::new(0),
528            prev_bytes_recv: AtomicI64::new(0),
529        }
530    }
531
532    /// Atomically add `delta` to a counter metric.
533    #[inline]
534    pub fn inc(&self, metric: usize, delta: i64) {
535        debug_assert!(metric < NUM_METRICS);
536        self.values[metric].fetch_add(delta, Ordering::Relaxed);
537    }
538
539    /// Atomically set a gauge metric to `value`.
540    #[inline]
541    pub fn set(&self, metric: usize, value: i64) {
542        debug_assert!(metric < NUM_METRICS);
543        self.values[metric].store(value, Ordering::Relaxed);
544    }
545
546    /// Read the current value of a metric.
547    #[inline]
548    pub fn get(&self, metric: usize) -> i64 {
549        debug_assert!(metric < NUM_METRICS);
550        self.values[metric].load(Ordering::Relaxed)
551    }
552
553    /// Take a consistent snapshot of all metric values.
554    ///
555    /// Also updates the uptime gauge and computes bandwidth rate deltas
556    /// (upload/download rate = bytes since last snapshot).
557    pub fn snapshot(&self) -> Vec<i64> {
558        let mut vals: Vec<i64> = self
559            .values
560            .iter()
561            .map(|a| a.load(Ordering::Relaxed))
562            .collect();
563
564        // Update uptime gauge.
565        vals[SES_UPTIME_SECS] = self.started_at.elapsed().as_secs() as i64;
566
567        // Compute bandwidth rate deltas.
568        let cur_sent = vals[NET_BYTES_SENT];
569        let cur_recv = vals[NET_BYTES_RECV];
570        let prev_sent = self.prev_bytes_sent.swap(cur_sent, Ordering::Relaxed);
571        let prev_recv = self.prev_bytes_recv.swap(cur_recv, Ordering::Relaxed);
572        vals[BW_UPLOAD_RATE] = cur_sent.saturating_sub(prev_sent);
573        vals[BW_DOWNLOAD_RATE] = cur_recv.saturating_sub(prev_recv);
574
575        vals
576    }
577
578    /// Number of metrics tracked.
579    pub fn len(&self) -> usize {
580        NUM_METRICS
581    }
582
583    /// Always returns `false` — there are always metrics.
584    pub fn is_empty(&self) -> bool {
585        false
586    }
587
588    /// Seconds elapsed since the session was created.
589    pub fn uptime_secs(&self) -> u64 {
590        self.started_at.elapsed().as_secs()
591    }
592}
593
594impl Default for SessionCounters {
595    fn default() -> Self {
596        Self::new()
597    }
598}
599
600// ---------------------------------------------------------------------------
601// Tests
602// ---------------------------------------------------------------------------
603
604#[cfg(test)]
605mod tests {
606    use super::*;
607    use std::collections::HashSet;
608
609    #[test]
610    fn metrics_registry_has_correct_count() {
611        assert_eq!(session_stats_metrics().len(), NUM_METRICS);
612    }
613
614    #[test]
615    fn all_metric_names_are_unique() {
616        let names: HashSet<&str> = session_stats_metrics().iter().map(|m| m.name).collect();
617        assert_eq!(names.len(), NUM_METRICS);
618    }
619
620    #[test]
621    fn all_metric_names_have_category_prefix() {
622        for m in session_stats_metrics() {
623            assert!(
624                m.name.contains('.'),
625                "metric name {:?} has no category prefix",
626                m.name
627            );
628        }
629    }
630
631    #[test]
632    fn counter_inc_and_get() {
633        let c = SessionCounters::new();
634        c.inc(NET_BYTES_SENT, 5);
635        assert_eq!(c.get(NET_BYTES_SENT), 5);
636        c.inc(NET_BYTES_SENT, 3);
637        assert_eq!(c.get(NET_BYTES_SENT), 8);
638    }
639
640    #[test]
641    fn gauge_set_and_get() {
642        let c = SessionCounters::new();
643        c.set(NET_NUM_CONNECTIONS, 42);
644        assert_eq!(c.get(NET_NUM_CONNECTIONS), 42);
645        c.set(NET_NUM_CONNECTIONS, 0);
646        assert_eq!(c.get(NET_NUM_CONNECTIONS), 0);
647    }
648
649    #[test]
650    fn snapshot_returns_all_values() {
651        let c = SessionCounters::new();
652        c.inc(NET_BYTES_SENT, 100);
653        c.set(DHT_NODES, 50);
654        c.inc(PROTO_HASHFAILS, 3);
655        let snap = c.snapshot();
656        assert_eq!(snap.len(), NUM_METRICS);
657        assert_eq!(snap[NET_BYTES_SENT], 100);
658        assert_eq!(snap[DHT_NODES], 50);
659        assert_eq!(snap[PROTO_HASHFAILS], 3);
660    }
661
662    #[test]
663    fn snapshot_includes_uptime() {
664        let c = SessionCounters::new();
665        // Even without sleeping, uptime should be >= 0.
666        let snap = c.snapshot();
667        assert!(snap[SES_UPTIME_SECS] >= 0);
668    }
669
670    #[test]
671    fn counters_are_send_and_sync() {
672        fn assert_send_sync<T: Send + Sync>() {}
673        assert_send_sync::<SessionCounters>();
674    }
675
676    #[test]
677    fn metric_kind_serializes() {
678        let counter_json = serde_json::to_string(&MetricKind::Counter).unwrap();
679        let gauge_json = serde_json::to_string(&MetricKind::Gauge).unwrap();
680        assert_eq!(
681            serde_json::from_str::<MetricKind>(&counter_json).unwrap(),
682            MetricKind::Counter
683        );
684        assert_eq!(
685            serde_json::from_str::<MetricKind>(&gauge_json).unwrap(),
686            MetricKind::Gauge
687        );
688    }
689
690    #[test]
691    fn metric_index_constants_in_range() {
692        let indices = [
693            NET_BYTES_SENT,
694            NET_BYTES_RECV,
695            NET_NUM_CONNECTIONS,
696            NET_NUM_HALF_OPEN,
697            NET_NUM_TCP_PEERS,
698            NET_NUM_UTP_PEERS,
699            NET_NUM_TCP_CONNECTIONS,
700            NET_NUM_UTP_CONNECTIONS,
701            NET_TCP_BYTES_SENT,
702            NET_TCP_BYTES_RECV,
703            NET_UTP_BYTES_SENT,
704            NET_UTP_BYTES_RECV,
705            DISK_READ_COUNT,
706            DISK_WRITE_COUNT,
707            DISK_READ_BYTES,
708            DISK_WRITE_BYTES,
709            DISK_CACHE_HITS,
710            DISK_CACHE_MISSES,
711            DISK_QUEUE_DEPTH,
712            DISK_JOB_TIME_US,
713            DISK_WRITE_BUFFER_BYTES,
714            DISK_HASH_COUNT,
715            DHT_NODES,
716            DHT_LOOKUPS,
717            DHT_BYTES_IN,
718            DHT_BYTES_OUT,
719            DHT_NODES_V4,
720            DHT_NODES_V6,
721            DHT_ANNOUNCE_COUNT,
722            PEER_NUM_UNCHOKED,
723            PEER_NUM_INTERESTED,
724            PEER_NUM_UPLOADING,
725            PEER_NUM_DOWNLOADING,
726            PEER_NUM_SEEDING_TORRENTS,
727            PEER_NUM_DOWNLOADING_TORRENTS,
728            PEER_NUM_CHECKING_TORRENTS,
729            PEER_NUM_PAUSED_TORRENTS,
730            PEER_PEERS_CONNECTED,
731            PEER_PEERS_AVAILABLE,
732            PEER_NUM_WEB_SEEDS,
733            PEER_NUM_BANNED,
734            PROTO_PIECES_DOWNLOADED,
735            PROTO_PIECES_UPLOADED,
736            PROTO_HASHFAILS,
737            PROTO_WASTE_BYTES,
738            PROTO_PIECE_REQUESTS,
739            PROTO_PIECE_REJECTS,
740            PROTO_HANDSHAKES_IN,
741            PROTO_HANDSHAKES_OUT,
742            PROTO_PEX_MESSAGES_IN,
743            PROTO_PEX_MESSAGES_OUT,
744            PROTO_TRACKER_ANNOUNCES,
745            PROTO_TRACKER_ERRORS,
746            PROTO_METADATA_REQUESTS,
747            PROTO_METADATA_RECEIVES,
748            BW_UPLOAD_RATE,
749            BW_DOWNLOAD_RATE,
750            BW_UPLOAD_RATE_TCP,
751            BW_DOWNLOAD_RATE_TCP,
752            BW_UPLOAD_RATE_UTP,
753            BW_DOWNLOAD_RATE_UTP,
754            BW_PAYLOAD_UPLOAD_RATE,
755            BW_PAYLOAD_DOWNLOAD_RATE,
756            BW_TOTAL_UPLOADED,
757            BW_TOTAL_DOWNLOADED,
758            SES_ACTIVE_TORRENTS,
759            SES_NUM_TORRENTS,
760            SES_UPTIME_SECS,
761            SES_IP_FILTER_BLOCKED,
762            SES_QUEUE_PAUSED_BY_AUTO,
763        ];
764        assert_eq!(indices.len(), NUM_METRICS);
765        for &idx in &indices {
766            assert!(idx < NUM_METRICS, "index {idx} >= NUM_METRICS");
767        }
768    }
769
770    #[test]
771    fn default_counters_all_zero() {
772        let c = SessionCounters::default();
773        let snap = c.snapshot();
774        for (i, &val) in snap.iter().enumerate() {
775            if i == SES_UPTIME_SECS {
776                continue; // uptime is computed dynamically
777            }
778            // BW_UPLOAD_RATE and BW_DOWNLOAD_RATE are computed from deltas
779            // and will be 0 on first snapshot (prev == 0, cur == 0).
780            assert_eq!(val, 0, "metric index {i} should be 0 but was {val}");
781        }
782    }
783
784    #[test]
785    fn concurrent_inc_from_multiple_threads() {
786        use std::sync::Arc;
787
788        let c = Arc::new(SessionCounters::new());
789        let threads: Vec<_> = (0..4)
790            .map(|_| {
791                let c = Arc::clone(&c);
792                std::thread::spawn(move || {
793                    for _ in 0..1000 {
794                        c.inc(NET_BYTES_SENT, 1);
795                    }
796                })
797            })
798            .collect();
799        for t in threads {
800            t.join().unwrap();
801        }
802        assert_eq!(c.get(NET_BYTES_SENT), 4000);
803    }
804
805    #[test]
806    fn len_and_is_empty() {
807        let c = SessionCounters::new();
808        assert_eq!(c.len(), 70);
809        assert!(!c.is_empty());
810    }
811}