use super::types::{
ActiveConnections, ArticleCount, BackendHealthStatus, CommandCount, ErrorCount,
ErrorRatePercent, FailureCount, OverheadMillis, RecvMicros, SendMicros, TtfbMicros,
};
use crate::types::{
ArticleBytesTotal, BackendId, BytesReceived, BytesSent, TimingMeasurementCount,
};
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct BackendStats {
pub backend_id: BackendId,
#[serde(skip, default)]
pub active_connections: ActiveConnections,
pub total_commands: CommandCount,
pub bytes_sent: BytesSent,
pub bytes_received: BytesReceived,
pub errors: ErrorCount,
pub errors_4xx: ErrorCount,
pub errors_5xx: ErrorCount,
pub article_bytes_total: ArticleBytesTotal,
pub article_count: ArticleCount,
pub ttfb_micros_total: TtfbMicros,
pub ttfb_count: TimingMeasurementCount,
pub send_micros_total: SendMicros,
pub recv_micros_total: RecvMicros,
pub connection_failures: FailureCount,
#[serde(skip, default)]
pub health_status: BackendHealthStatus,
}
impl Default for BackendStats {
fn default() -> Self {
Self {
backend_id: BackendId::from_index(0),
active_connections: ActiveConnections::default(),
total_commands: CommandCount::default(),
bytes_sent: BytesSent::ZERO,
bytes_received: BytesReceived::ZERO,
errors: ErrorCount::default(),
errors_4xx: ErrorCount::default(),
errors_5xx: ErrorCount::default(),
article_bytes_total: ArticleBytesTotal::ZERO,
article_count: ArticleCount::default(),
ttfb_micros_total: TtfbMicros::default(),
ttfb_count: TimingMeasurementCount::ZERO,
send_micros_total: SendMicros::default(),
recv_micros_total: RecvMicros::default(),
connection_failures: FailureCount::default(),
health_status: BackendHealthStatus::Healthy,
}
}
}
impl BackendStats {
#[must_use]
#[inline]
pub const fn average_article_size(&self) -> Option<u64> {
self.article_count
.average_bytes(self.article_bytes_total.get())
}
#[must_use]
#[inline]
pub fn average_ttfb_ms(&self) -> Option<f64> {
std::num::NonZeroU64::new(self.ttfb_count.get())
.map(|count| TtfbMicros::average(self.ttfb_micros_total, count).get())
}
#[must_use]
#[inline]
pub fn average_send_ms(&self) -> Option<f64> {
std::num::NonZeroU64::new(self.ttfb_count.get())
.map(|count| SendMicros::average(self.send_micros_total, count).get())
}
#[must_use]
#[inline]
pub fn average_recv_ms(&self) -> Option<f64> {
std::num::NonZeroU64::new(self.ttfb_count.get())
.map(|count| RecvMicros::average(self.recv_micros_total, count).get())
}
#[must_use]
#[inline]
pub fn average_overhead_ms(&self) -> Option<f64> {
std::num::NonZeroU64::new(self.ttfb_count.get()).map(|count| {
let ttfb = TtfbMicros::average(self.ttfb_micros_total, count);
let send = SendMicros::average(self.send_micros_total, count);
let recv = RecvMicros::average(self.recv_micros_total, count);
OverheadMillis::from_components(ttfb, send, recv).get()
})
}
#[must_use]
#[inline]
pub fn error_rate_percent(&self) -> f64 {
ErrorRatePercent::from_counts(self.errors, self.total_commands).get()
}
#[must_use]
#[inline]
pub fn has_high_error_rate(&self) -> bool {
ErrorRatePercent::from_counts(self.errors, self.total_commands).is_high()
}
#[must_use]
#[inline]
pub fn is_healthy(&self) -> bool {
self.health_status == BackendHealthStatus::Healthy
}
#[must_use]
#[inline]
pub fn is_degraded(&self) -> bool {
self.health_status == BackendHealthStatus::Degraded
}
#[must_use]
#[inline]
pub fn is_down(&self) -> bool {
self.health_status == BackendHealthStatus::Down
}
#[must_use]
#[inline]
pub const fn total_bytes(&self) -> u64 {
self.bytes_sent
.as_u64()
.saturating_add(self.bytes_received.as_u64())
}
#[must_use]
#[inline]
pub const fn has_activity(&self) -> bool {
self.total_commands.get() > 0
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
use crate::types::BackendId;
fn create_test_stats() -> BackendStats {
BackendStats {
backend_id: BackendId::from_index(0),
total_commands: CommandCount::new(100),
errors: ErrorCount::new(5),
bytes_sent: BytesSent::new(1000),
bytes_received: BytesReceived::new(2000),
article_count: ArticleCount::new(10),
article_bytes_total: ArticleBytesTotal::new(5000),
ttfb_micros_total: TtfbMicros::new(10000),
ttfb_count: TimingMeasurementCount::new(10),
send_micros_total: SendMicros::new(3000),
recv_micros_total: RecvMicros::new(5000),
health_status: BackendHealthStatus::Healthy,
..Default::default()
}
}
#[test]
fn test_backend_stats_default() {
let stats = BackendStats::default();
assert_eq!(stats.backend_id, BackendId::from_index(0));
assert_eq!(stats.total_commands.get(), 0);
assert_eq!(stats.errors.get(), 0);
assert_eq!(stats.bytes_sent.as_u64(), 0);
assert_eq!(stats.bytes_received.as_u64(), 0);
assert_eq!(stats.health_status, BackendHealthStatus::Healthy);
}
#[test]
fn test_average_article_size() {
let stats = create_test_stats();
let avg = stats.average_article_size();
assert_eq!(avg, Some(500)); }
#[test]
fn test_average_article_size_zero_articles() {
let stats = BackendStats {
article_count: ArticleCount::new(0),
article_bytes_total: ArticleBytesTotal::new(1000),
..Default::default()
};
assert_eq!(stats.average_article_size(), None);
}
#[test]
fn test_average_article_size_zero_bytes() {
let stats = BackendStats {
article_count: ArticleCount::new(10),
article_bytes_total: ArticleBytesTotal::new(0),
..Default::default()
};
assert_eq!(stats.average_article_size(), Some(0));
}
#[test]
fn test_average_ttfb_ms() {
let stats = create_test_stats();
let avg = stats.average_ttfb_ms();
assert!(avg.is_some());
assert!((avg.unwrap() - 1.0).abs() < 0.01);
}
#[test]
fn test_average_ttfb_ms_zero_count() {
let stats = BackendStats {
ttfb_micros_total: TtfbMicros::new(1000),
ttfb_count: TimingMeasurementCount::new(0),
..Default::default()
};
assert_eq!(stats.average_ttfb_ms(), None);
}
#[test]
fn test_average_send_ms() {
let stats = create_test_stats();
let avg = stats.average_send_ms();
assert!(avg.is_some());
assert!((avg.unwrap() - 0.3).abs() < 0.01);
}
#[test]
fn test_average_send_ms_zero_count() {
let stats = BackendStats {
send_micros_total: SendMicros::new(1000),
ttfb_count: TimingMeasurementCount::new(0),
..Default::default()
};
assert_eq!(stats.average_send_ms(), None);
}
#[test]
fn test_average_recv_ms() {
let stats = create_test_stats();
let avg = stats.average_recv_ms();
assert!(avg.is_some());
assert!((avg.unwrap() - 0.5).abs() < 0.01);
}
#[test]
fn test_average_recv_ms_zero_count() {
let stats = BackendStats {
recv_micros_total: RecvMicros::new(1000),
ttfb_count: TimingMeasurementCount::new(0),
..Default::default()
};
assert_eq!(stats.average_recv_ms(), None);
}
#[test]
fn test_average_overhead_ms() {
let stats = create_test_stats();
let overhead = stats.average_overhead_ms();
assert!(overhead.is_some());
assert!((overhead.unwrap() - 0.2).abs() < 0.01);
}
#[test]
fn test_average_overhead_ms_zero_count() {
let stats = BackendStats::default();
assert_eq!(stats.average_overhead_ms(), None);
}
#[test]
fn test_error_rate_percent() {
let stats = create_test_stats();
let rate = stats.error_rate_percent();
assert!((rate - 5.0).abs() < 0.01); }
#[test]
fn test_error_rate_percent_zero_commands() {
let stats = BackendStats {
total_commands: CommandCount::new(0),
errors: ErrorCount::new(10),
..Default::default()
};
assert_eq!(stats.error_rate_percent(), 0.0);
}
#[test]
fn test_error_rate_percent_no_errors() {
let stats = BackendStats {
total_commands: CommandCount::new(100),
errors: ErrorCount::new(0),
..Default::default()
};
assert_eq!(stats.error_rate_percent(), 0.0);
}
#[test]
fn test_has_high_error_rate() {
let stats = BackendStats {
total_commands: CommandCount::new(100),
errors: ErrorCount::new(10),
..Default::default()
};
assert!(stats.has_high_error_rate());
let stats_threshold = BackendStats {
total_commands: CommandCount::new(100),
errors: ErrorCount::new(5),
..Default::default()
};
assert!(!stats_threshold.has_high_error_rate());
let stats_low = BackendStats {
total_commands: CommandCount::new(100),
errors: ErrorCount::new(3),
..Default::default()
};
assert!(!stats_low.has_high_error_rate());
}
#[test]
fn test_is_healthy() {
let stats = BackendStats {
health_status: BackendHealthStatus::Healthy,
..Default::default()
};
assert!(stats.is_healthy());
assert!(!stats.is_degraded());
assert!(!stats.is_down());
}
#[test]
fn test_is_degraded() {
let stats = BackendStats {
health_status: BackendHealthStatus::Degraded,
..Default::default()
};
assert!(!stats.is_healthy());
assert!(stats.is_degraded());
assert!(!stats.is_down());
}
#[test]
fn test_is_down() {
let stats = BackendStats {
health_status: BackendHealthStatus::Down,
..Default::default()
};
assert!(!stats.is_healthy());
assert!(!stats.is_degraded());
assert!(stats.is_down());
}
#[test]
fn test_total_bytes() {
let stats = create_test_stats();
assert_eq!(stats.total_bytes(), 3000); }
#[test]
fn test_total_bytes_zero() {
let stats = BackendStats::default();
assert_eq!(stats.total_bytes(), 0);
}
#[test]
fn test_total_bytes_saturating() {
let stats = BackendStats {
bytes_sent: BytesSent::new(u64::MAX),
bytes_received: BytesReceived::new(1),
..Default::default()
};
assert_eq!(stats.total_bytes(), u64::MAX);
}
#[test]
fn test_has_activity() {
let stats = BackendStats {
total_commands: CommandCount::new(1),
..Default::default()
};
assert!(stats.has_activity());
}
#[test]
fn test_has_activity_none() {
let stats = BackendStats::default();
assert!(!stats.has_activity());
}
#[test]
fn test_backend_stats_clone() {
let stats = create_test_stats();
let cloned = stats.clone();
assert_eq!(stats.backend_id, cloned.backend_id);
assert_eq!(stats.total_commands.get(), cloned.total_commands.get());
assert_eq!(stats.total_bytes(), cloned.total_bytes());
assert_eq!(stats.health_status, cloned.health_status);
}
}