#![allow(clippy::cast_precision_loss, clippy::float_cmp)]
use super::types::{ActiveConnections, BackendHealthStatus, ErrorRatePercent};
use crate::types::{BackendId, BackendToClientBytes, ClientToBackendBytes};
use std::sync::Arc;
use std::time::Duration;
use super::BackendStats;
use super::UserStats;
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct MetricsSnapshot {
pub total_connections: u64,
#[serde(skip, default)]
pub active_connections: usize,
#[serde(skip, default)]
pub stateful_sessions: usize,
pub client_to_backend_bytes: ClientToBackendBytes,
pub backend_to_client_bytes: BackendToClientBytes,
#[serde(skip, default)]
pub uptime: Duration,
pub backend_stats: Arc<[BackendStats]>,
pub user_stats: Vec<UserStats>,
#[serde(skip, default)]
pub cache_entries: u64,
#[serde(skip, default)]
pub cache_size_bytes: u64,
#[serde(skip, default)]
pub cache_hit_rate: f64,
#[serde(skip, default)]
pub disk_cache: Option<DiskCacheStats>,
pub pipeline_batches: u64,
pub pipeline_commands: u64,
pub pipeline_requests_queued: u64,
pub pipeline_requests_completed: u64,
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
pub struct DiskCacheStats {
pub disk_hits: u64,
pub disk_hit_rate: f64,
pub disk_capacity: u64,
pub bytes_written: u64,
pub bytes_read: u64,
pub write_ios: u64,
pub read_ios: u64,
}
impl MetricsSnapshot {
#[must_use]
pub fn with_pool_status(mut self, router: &crate::router::BackendSelector) -> Self {
use crate::pool::ConnectionProvider;
let backend_stats = Arc::make_mut(&mut self.backend_stats);
for stats in backend_stats {
if let Some(provider) = router.backend_provider(stats.backend_id) {
let pool_status = provider.status();
let active = pool_status
.created
.get()
.saturating_sub(pool_status.available.get());
stats.active_connections = ActiveConnections::new(active);
}
}
self
}
#[must_use]
pub fn format_uptime(&self) -> String {
let secs = self.uptime.as_secs();
let hours = secs / 3600;
let minutes = (secs % 3600) / 60;
let seconds = secs % 60;
if hours > 0 {
format!("{hours}h {minutes}m {seconds}s")
} else if minutes > 0 {
format!("{minutes}m {seconds}s")
} else {
format!("{seconds}s")
}
}
#[must_use]
#[inline]
pub const fn total_bytes(&self) -> u64 {
self.client_to_backend_bytes.as_u64() + self.backend_to_client_bytes.as_u64()
}
#[must_use]
pub fn throughput_bps(&self) -> f64 {
let secs = self.uptime.as_secs_f64();
if secs > 0.0 {
self.total_bytes() as f64 / secs
} else {
0.0
}
}
#[must_use]
#[inline]
pub fn total_commands(&self) -> u64 {
self.backend_stats
.iter()
.map(|stats| stats.total_commands.get())
.sum()
}
#[must_use]
#[inline]
pub fn total_errors(&self) -> u64 {
self.backend_stats
.iter()
.map(|stats| stats.errors.get())
.sum()
}
#[must_use]
pub fn error_rate_percent(&self) -> f64 {
let total_cmds = self.total_commands();
let total_errs = self.total_errors();
ErrorRatePercent::from_raw_counts(total_errs, total_cmds).get()
}
pub fn high_error_backends(&self) -> impl Iterator<Item = BackendId> + '_ {
self.backend_stats
.iter()
.filter(|stats| stats.has_high_error_rate())
.map(|stats| stats.backend_id)
}
pub fn healthy_backends(&self) -> impl Iterator<Item = BackendId> + '_ {
self.backend_stats
.iter()
.filter(|stats| stats.health_status == BackendHealthStatus::Healthy)
.map(|stats| stats.backend_id)
}
#[must_use]
pub fn backend(&self, backend_id: BackendId) -> Option<&BackendStats> {
self.backend_stats.get(backend_id.as_index())
}
#[must_use]
pub fn backend_health_counts(&self) -> (usize, usize, usize) {
self.backend_stats
.iter()
.fold((0, 0, 0), |(h, d, dn), stats| match stats.health_status {
BackendHealthStatus::Healthy => (h + 1, d, dn),
BackendHealthStatus::Degraded => (h, d + 1, dn),
BackendHealthStatus::Down => (h, d, dn + 1),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::metrics::{
ArticleCount, CommandCount, ErrorCount, FailureCount, RecvMicros, SendMicros, TtfbMicros,
};
use crate::types::BackendId;
fn create_test_snapshot() -> MetricsSnapshot {
use crate::types::{ArticleBytesTotal, BytesReceived, BytesSent, TimingMeasurementCount};
let backend1 = 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),
health_status: BackendHealthStatus::Healthy,
active_connections: ActiveConnections::new(3),
errors_4xx: ErrorCount::new(2),
errors_5xx: ErrorCount::new(3),
article_bytes_total: ArticleBytesTotal::new(5000),
article_count: ArticleCount::new(10),
ttfb_micros_total: TtfbMicros::new(1000),
ttfb_count: TimingMeasurementCount::new(10),
send_micros_total: SendMicros::new(500),
recv_micros_total: RecvMicros::new(1500),
connection_failures: FailureCount::new(0),
};
let backend2 = BackendStats {
backend_id: BackendId::from_index(1),
total_commands: CommandCount::new(50),
errors: ErrorCount::new(10),
bytes_sent: BytesSent::new(500),
bytes_received: BytesReceived::new(1500),
health_status: BackendHealthStatus::Degraded,
active_connections: ActiveConnections::new(2),
errors_4xx: ErrorCount::new(5),
errors_5xx: ErrorCount::new(5),
article_bytes_total: ArticleBytesTotal::new(2500),
article_count: ArticleCount::new(5),
ttfb_micros_total: TtfbMicros::new(500),
ttfb_count: TimingMeasurementCount::new(5),
send_micros_total: SendMicros::new(250),
recv_micros_total: RecvMicros::new(750),
connection_failures: FailureCount::new(1),
};
MetricsSnapshot {
total_connections: 5,
active_connections: 5,
stateful_sessions: 2,
client_to_backend_bytes: ClientToBackendBytes::new(1500),
backend_to_client_bytes: BackendToClientBytes::new(3500),
uptime: crate::constants::duration_polyfill::from_hours(1),
backend_stats: vec![backend1, backend2].into(),
user_stats: vec![],
cache_entries: 0,
cache_size_bytes: 0,
cache_hit_rate: 0.0,
disk_cache: None,
pipeline_batches: 0,
pipeline_commands: 0,
pipeline_requests_queued: 0,
pipeline_requests_completed: 0,
}
}
#[test]
fn test_format_uptime_hours() {
let snapshot = MetricsSnapshot {
uptime: Duration::from_secs(3661), ..Default::default()
};
assert_eq!(snapshot.format_uptime(), "1h 1m 1s");
}
#[test]
fn test_format_uptime_minutes() {
let snapshot = MetricsSnapshot {
uptime: Duration::from_secs(125), ..Default::default()
};
assert_eq!(snapshot.format_uptime(), "2m 5s");
}
#[test]
fn test_format_uptime_seconds() {
let snapshot = MetricsSnapshot {
uptime: Duration::from_secs(42),
..Default::default()
};
assert_eq!(snapshot.format_uptime(), "42s");
}
#[test]
fn test_format_uptime_zero() {
let snapshot = MetricsSnapshot {
uptime: Duration::from_secs(0),
..Default::default()
};
assert_eq!(snapshot.format_uptime(), "0s");
}
#[test]
fn test_total_bytes() {
let snapshot = create_test_snapshot();
assert_eq!(snapshot.total_bytes(), 5000); }
#[test]
fn test_total_bytes_zero() {
let snapshot = MetricsSnapshot::default();
assert_eq!(snapshot.total_bytes(), 0);
}
#[test]
fn test_throughput_bps() {
let snapshot = create_test_snapshot();
let expected = 5000.0 / 3600.0; assert!((snapshot.throughput_bps() - expected).abs() < 0.01);
}
#[test]
fn test_throughput_bps_zero_uptime() {
let snapshot = MetricsSnapshot {
client_to_backend_bytes: ClientToBackendBytes::new(1000),
backend_to_client_bytes: BackendToClientBytes::new(2000),
uptime: Duration::from_secs(0),
..Default::default()
};
assert_eq!(snapshot.throughput_bps(), 0.0);
}
#[test]
fn test_total_commands() {
let snapshot = create_test_snapshot();
assert_eq!(snapshot.total_commands(), 150); }
#[test]
fn test_total_commands_empty() {
let snapshot = MetricsSnapshot::default();
assert_eq!(snapshot.total_commands(), 0);
}
#[test]
fn test_total_errors() {
let snapshot = create_test_snapshot();
assert_eq!(snapshot.total_errors(), 15); }
#[test]
fn test_total_errors_empty() {
let snapshot = MetricsSnapshot::default();
assert_eq!(snapshot.total_errors(), 0);
}
#[test]
fn test_error_rate_percent() {
let snapshot = create_test_snapshot();
let expected = 15.0 / 150.0 * 100.0; assert!((snapshot.error_rate_percent() - expected).abs() < 0.01);
}
#[test]
fn test_error_rate_percent_zero_commands() {
let snapshot = MetricsSnapshot::default();
assert_eq!(snapshot.error_rate_percent(), 0.0);
}
#[test]
fn test_high_error_backends() {
let snapshot = create_test_snapshot();
let high_error: Vec<_> = snapshot.high_error_backends().collect();
assert_eq!(high_error.len(), 1);
assert_eq!(high_error[0], BackendId::from_index(1));
}
#[test]
fn test_high_error_backends_empty() {
let snapshot = MetricsSnapshot::default();
assert_eq!(snapshot.high_error_backends().count(), 0);
}
#[test]
fn test_healthy_backends() {
let snapshot = create_test_snapshot();
let healthy: Vec<_> = snapshot.healthy_backends().collect();
assert_eq!(healthy.len(), 1);
assert_eq!(healthy[0], BackendId::from_index(0));
}
#[test]
fn test_healthy_backends_all_down() {
let backend = BackendStats {
health_status: BackendHealthStatus::Down,
..Default::default()
};
let snapshot = MetricsSnapshot {
backend_stats: vec![backend].into(),
..Default::default()
};
assert_eq!(snapshot.healthy_backends().count(), 0);
}
#[test]
fn test_backend_by_id() {
let snapshot = create_test_snapshot();
let backend0 = snapshot.backend(BackendId::from_index(0));
assert!(backend0.is_some());
assert_eq!(backend0.unwrap().backend_id, BackendId::from_index(0));
assert_eq!(backend0.unwrap().total_commands.get(), 100);
let backend1 = snapshot.backend(BackendId::from_index(1));
assert!(backend1.is_some());
assert_eq!(backend1.unwrap().backend_id, BackendId::from_index(1));
assert_eq!(backend1.unwrap().total_commands.get(), 50);
}
#[test]
fn test_backend_by_id_out_of_range() {
let snapshot = create_test_snapshot();
let backend = snapshot.backend(BackendId::from_index(2));
assert!(backend.is_none());
}
#[test]
fn test_backend_health_counts() {
let snapshot = create_test_snapshot();
let (healthy, degraded, down) = snapshot.backend_health_counts();
assert_eq!(healthy, 1);
assert_eq!(degraded, 1);
assert_eq!(down, 0);
}
#[test]
fn test_backend_health_counts_mixed() {
let backends = vec![
BackendStats {
backend_id: BackendId::from_index(0),
health_status: BackendHealthStatus::Healthy,
..Default::default()
},
BackendStats {
backend_id: BackendId::from_index(1),
health_status: BackendHealthStatus::Healthy,
..Default::default()
},
BackendStats {
backend_id: BackendId::from_index(2),
health_status: BackendHealthStatus::Down,
..Default::default()
},
];
let snapshot = MetricsSnapshot {
backend_stats: backends.into(),
..Default::default()
};
let (healthy, degraded, down) = snapshot.backend_health_counts();
assert_eq!(healthy, 2);
assert_eq!(degraded, 0);
assert_eq!(down, 1);
}
#[test]
fn test_backend_health_counts_empty() {
let snapshot = MetricsSnapshot::default();
let (healthy, degraded, down) = snapshot.backend_health_counts();
assert_eq!(healthy, 0);
assert_eq!(degraded, 0);
assert_eq!(down, 0);
}
#[test]
fn with_pool_status_does_not_count_unopened_capacity_as_active() {
use crate::pool::DeadpoolConnectionProvider;
use crate::router::BackendSelector;
use crate::types::{BackendId, ServerName};
let provider = DeadpoolConnectionProvider::builder("127.0.0.1", 9)
.name("unused")
.max_connections(50)
.build()
.expect("provider should build without connecting");
let mut router = BackendSelector::new();
router.add_backend(
ServerName::try_new("unused".to_string()).unwrap(),
provider,
1,
);
let snapshot = MetricsSnapshot {
backend_stats: vec![BackendStats {
backend_id: BackendId::from_index(0),
..Default::default()
}]
.into(),
..Default::default()
}
.with_pool_status(&router);
assert_eq!(snapshot.backend_stats[0].active_connections.get(), 0);
}
#[test]
fn test_snapshot_default() {
let snapshot = MetricsSnapshot::default();
assert_eq!(snapshot.total_connections, 0);
assert_eq!(snapshot.active_connections, 0);
assert_eq!(snapshot.stateful_sessions, 0);
assert_eq!(snapshot.total_bytes(), 0);
assert_eq!(snapshot.uptime, Duration::from_secs(0));
assert_eq!(snapshot.backend_stats.len(), 0);
assert_eq!(snapshot.user_stats.len(), 0);
}
#[test]
fn test_snapshot_clone() {
let snapshot = create_test_snapshot();
let cloned = snapshot.clone();
assert_eq!(snapshot.total_connections, cloned.total_connections);
assert_eq!(snapshot.total_bytes(), cloned.total_bytes());
assert_eq!(snapshot.uptime, cloned.uptime);
assert!(Arc::ptr_eq(&snapshot.backend_stats, &cloned.backend_stats));
}
}