1use std::sync::atomic::{AtomicI64, Ordering};
9use std::time::Instant;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
17pub enum MetricKind {
18 Counter,
20 Gauge,
22}
23
24#[derive(Debug, Clone, Copy)]
30pub struct SessionStatsMetric {
31 pub name: &'static str,
33 pub kind: MetricKind,
35}
36
37pub const NET_BYTES_SENT: usize = 0;
45pub const NET_BYTES_RECV: usize = 1;
47pub const NET_NUM_CONNECTIONS: usize = 2;
49pub const NET_NUM_HALF_OPEN: usize = 3;
51pub const NET_NUM_TCP_PEERS: usize = 4;
53pub const NET_NUM_UTP_PEERS: usize = 5;
55pub const NET_NUM_TCP_CONNECTIONS: usize = 6;
57pub const NET_NUM_UTP_CONNECTIONS: usize = 7;
59pub const NET_TCP_BYTES_SENT: usize = 8;
61pub const NET_TCP_BYTES_RECV: usize = 9;
63pub const NET_UTP_BYTES_SENT: usize = 10;
65pub const NET_UTP_BYTES_RECV: usize = 11;
67
68pub const DISK_READ_COUNT: usize = 12;
72pub const DISK_WRITE_COUNT: usize = 13;
74pub const DISK_READ_BYTES: usize = 14;
76pub const DISK_WRITE_BYTES: usize = 15;
78pub const DISK_CACHE_HITS: usize = 16;
80pub const DISK_CACHE_MISSES: usize = 17;
82pub const DISK_QUEUE_DEPTH: usize = 18;
84pub const DISK_JOB_TIME_US: usize = 19;
86pub const DISK_WRITE_BUFFER_BYTES: usize = 20;
88pub const DISK_HASH_COUNT: usize = 21;
90
91pub const DHT_NODES: usize = 22;
95pub const DHT_LOOKUPS: usize = 23;
97pub const DHT_BYTES_IN: usize = 24;
99pub const DHT_BYTES_OUT: usize = 25;
101pub const DHT_NODES_V4: usize = 26;
103pub const DHT_NODES_V6: usize = 27;
105pub const DHT_ANNOUNCE_COUNT: usize = 28;
107
108pub const PEER_NUM_UNCHOKED: usize = 29;
112pub const PEER_NUM_INTERESTED: usize = 30;
114pub const PEER_NUM_UPLOADING: usize = 31;
116pub const PEER_NUM_DOWNLOADING: usize = 32;
118pub const PEER_NUM_SEEDING_TORRENTS: usize = 33;
120pub const PEER_NUM_DOWNLOADING_TORRENTS: usize = 34;
122pub const PEER_NUM_CHECKING_TORRENTS: usize = 35;
124pub const PEER_NUM_PAUSED_TORRENTS: usize = 36;
126pub const PEER_PEERS_CONNECTED: usize = 37;
128pub const PEER_PEERS_AVAILABLE: usize = 38;
130pub const PEER_NUM_WEB_SEEDS: usize = 39;
132pub const PEER_NUM_BANNED: usize = 40;
134
135pub const PROTO_PIECES_DOWNLOADED: usize = 41;
139pub const PROTO_PIECES_UPLOADED: usize = 42;
141pub const PROTO_HASHFAILS: usize = 43;
143pub const PROTO_WASTE_BYTES: usize = 44;
145pub const PROTO_PIECE_REQUESTS: usize = 45;
147pub const PROTO_PIECE_REJECTS: usize = 46;
149pub const PROTO_HANDSHAKES_IN: usize = 47;
151pub const PROTO_HANDSHAKES_OUT: usize = 48;
153pub const PROTO_PEX_MESSAGES_IN: usize = 49;
155pub const PROTO_PEX_MESSAGES_OUT: usize = 50;
157pub const PROTO_TRACKER_ANNOUNCES: usize = 51;
159pub const PROTO_TRACKER_ERRORS: usize = 52;
161pub const PROTO_METADATA_REQUESTS: usize = 53;
163pub const PROTO_METADATA_RECEIVES: usize = 54;
165
166pub const BW_UPLOAD_RATE: usize = 55;
170pub const BW_DOWNLOAD_RATE: usize = 56;
172pub const BW_UPLOAD_RATE_TCP: usize = 57;
174pub const BW_DOWNLOAD_RATE_TCP: usize = 58;
176pub const BW_UPLOAD_RATE_UTP: usize = 59;
178pub const BW_DOWNLOAD_RATE_UTP: usize = 60;
180pub const BW_PAYLOAD_UPLOAD_RATE: usize = 61;
182pub const BW_PAYLOAD_DOWNLOAD_RATE: usize = 62;
184pub const BW_TOTAL_UPLOADED: usize = 63;
186pub const BW_TOTAL_DOWNLOADED: usize = 64;
188
189pub const SES_ACTIVE_TORRENTS: usize = 65;
193pub const SES_NUM_TORRENTS: usize = 66;
195pub const SES_UPTIME_SECS: usize = 67;
197pub const SES_IP_FILTER_BLOCKED: usize = 68;
199pub const SES_QUEUE_PAUSED_BY_AUTO: usize = 69;
201
202pub const NUM_METRICS: usize = 70;
204
205pub fn session_stats_metrics() -> &'static [SessionStatsMetric] {
213 use MetricKind::*;
214 static METRICS: [SessionStatsMetric; NUM_METRICS] = [
215 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 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 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 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 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 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 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
506pub 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 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 #[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 #[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 #[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 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 vals[SES_UPTIME_SECS] = self.started_at.elapsed().as_secs() as i64;
566
567 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 pub fn len(&self) -> usize {
580 NUM_METRICS
581 }
582
583 pub fn is_empty(&self) -> bool {
585 false
586 }
587
588 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#[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 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; }
778 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}