diff --git a/cSpell.json b/cSpell.json index fcbf53f1f..76939c199 100644 --- a/cSpell.json +++ b/cSpell.json @@ -34,6 +34,7 @@ "chrono", "ciphertext", "clippy", + "cloneable", "codecov", "codegen", "completei", @@ -174,6 +175,7 @@ "trackerid", "Trackon", "typenum", + "udpv", "Unamed", "underflows", "Unsendable", diff --git a/packages/udp-tracker-server/src/statistics/metrics.rs b/packages/udp-tracker-server/src/statistics/metrics.rs index c50966bc6..3c162ff02 100644 --- a/packages/udp-tracker-server/src/statistics/metrics.rs +++ b/packages/udp-tracker-server/src/statistics/metrics.rs @@ -276,3 +276,784 @@ impl Metrics { .unwrap_or_default() as u64 } } + +#[cfg(test)] +mod tests { + use torrust_tracker_clock::clock::Time; + use torrust_tracker_metrics::metric_name; + + use super::*; + use crate::statistics::{ + UDP_TRACKER_SERVER_ERRORS_TOTAL, UDP_TRACKER_SERVER_IPS_BANNED_TOTAL, + UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS, UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL, + UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL, UDP_TRACKER_SERVER_REQUESTS_BANNED_TOTAL, + UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL, UDP_TRACKER_SERVER_RESPONSES_SENT_TOTAL, + }; + use crate::CurrentClock; + + #[test] + fn it_should_implement_default() { + let metrics = Metrics::default(); + // MetricCollection starts with empty collections + assert_eq!(metrics, Metrics::default()); + } + + #[test] + fn it_should_implement_debug() { + let metrics = Metrics::default(); + let debug_string = format!("{metrics:?}"); + assert!(debug_string.contains("Metrics")); + assert!(debug_string.contains("metric_collection")); + } + + #[test] + fn it_should_implement_partial_eq() { + let metrics1 = Metrics::default(); + let metrics2 = Metrics::default(); + assert_eq!(metrics1, metrics2); + } + + #[test] + fn it_should_increase_counter_metric() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + let result = metrics.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now); + + assert!(result.is_ok()); + } + + #[test] + fn it_should_increase_counter_metric_with_labels() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet")]); + + let result = metrics.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels, now); + + assert!(result.is_ok()); + } + + #[test] + fn it_should_set_gauge_metric() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + let result = metrics.set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 42.0, now); + + assert!(result.is_ok()); + } + + #[test] + fn it_should_set_gauge_metric_with_labels() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("request_kind", "connect")]); + + let result = metrics.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels, + 1000.0, + now, + ); + + assert!(result.is_ok()); + } + + mod udp_general_metrics { + use super::*; + + #[test] + fn it_should_return_zero_for_udp_requests_aborted_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp_requests_aborted(), 0); + } + + #[test] + fn it_should_return_sum_of_udp_requests_aborted() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now) + .unwrap(); + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now) + .unwrap(); + + assert_eq!(metrics.udp_requests_aborted(), 2); + } + + #[test] + fn it_should_return_zero_for_udp_requests_banned_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp_requests_banned(), 0); + } + + #[test] + fn it_should_return_sum_of_udp_requests_banned() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + for _ in 0..3 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_BANNED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp_requests_banned(), 3); + } + + #[test] + fn it_should_return_zero_for_udp_banned_ips_total_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp_banned_ips_total(), 0); + } + + #[test] + fn it_should_return_gauge_value_for_udp_banned_ips_total() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + metrics + .set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 10.0, now) + .unwrap(); + + assert_eq!(metrics.udp_banned_ips_total(), 10); + } + } + + mod udp_performance_metrics { + use super::*; + + #[test] + fn it_should_return_zero_for_udp_avg_connect_processing_time_ns_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp_avg_connect_processing_time_ns(), 0); + } + + #[test] + fn it_should_return_gauge_value_for_udp_avg_connect_processing_time_ns() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("request_kind", "connect")]); + + metrics + .set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels, + 1500.0, + now, + ) + .unwrap(); + + assert_eq!(metrics.udp_avg_connect_processing_time_ns(), 1500); + } + + #[test] + fn it_should_return_zero_for_udp_avg_announce_processing_time_ns_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp_avg_announce_processing_time_ns(), 0); + } + + #[test] + fn it_should_return_gauge_value_for_udp_avg_announce_processing_time_ns() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("request_kind", "announce")]); + + metrics + .set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels, + 2500.0, + now, + ) + .unwrap(); + + assert_eq!(metrics.udp_avg_announce_processing_time_ns(), 2500); + } + + #[test] + fn it_should_return_zero_for_udp_avg_scrape_processing_time_ns_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp_avg_scrape_processing_time_ns(), 0); + } + + #[test] + fn it_should_return_gauge_value_for_udp_avg_scrape_processing_time_ns() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("request_kind", "scrape")]); + + metrics + .set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels, + 3500.0, + now, + ) + .unwrap(); + + assert_eq!(metrics.udp_avg_scrape_processing_time_ns(), 3500); + } + } + + mod udpv4_metrics { + use super::*; + + #[test] + fn it_should_return_zero_for_udp4_requests_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp4_requests(), 0); + } + + #[test] + fn it_should_return_sum_of_udp4_requests() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet")]); + + for _ in 0..5 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_requests(), 5); + } + + #[test] + fn it_should_return_zero_for_udp4_connections_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp4_connections_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp4_connections_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "connect")]); + + for _ in 0..3 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_connections_handled(), 3); + } + + #[test] + fn it_should_return_zero_for_udp4_announces_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp4_announces_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp4_announces_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "announce")]); + + for _ in 0..7 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_announces_handled(), 7); + } + + #[test] + fn it_should_return_zero_for_udp4_scrapes_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp4_scrapes_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp4_scrapes_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "scrape")]); + + for _ in 0..4 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_scrapes_handled(), 4); + } + + #[test] + fn it_should_return_zero_for_udp4_responses_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp4_responses(), 0); + } + + #[test] + fn it_should_return_sum_of_udp4_responses() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet")]); + + for _ in 0..6 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_RESPONSES_SENT_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_responses(), 6); + } + + #[test] + fn it_should_return_zero_for_udp4_errors_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp4_errors_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp4_errors_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet")]); + + for _ in 0..2 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_ERRORS_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_errors_handled(), 2); + } + } + + mod udpv6_metrics { + use super::*; + + #[test] + fn it_should_return_zero_for_udp6_requests_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp6_requests(), 0); + } + + #[test] + fn it_should_return_sum_of_udp6_requests() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet6")]); + + for _ in 0..8 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp6_requests(), 8); + } + + #[test] + fn it_should_return_zero_for_udp6_connections_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp6_connections_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp6_connections_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "connect")]); + + for _ in 0..4 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp6_connections_handled(), 4); + } + + #[test] + fn it_should_return_zero_for_udp6_announces_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp6_announces_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp6_announces_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "announce")]); + + for _ in 0..9 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp6_announces_handled(), 9); + } + + #[test] + fn it_should_return_zero_for_udp6_scrapes_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp6_scrapes_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp6_scrapes_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "scrape")]); + + for _ in 0..6 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp6_scrapes_handled(), 6); + } + + #[test] + fn it_should_return_zero_for_udp6_responses_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp6_responses(), 0); + } + + #[test] + fn it_should_return_sum_of_udp6_responses() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet6")]); + + for _ in 0..11 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_RESPONSES_SENT_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp6_responses(), 11); + } + + #[test] + fn it_should_return_zero_for_udp6_errors_handled_when_no_data() { + let metrics = Metrics::default(); + assert_eq!(metrics.udp6_errors_handled(), 0); + } + + #[test] + fn it_should_return_sum_of_udp6_errors_handled() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("server_binding_address_ip_family", "inet6")]); + + for _ in 0..3 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_ERRORS_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp6_errors_handled(), 3); + } + } + + mod combined_metrics { + use super::*; + + #[test] + fn it_should_distinguish_between_ipv4_and_ipv6_metrics() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + + let ipv4_labels = LabelSet::from([("server_binding_address_ip_family", "inet")]); + let ipv6_labels = LabelSet::from([("server_binding_address_ip_family", "inet6")]); + + // Add different counts for IPv4 and IPv6 + for _ in 0..3 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &ipv4_labels, now) + .unwrap(); + } + + for _ in 0..7 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &ipv6_labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_requests(), 3); + assert_eq!(metrics.udp6_requests(), 7); + } + + #[test] + fn it_should_distinguish_between_different_request_kinds() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + + let connect_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "connect")]); + let announce_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "announce")]); + let scrape_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "scrape")]); + + // Add different counts for different request kinds + for _ in 0..2 { + metrics + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), + &connect_labels, + now, + ) + .unwrap(); + } + + for _ in 0..5 { + metrics + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), + &announce_labels, + now, + ) + .unwrap(); + } + + for _ in 0..1 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &scrape_labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp4_connections_handled(), 2); + assert_eq!(metrics.udp4_announces_handled(), 5); + assert_eq!(metrics.udp4_scrapes_handled(), 1); + } + + #[test] + fn it_should_handle_mixed_ipv4_and_ipv6_for_different_request_kinds() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + + let ipv4_connect_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "connect")]); + let ipv6_connect_labels = + LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "connect")]); + let ipv4_announce_labels = + LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "announce")]); + let ipv6_announce_labels = + LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "announce")]); + + // Add mixed IPv4/IPv6 counts + for _ in 0..3 { + metrics + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), + &ipv4_connect_labels, + now, + ) + .unwrap(); + } + + for _ in 0..2 { + metrics + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), + &ipv6_connect_labels, + now, + ) + .unwrap(); + } + + for _ in 0..4 { + metrics + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), + &ipv4_announce_labels, + now, + ) + .unwrap(); + } + + for _ in 0..6 { + metrics + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), + &ipv6_announce_labels, + now, + ) + .unwrap(); + } + + assert_eq!(metrics.udp4_connections_handled(), 3); + assert_eq!(metrics.udp6_connections_handled(), 2); + assert_eq!(metrics.udp4_announces_handled(), 4); + assert_eq!(metrics.udp6_announces_handled(), 6); + } + } + + mod edge_cases { + use super::*; + + #[test] + fn it_should_handle_large_counter_values() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Add a large number of increments + for _ in 0..1000 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now) + .unwrap(); + } + + assert_eq!(metrics.udp_requests_aborted(), 1000); + } + + #[test] + fn it_should_handle_large_gauge_values() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Set a large gauge value + metrics + .set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 999_999.0, now) + .unwrap(); + + assert_eq!(metrics.udp_banned_ips_total(), 999_999); + } + + #[test] + fn it_should_handle_zero_gauge_values() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + metrics + .set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 0.0, now) + .unwrap(); + + assert_eq!(metrics.udp_banned_ips_total(), 0); + } + + #[test] + fn it_should_handle_fractional_gauge_values_with_truncation() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::from([("request_kind", "connect")]); + + metrics + .set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels, + 1234.567, + now, + ) + .unwrap(); + + // Should truncate to 1234 + assert_eq!(metrics.udp_avg_connect_processing_time_ns(), 1234); + } + + #[test] + fn it_should_overwrite_gauge_values_when_set_multiple_times() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Set initial value + metrics + .set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 50.0, now) + .unwrap(); + + assert_eq!(metrics.udp_banned_ips_total(), 50); + + // Overwrite with new value + metrics + .set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 75.0, now) + .unwrap(); + + assert_eq!(metrics.udp_banned_ips_total(), 75); + } + + #[test] + fn it_should_handle_empty_label_sets() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let empty_labels = LabelSet::empty(); + + let result = metrics.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &empty_labels, now); + + assert!(result.is_ok()); + assert_eq!(metrics.udp_requests_aborted(), 1); + } + + #[test] + fn it_should_handle_multiple_labels_on_same_metric() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + + let labels1 = LabelSet::from([("server_binding_address_ip_family", "inet")]); + let labels2 = LabelSet::from([("server_binding_address_ip_family", "inet6")]); + + // Add to same metric with different labels + for _ in 0..3 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels1, now) + .unwrap(); + } + + for _ in 0..5 { + metrics + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels2, now) + .unwrap(); + } + + // Should return labeled sums correctly + assert_eq!(metrics.udp4_requests(), 3); + assert_eq!(metrics.udp6_requests(), 5); + } + } + + mod error_handling { + use super::*; + + #[test] + fn it_should_return_ok_result_for_valid_counter_operations() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + let result = metrics.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now); + + assert!(result.is_ok()); + } + + #[test] + fn it_should_return_ok_result_for_valid_gauge_operations() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + let result = metrics.set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 42.0, now); + + assert!(result.is_ok()); + } + + #[test] + fn it_should_handle_unknown_metric_names_gracefully() { + let mut metrics = Metrics::default(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // This should still work as metrics are created on demand + let result = metrics.increase_counter(&metric_name!("unknown_metric"), &labels, now); + + assert!(result.is_ok()); + } + } +} diff --git a/packages/udp-tracker-server/src/statistics/mod.rs b/packages/udp-tracker-server/src/statistics/mod.rs index b42a73f27..768722ba3 100644 --- a/packages/udp-tracker-server/src/statistics/mod.rs +++ b/packages/udp-tracker-server/src/statistics/mod.rs @@ -73,9 +73,7 @@ pub fn describe_metrics() -> Metrics { metrics.metric_collection.describe_gauge( &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), Some(Unit::Nanoseconds), - Some(MetricDescription::new( - "Average time to process a UDP connect request in nanoseconds", - )), + Some(MetricDescription::new("Average time to process a UDP request in nanoseconds")), ); metrics diff --git a/packages/udp-tracker-server/src/statistics/repository.rs b/packages/udp-tracker-server/src/statistics/repository.rs index 1851b78a8..eb0951614 100644 --- a/packages/udp-tracker-server/src/statistics/repository.rs +++ b/packages/udp-tracker-server/src/statistics/repository.rs @@ -89,6 +89,14 @@ impl Repository { drop(stats_lock); + tracing::debug!( + "Recalculated UDP average connect processing time: {} ns (previous: {} ns, req_processing_time: {} ns, udp_connections_handled: {})", + new_avg, + previous_avg, + req_processing_time, + udp_connections_handled + ); + new_avg } @@ -109,6 +117,14 @@ impl Repository { drop(stats_lock); + tracing::debug!( + "Recalculated UDP average announce processing time: {} ns (previous: {} ns, req_processing_time: {} ns, udp_announces_handled: {})", + new_avg, + previous_avg, + req_processing_time, + udp_announces_handled + ); + new_avg } @@ -128,6 +144,526 @@ impl Repository { drop(stats_lock); + tracing::debug!( + "Recalculated UDP average scrape processing time: {} ns (previous: {} ns, req_processing_time: {} ns, udp_scrapes_handled: {})", + new_avg, + previous_avg, + req_processing_time, + udp_scrapes_handled + ); + new_avg } } + +#[cfg(test)] +mod tests { + use core::f64; + use std::time::Duration; + + use torrust_tracker_clock::clock::Time; + use torrust_tracker_metrics::metric_name; + + use super::*; + use crate::statistics::*; + use crate::CurrentClock; + + #[test] + fn it_should_implement_default() { + let repo = Repository::default(); + assert!(!std::ptr::eq(&repo.stats, &Repository::new().stats)); + } + + #[test] + fn it_should_be_cloneable() { + let repo = Repository::new(); + let cloned_repo = repo.clone(); + assert!(!std::ptr::eq(&repo.stats, &cloned_repo.stats)); + } + + #[tokio::test] + async fn it_should_be_initialized_with_described_metrics() { + let repo = Repository::new(); + let stats = repo.get_stats().await; + + // Check that the described metrics are present + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL))); + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_BANNED_TOTAL))); + assert!(stats + .metric_collection + .contains_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL))); + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_CONNECTION_ID_ERRORS_TOTAL))); + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL))); + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL))); + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_RESPONSES_SENT_TOTAL))); + assert!(stats + .metric_collection + .contains_counter(&metric_name!(UDP_TRACKER_SERVER_ERRORS_TOTAL))); + assert!(stats + .metric_collection + .contains_gauge(&metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS))); + } + + #[tokio::test] + async fn it_should_return_a_read_guard_to_metrics() { + let repo = Repository::new(); + let stats = repo.get_stats().await; + + // Should be able to read metrics through the guard + assert_eq!(stats.udp_requests_aborted(), 0); + assert_eq!(stats.udp_requests_banned(), 0); + } + + #[tokio::test] + async fn it_should_allow_increasing_a_counter_metric_successfully() { + let repo = Repository::new(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Increase a counter metric + let result = repo + .increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now) + .await; + + assert!(result.is_ok()); + + // Verify the counter was incremented + let stats = repo.get_stats().await; + assert_eq!(stats.udp_requests_aborted(), 1); + } + + #[tokio::test] + async fn it_should_allow_increasing_a_counter_multiple_times() { + let repo = Repository::new(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Increase counter multiple times + for _ in 0..5 { + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), &labels, now) + .await + .unwrap(); + } + + // Verify the counter was incremented correctly + let stats = repo.get_stats().await; + assert_eq!(stats.udp_requests_aborted(), 5); + } + + #[tokio::test] + async fn it_should_allow_increasing_a_counter_with_different_labels() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + let labels_ipv4 = LabelSet::from([("server_binding_address_ip_family", "inet")]); + let labels_ipv6 = LabelSet::from([("server_binding_address_ip_family", "inet6")]); + + // Increase counters with different labels + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels_ipv4, now) + .await + .unwrap(); + + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_RECEIVED_TOTAL), &labels_ipv6, now) + .await + .unwrap(); + + // Verify both labeled metrics + let stats = repo.get_stats().await; + assert_eq!(stats.udp4_requests(), 1); + assert_eq!(stats.udp6_requests(), 1); + } + + #[tokio::test] + async fn it_should_set_a_gauge_metric_successfully() { + let repo = Repository::new(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Set a gauge metric + let result = repo + .set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 42.0, now) + .await; + + assert!(result.is_ok()); + + // Verify the gauge was set + let stats = repo.get_stats().await; + assert_eq!(stats.udp_banned_ips_total(), 42); + } + + #[tokio::test] + async fn it_should_overwrite_previous_value_when_setting_a_gauge_with_a_previous_value() { + let repo = Repository::new(); + let now = CurrentClock::now(); + let labels = LabelSet::empty(); + + // Set gauge to initial value + repo.set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 10.0, now) + .await + .unwrap(); + + // Overwrite with new value + repo.set_gauge(&metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), &labels, 25.0, now) + .await + .unwrap(); + + // Verify the gauge has the new value + let stats = repo.get_stats().await; + assert_eq!(stats.udp_banned_ips_total(), 25); + } + + #[tokio::test] + async fn it_should_allow_setting_a_gauge_with_different_labels() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + let labels_connect = LabelSet::from([("request_kind", "connect")]); + let labels_announce = LabelSet::from([("request_kind", "announce")]); + + // Set gauges with different labels + repo.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels_connect, + 1000.0, + now, + ) + .await + .unwrap(); + + repo.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &labels_announce, + 2000.0, + now, + ) + .await + .unwrap(); + + // Verify both labeled metrics + let stats = repo.get_stats().await; + assert_eq!(stats.udp_avg_connect_processing_time_ns(), 1000); + assert_eq!(stats.udp_avg_announce_processing_time_ns(), 2000); + } + + #[tokio::test] + async fn it_should_recalculate_the_udp_average_connect_processing_time_in_nanoseconds_using_moving_average() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Set up initial connections handled + let ipv4_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "connect")]); + let ipv6_labels = LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "connect")]); + + // Simulate 2 IPv4 and 1 IPv6 connections + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv4_labels, now) + .await + .unwrap(); + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv4_labels, now) + .await + .unwrap(); + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv6_labels, now) + .await + .unwrap(); + + // Set initial average to 1000ns + let connect_labels = LabelSet::from([("request_kind", "connect")]); + repo.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &connect_labels, + 1000.0, + now, + ) + .await + .unwrap(); + + // Calculate new average with processing time of 2000ns + let processing_time = Duration::from_nanos(2000); + let new_avg = repo.recalculate_udp_avg_connect_processing_time_ns(processing_time).await; + + // Moving average: previous_avg + (new_value - previous_avg) / total_connections + // 1000 + (2000 - 1000) / 3 = 1000 + 333.33 = 1333.33 + let expected_avg = 1000.0 + (2000.0 - 1000.0) / 3.0; + assert!( + (new_avg - expected_avg).abs() < 0.01, + "Expected {expected_avg}, got {new_avg}" + ); + } + + #[tokio::test] + async fn it_should_recalculate_the_udp_average_announce_processing_time_in_nanoseconds_using_moving_average() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Set up initial announces handled + let ipv4_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "announce")]); + let ipv6_labels = LabelSet::from([("server_binding_address_ip_family", "inet6"), ("request_kind", "announce")]); + + // Simulate 3 IPv4 and 2 IPv6 announces + for _ in 0..3 { + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv4_labels, now) + .await + .unwrap(); + } + for _ in 0..2 { + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv6_labels, now) + .await + .unwrap(); + } + + // Set initial average to 500ns + let announce_labels = LabelSet::from([("request_kind", "announce")]); + repo.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &announce_labels, + 500.0, + now, + ) + .await + .unwrap(); + + // Calculate new average with processing time of 1500ns + let processing_time = Duration::from_nanos(1500); + let new_avg = repo.recalculate_udp_avg_announce_processing_time_ns(processing_time).await; + + // Moving average: previous_avg + (new_value - previous_avg) / total_announces + // 500 + (1500 - 500) / 5 = 500 + 200 = 700 + let expected_avg = 500.0 + (1500.0 - 500.0) / 5.0; + assert!( + (new_avg - expected_avg).abs() < 0.01, + "Expected {expected_avg}, got {new_avg}" + ); + } + + #[tokio::test] + async fn it_should_recalculate_the_udp_average_scrape_processing_time_in_nanoseconds_using_moving_average() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Set up initial scrapes handled + let ipv4_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "scrape")]); + + // Simulate 4 IPv4 scrapes + for _ in 0..4 { + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv4_labels, now) + .await + .unwrap(); + } + + // Set initial average to 800ns + let scrape_labels = LabelSet::from([("request_kind", "scrape")]); + repo.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_PERFORMANCE_AVG_PROCESSING_TIME_NS), + &scrape_labels, + 800.0, + now, + ) + .await + .unwrap(); + + // Calculate new average with processing time of 1200ns + let processing_time = Duration::from_nanos(1200); + let new_avg = repo.recalculate_udp_avg_scrape_processing_time_ns(processing_time).await; + + // Moving average: previous_avg + (new_value - previous_avg) / total_scrapes + // 800 + (1200 - 800) / 4 = 800 + 100 = 900 + let expected_avg = 800.0 + (1200.0 - 800.0) / 4.0; + assert!( + (new_avg - expected_avg).abs() < 0.01, + "Expected {expected_avg}, got {new_avg}" + ); + } + + #[tokio::test] + async fn recalculate_average_methods_should_handle_zero_connections_gracefully() { + let repo = Repository::new(); + + // Test with zero connections (should not panic, should handle division by zero) + let processing_time = Duration::from_nanos(1000); + + let connect_avg = repo.recalculate_udp_avg_connect_processing_time_ns(processing_time).await; + let announce_avg = repo.recalculate_udp_avg_announce_processing_time_ns(processing_time).await; + let scrape_avg = repo.recalculate_udp_avg_scrape_processing_time_ns(processing_time).await; + + // With 0 total connections, the formula becomes 0 + (1000 - 0) / 0 + // This should handle the division by zero case gracefully + assert!(connect_avg.is_infinite() || connect_avg.is_nan()); + assert!(announce_avg.is_infinite() || announce_avg.is_nan()); + assert!(scrape_avg.is_infinite() || scrape_avg.is_nan()); + } + + #[tokio::test] + async fn it_should_handle_concurrent_access() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Spawn multiple concurrent tasks + let mut handles = vec![]; + + for i in 0..10 { + let repo_clone = repo.clone(); + let handle = tokio::spawn(async move { + for _ in 0..5 { + repo_clone + .increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), + &LabelSet::empty(), + now, + ) + .await + .unwrap(); + } + i + }); + handles.push(handle); + } + + // Wait for all tasks to complete + for handle in handles { + handle.await.unwrap(); + } + + // Verify all increments were properly recorded + let stats = repo.get_stats().await; + assert_eq!(stats.udp_requests_aborted(), 50); // 10 tasks * 5 increments each + } + + #[tokio::test] + async fn it_should_handle_large_processing_times() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Set up a connection + let ipv4_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "connect")]); + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv4_labels, now) + .await + .unwrap(); + + // Test with very large processing time + let large_duration = Duration::from_secs(1); // 1 second = 1,000,000,000 ns + let new_avg = repo.recalculate_udp_avg_connect_processing_time_ns(large_duration).await; + + // Should handle large numbers without overflow + assert!(new_avg > 0.0); + assert!(new_avg.is_finite()); + } + + #[tokio::test] + async fn it_should_maintain_consistency_across_operations() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Perform a series of operations + repo.increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_ABORTED_TOTAL), + &LabelSet::empty(), + now, + ) + .await + .unwrap(); + + repo.set_gauge( + &metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), + &LabelSet::empty(), + 10.0, + now, + ) + .await + .unwrap(); + + repo.increase_counter( + &metric_name!(UDP_TRACKER_SERVER_REQUESTS_BANNED_TOTAL), + &LabelSet::empty(), + now, + ) + .await + .unwrap(); + + // Check final state + let stats = repo.get_stats().await; + assert_eq!(stats.udp_requests_aborted(), 1); + assert_eq!(stats.udp_banned_ips_total(), 10); + assert_eq!(stats.udp_requests_banned(), 1); + } + + #[tokio::test] + async fn it_should_handle_error_cases_gracefully() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // Test with invalid metric name (this should still work as metrics are created dynamically) + let result = repo + .increase_counter(&metric_name!("non_existent_metric"), &LabelSet::empty(), now) + .await; + + // Should succeed as metrics are created on demand + assert!(result.is_ok()); + + // Test with NaN value for gauge + let result = repo + .set_gauge( + &metric_name!(UDP_TRACKER_SERVER_IPS_BANNED_TOTAL), + &LabelSet::empty(), + f64::NAN, + now, + ) + .await; + + // Should handle NaN values + assert!(result.is_ok()); + } + + #[tokio::test] + async fn it_should_handle_moving_average_calculation_before_any_connections_are_recorded() { + let repo = Repository::new(); + let now = CurrentClock::now(); + + // This test checks the behavior of `recalculate_udp_avg_connect_processing_time_ns`` + // when no connections have been recorded yet. The first call should + // handle division by zero gracefully and return an infinite average, + // which is the current behavior. + + // todo: the first average should be 2000ns, not infinity. + // This is because the first connection is not counted in the average + // calculation if the counter is increased after calculating the average. + // The problem is that we count requests when they are accepted, not + // when they are processed. And we calculate the average when the + // response is sent. + + // First calculation: no connections recorded yet, should result in infinity + let processing_time_1 = Duration::from_nanos(2000); + let avg_1 = repo.recalculate_udp_avg_connect_processing_time_ns(processing_time_1).await; + + // Division by zero: 1000 + (2000 - 1000) / 0 = infinity + assert!( + avg_1.is_infinite(), + "First calculation should be infinite due to division by zero" + ); + + // Now add one connection and try again + let ipv4_labels = LabelSet::from([("server_binding_address_ip_family", "inet"), ("request_kind", "connect")]); + repo.increase_counter(&metric_name!(UDP_TRACKER_SERVER_REQUESTS_ACCEPTED_TOTAL), &ipv4_labels, now) + .await + .unwrap(); + + // Second calculation: 1 connection, but previous average is infinity + let processing_time_2 = Duration::from_nanos(3000); + let avg_2 = repo.recalculate_udp_avg_connect_processing_time_ns(processing_time_2).await; + + assert!( + (avg_2 - 3000.0).abs() < f64::EPSILON, + "Second calculation should be 3000ns, but got {avg_2}" + ); + } +}