use super::store::{BackendStore, MetricsStore, UserMetrics};
use super::{BackendHealthStatus, BackendStats, MetricsSnapshot, UserStats};
use crate::types::{BackendId, BackendToClientBytes, ClientToBackendBytes};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
use std::time::Instant;
#[derive(Debug)]
struct BackendMetrics {
active_connections: AtomicUsize,
health_status: AtomicU8,
}
impl Default for BackendMetrics {
fn default() -> Self {
Self {
active_connections: AtomicUsize::new(0),
health_status: AtomicU8::new(BackendHealthStatus::Healthy as u8),
}
}
}
impl BackendMetrics {
#[must_use]
pub fn to_backend_stats(&self, backend_id: BackendId, store: &BackendStore) -> BackendStats {
use super::types::{
ActiveConnections, ArticleCount, CommandCount, ErrorCount, FailureCount, RecvMicros,
SendMicros, TtfbMicros,
};
use crate::types::{ArticleBytesTotal, BytesReceived, BytesSent, TimingMeasurementCount};
BackendStats {
backend_id,
active_connections: ActiveConnections::new(
self.active_connections.load(Ordering::Relaxed),
),
total_commands: CommandCount::new(store.total_commands.load(Ordering::Relaxed)),
bytes_sent: BytesSent::new(store.bytes_sent.load(Ordering::Relaxed)),
bytes_received: BytesReceived::new(store.bytes_received.load(Ordering::Relaxed)),
errors: ErrorCount::new(store.errors.load(Ordering::Relaxed)),
errors_4xx: ErrorCount::new(store.errors_4xx.load(Ordering::Relaxed)),
errors_5xx: ErrorCount::new(store.errors_5xx.load(Ordering::Relaxed)),
article_bytes_total: ArticleBytesTotal::new(
store.article_bytes_total.load(Ordering::Relaxed),
),
article_count: ArticleCount::new(store.article_count.load(Ordering::Relaxed)),
ttfb_micros_total: TtfbMicros::new(store.ttfb_micros_total.load(Ordering::Relaxed)),
ttfb_count: TimingMeasurementCount::new(store.ttfb_count.load(Ordering::Relaxed)),
send_micros_total: SendMicros::new(store.send_micros_total.load(Ordering::Relaxed)),
recv_micros_total: RecvMicros::new(store.recv_micros_total.load(Ordering::Relaxed)),
connection_failures: FailureCount::new(
store.connection_failures.load(Ordering::Relaxed),
),
health_status: self.health_status.load(Ordering::Relaxed).into(),
}
}
}
#[derive(Debug, Clone)]
pub struct MetricsCollector {
inner: Arc<MetricsInner>,
}
#[derive(Debug)]
struct MetricsInner {
store: MetricsStore,
active_connections: AtomicUsize,
stateful_sessions: AtomicUsize,
backend_metrics: Vec<BackendMetrics>,
start_time: Instant,
}
impl MetricsCollector {
#[inline]
fn backend_get(&self, backend_id: BackendId) -> Option<&BackendMetrics> {
self.inner.backend_metrics.get(backend_id.as_index())
}
#[inline]
fn with_backend_store<F>(&self, backend_id: BackendId, action: F)
where
F: FnOnce(&BackendStore),
{
let idx = backend_id.as_index();
if let Some(store) = self.inner.store.backend_stores.get(idx) {
action(store);
}
}
#[inline]
fn update_user_metrics<F>(&self, username: Option<&str>, update: F)
where
F: Fn(&mut UserMetrics),
{
let key = username.unwrap_or(crate::constants::user::ANONYMOUS);
if let Some(mut entry) = self.inner.store.user_metrics.get_mut(key) {
update(&mut entry);
return;
}
let owned = key.to_string();
self.inner
.store
.user_metrics
.entry(owned)
.or_insert_with(|| UserMetrics::new(key.to_string()));
if let Some(mut entry) = self.inner.store.user_metrics.get_mut(key) {
update(&mut entry);
}
let pruned = self.inner.store.prune_user_metrics();
if pruned > 0 {
tracing::warn!(
pruned,
retained_users = self.inner.store.user_metrics.len(),
"Pruned inactive user metrics after new user insertion"
);
}
}
#[must_use]
pub fn new(num_backends: usize) -> Self {
Self::with_store(MetricsStore::new(num_backends))
}
pub fn with_store(store: MetricsStore) -> Self {
let pruned = store.prune_user_metrics();
if pruned > 0 {
tracing::warn!(
pruned,
retained_users = store.user_metrics.len(),
"Pruned restored user metrics before building collector"
);
}
let num_backends = store.backend_stores.len();
let backend_metrics = (0..num_backends)
.map(|_| BackendMetrics::default())
.collect();
Self {
inner: Arc::new(MetricsInner {
store,
active_connections: AtomicUsize::new(0),
stateful_sessions: AtomicUsize::new(0),
backend_metrics,
start_time: Instant::now(),
}),
}
}
pub fn save_to_disk(&self, path: &Path, server_names: &[String]) -> anyhow::Result<()> {
self.inner.store.save(path, server_names)
}
#[inline]
pub fn connection_opened(&self) {
self.inner
.store
.total_connections
.fetch_add(1, Ordering::Relaxed);
self.inner
.active_connections
.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn connection_closed(&self) {
self.inner
.active_connections
.fetch_sub(1, Ordering::Relaxed);
}
#[inline]
pub fn stateful_session_started(&self) {
self.inner.stateful_sessions.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn stateful_session_ended(&self) {
self.inner.stateful_sessions.fetch_sub(1, Ordering::Relaxed);
}
#[inline]
pub fn record_command(&self, backend_id: BackendId) {
self.with_backend_store(backend_id, |store| {
store.total_commands.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_connection_failure(&self, backend_id: BackendId) {
self.with_backend_store(backend_id, |store| {
store.connection_failures.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_error(&self, backend_id: BackendId) {
self.with_backend_store(backend_id, |store| {
store.errors.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_client_to_backend_bytes_for(&self, backend_id: BackendId, bytes: u64) {
self.with_backend_store(backend_id, |store| {
store.bytes_sent.fetch_add(bytes, Ordering::Relaxed);
});
}
#[inline]
pub fn record_backend_to_client_bytes_for(&self, backend_id: BackendId, bytes: u64) {
self.with_backend_store(backend_id, |store| {
store.bytes_received.fetch_add(bytes, Ordering::Relaxed);
});
}
#[inline]
pub fn backend_connection_opened(&self, backend_id: BackendId) {
if let Some(backend) = self.backend_get(backend_id) {
backend.active_connections.fetch_add(1, Ordering::Relaxed);
}
}
#[inline]
#[must_use]
pub const fn record_client_to_backend(
&self,
bytes: crate::types::MetricsBytes<crate::types::Unrecorded>,
) -> crate::types::MetricsBytes<crate::types::Recorded> {
crate::types::MetricsBytes::new(bytes.into_u64()).mark_recorded()
}
#[inline]
#[must_use]
pub const fn record_backend_to_client(
&self,
bytes: crate::types::MetricsBytes<crate::types::Unrecorded>,
) -> crate::types::MetricsBytes<crate::types::Recorded> {
crate::types::MetricsBytes::new(bytes.into_u64()).mark_recorded()
}
#[inline]
#[must_use]
pub fn record_command_execution(
&self,
backend_id: BackendId,
client_to_backend: crate::types::MetricsBytes<crate::types::Unrecorded>,
backend_to_client: crate::types::MetricsBytes<crate::types::Unrecorded>,
) -> (
crate::types::MetricsBytes<crate::types::Recorded>,
crate::types::MetricsBytes<crate::types::Recorded>,
) {
self.record_command(backend_id);
self.record_client_to_backend_bytes_for(backend_id, client_to_backend.peek());
self.record_backend_to_client_bytes_for(backend_id, backend_to_client.peek());
let sent_recorded = self.record_client_to_backend(client_to_backend);
let recv_recorded = self.record_backend_to_client(backend_to_client);
(sent_recorded, recv_recorded)
}
#[inline]
pub fn backend_connection_closed(&self, backend_id: BackendId) {
if let Some(backend) = self.backend_get(backend_id) {
backend.active_connections.fetch_sub(1, Ordering::Relaxed);
}
}
#[inline]
pub fn record_error_4xx(&self, backend_id: BackendId) {
self.with_backend_store(backend_id, |store| {
store.errors_4xx.fetch_add(1, Ordering::Relaxed);
store.errors.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_error_5xx(&self, backend_id: BackendId) {
self.with_backend_store(backend_id, |store| {
store.errors_5xx.fetch_add(1, Ordering::Relaxed);
store.errors.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_article(&self, backend_id: BackendId, bytes: u64) {
self.with_backend_store(backend_id, |store| {
store
.article_bytes_total
.fetch_add(bytes, Ordering::Relaxed);
store.article_count.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_ttfb_micros(&self, backend_id: BackendId, micros: u64) {
self.with_backend_store(backend_id, |store| {
store.ttfb_micros_total.fetch_add(micros, Ordering::Relaxed);
store.ttfb_count.fetch_add(1, Ordering::Relaxed);
});
}
#[inline]
pub fn record_send_recv_micros(
&self,
backend_id: BackendId,
send_micros: u64,
recv_micros: u64,
) {
self.with_backend_store(backend_id, |store| {
store
.send_micros_total
.fetch_add(send_micros, Ordering::Relaxed);
store
.recv_micros_total
.fetch_add(recv_micros, Ordering::Relaxed);
});
}
#[inline]
pub fn set_backend_health(&self, backend_id: BackendId, health: BackendHealthStatus) {
if let Some(backend) = self.backend_get(backend_id) {
backend
.health_status
.store(health.into(), Ordering::Relaxed);
}
}
pub fn user_connection_opened(&self, username: Option<&str>) {
self.update_user_metrics(username, |m| {
m.active_connections += 1;
m.total_connections += 1;
});
}
pub fn user_connection_closed(&self, username: Option<&str>) {
let key = username.unwrap_or(crate::constants::user::ANONYMOUS);
if let Some(mut m) = self.inner.store.user_metrics.get_mut(key) {
m.active_connections = m.active_connections.saturating_sub(1);
}
}
pub fn user_bytes_sent(&self, username: Option<&str>, bytes: u64) {
self.update_user_metrics(username, |m| m.bytes_sent += bytes);
}
pub fn user_bytes_received(&self, username: Option<&str>, bytes: u64) {
self.update_user_metrics(username, |m| m.bytes_received += bytes);
}
pub fn user_command(&self, username: Option<&str>) {
self.update_user_metrics(username, |m| m.total_commands += 1);
}
pub fn user_error(&self, username: Option<&str>) {
self.update_user_metrics(username, |m| m.errors += 1);
}
#[inline]
pub fn record_pipeline_batch(&self, batch_size: u64) {
self.inner
.store
.pipeline_batches
.fetch_add(1, Ordering::Relaxed);
self.inner
.store
.pipeline_commands
.fetch_add(batch_size, Ordering::Relaxed);
}
#[inline]
pub fn record_pipeline_enqueue(&self) {
self.inner
.store
.pipeline_requests_queued
.fetch_add(1, Ordering::Relaxed);
}
#[inline]
pub fn record_pipeline_complete(&self) {
self.inner
.store
.pipeline_requests_completed
.fetch_add(1, Ordering::Relaxed);
}
#[must_use]
pub fn snapshot(&self, cache: Option<&crate::cache::UnifiedCache>) -> MetricsSnapshot {
let cache_stats = cache.map(|c| c as &dyn crate::cache::CacheStatsProvider);
self.snapshot_with_cache(cache_stats)
}
#[must_use]
pub fn snapshot_with_cache(
&self,
cache: Option<&dyn crate::cache::CacheStatsProvider>,
) -> MetricsSnapshot {
let backend_stats: Vec<BackendStats> = self
.inner
.backend_metrics
.iter()
.enumerate()
.map(|(id, metrics)| {
let backend_id = BackendId::from_index(id);
let store = &self.inner.store.backend_stores[id];
metrics.to_backend_stats(backend_id, store)
})
.collect();
let user_stats: Vec<UserStats> = self
.inner
.store
.user_metrics
.iter()
.map(|entry| entry.value().to_user_stats())
.collect();
let total_sent: u64 = backend_stats.iter().map(|b| b.bytes_sent.as_u64()).sum();
let total_received: u64 = backend_stats
.iter()
.map(|b| b.bytes_received.as_u64())
.sum();
let (cache_entries, cache_size_bytes, cache_hit_rate, disk_cache) =
cache.map_or((0, 0, 0.0, None), |c| {
let stats = c.display_stats();
let disk = stats.disk.map(|d| super::snapshot::DiskCacheStats {
disk_hits: d.disk_hits,
disk_hit_rate: d.disk_hit_rate,
disk_capacity: d.capacity,
bytes_written: d.bytes_written,
bytes_read: d.bytes_read,
write_ios: d.write_ios,
read_ios: d.read_ios,
});
(stats.entry_count, stats.size_bytes, stats.hit_rate, disk)
});
MetricsSnapshot {
total_connections: self.inner.store.total_connections.load(Ordering::Relaxed),
active_connections: self.inner.active_connections.load(Ordering::Relaxed),
stateful_sessions: self.inner.stateful_sessions.load(Ordering::Relaxed),
client_to_backend_bytes: ClientToBackendBytes::new(total_sent),
backend_to_client_bytes: BackendToClientBytes::new(total_received),
uptime: self.inner.start_time.elapsed(),
backend_stats: backend_stats.into(),
user_stats,
cache_entries,
cache_size_bytes,
cache_hit_rate,
disk_cache,
pipeline_batches: self.inner.store.pipeline_batches.load(Ordering::Relaxed),
pipeline_commands: self.inner.store.pipeline_commands.load(Ordering::Relaxed),
pipeline_requests_queued: self
.inner
.store
.pipeline_requests_queued
.load(Ordering::Relaxed),
pipeline_requests_completed: self
.inner
.store
.pipeline_requests_completed
.load(Ordering::Relaxed),
}
}
#[must_use]
#[inline]
pub fn num_backends(&self) -> usize {
self.inner.backend_metrics.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_metrics_default() {
let metrics = BackendMetrics::default();
let store = BackendStore::default();
let stats = metrics.to_backend_stats(BackendId::from_index(0), &store);
assert_eq!(stats.total_commands.get(), 0);
assert_eq!(stats.bytes_sent.as_u64(), 0);
assert_eq!(stats.bytes_received.as_u64(), 0);
assert_eq!(stats.errors.get(), 0);
}
#[test]
fn test_backend_metrics_to_backend_stats() {
let metrics = BackendMetrics::default();
let store = BackendStore::default();
store.total_commands.store(42, Ordering::Relaxed);
store.bytes_sent.store(1024, Ordering::Relaxed);
store.bytes_received.store(2048, Ordering::Relaxed);
store.errors.store(3, Ordering::Relaxed);
let stats = metrics.to_backend_stats(BackendId::from_index(5), &store);
assert_eq!(stats.backend_id.as_index(), 5);
assert_eq!(stats.total_commands.get(), 42);
assert_eq!(stats.bytes_sent.as_u64(), 1024);
assert_eq!(stats.bytes_received.as_u64(), 2048);
assert_eq!(stats.errors.get(), 3);
}
#[test]
fn test_user_metrics_new() {
let metrics = UserMetrics::new("testuser".to_string());
assert_eq!(metrics.username, "testuser");
assert_eq!(metrics.total_commands, 0);
assert_eq!(metrics.bytes_sent, 0);
assert_eq!(metrics.bytes_received, 0);
}
#[test]
fn test_user_metrics_to_user_stats() {
let mut metrics = UserMetrics::new("alice".to_string());
metrics.total_commands = 100;
metrics.bytes_sent = 5000;
metrics.bytes_received = 10000;
metrics.errors = 2;
let stats = metrics.to_user_stats();
assert_eq!(stats.username, "alice");
assert_eq!(stats.total_commands.get(), 100);
assert_eq!(stats.bytes_sent.as_u64(), 5000);
assert_eq!(stats.bytes_received.as_u64(), 10000);
assert_eq!(stats.errors.get(), 2);
}
#[test]
fn test_metrics_collector_new() {
let collector = MetricsCollector::new(3);
assert_eq!(collector.num_backends(), 3);
}
#[test]
fn test_metrics_collector_record_command() {
let collector = MetricsCollector::new(2);
let backend_id = BackendId::from_index(0);
collector.record_command(backend_id);
collector.record_command(backend_id);
let snapshot = collector.snapshot(None);
assert_eq!(snapshot.backend_stats[0].total_commands.get(), 2);
}
#[test]
fn test_metrics_collector_record_bytes() {
let collector = MetricsCollector::new(1);
let backend_id = BackendId::from_index(0);
collector.record_client_to_backend_bytes_for(backend_id, 1024);
collector.record_backend_to_client_bytes_for(backend_id, 2048);
let snapshot = collector.snapshot(None);
assert_eq!(snapshot.backend_stats[0].bytes_sent.as_u64(), 1024);
assert_eq!(snapshot.backend_stats[0].bytes_received.as_u64(), 2048);
}
#[test]
fn test_metrics_collector_record_errors() {
let collector = MetricsCollector::new(1);
let backend_id = BackendId::from_index(0);
collector.record_error(backend_id);
collector.record_error(backend_id);
collector.record_error(backend_id);
let snapshot = collector.snapshot(None);
assert_eq!(snapshot.backend_stats[0].errors.get(), 3);
}
#[test]
fn test_metrics_collector_connection_lifecycle() {
let collector = MetricsCollector::new(1);
assert_eq!(collector.snapshot(None).active_connections, 0);
assert_eq!(collector.snapshot(None).total_connections, 0);
collector.connection_opened();
assert_eq!(collector.snapshot(None).active_connections, 1);
assert_eq!(collector.snapshot(None).total_connections, 1);
collector.connection_opened();
assert_eq!(collector.snapshot(None).active_connections, 2);
assert_eq!(collector.snapshot(None).total_connections, 2);
collector.connection_closed();
assert_eq!(collector.snapshot(None).active_connections, 1);
assert_eq!(collector.snapshot(None).total_connections, 2);
}
#[test]
fn test_metrics_collector_stateful_sessions() {
let collector = MetricsCollector::new(1);
collector.stateful_session_started();
assert_eq!(collector.snapshot(None).stateful_sessions, 1);
collector.stateful_session_started();
assert_eq!(collector.snapshot(None).stateful_sessions, 2);
collector.stateful_session_ended();
assert_eq!(collector.snapshot(None).stateful_sessions, 1);
}
#[test]
fn test_metrics_collector_user_metrics() {
let collector = MetricsCollector::new(1);
collector.user_connection_opened(Some("alice"));
collector.user_bytes_sent(Some("alice"), 1000);
collector.user_bytes_received(Some("alice"), 2000);
collector.user_command(Some("alice"));
let snapshot = collector.snapshot(None);
let alice_stats = snapshot.user_stats.iter().find(|s| s.username == "alice");
assert!(alice_stats.is_some());
let stats = alice_stats.unwrap();
assert_eq!(stats.bytes_sent.as_u64(), 1000);
assert_eq!(stats.bytes_received.as_u64(), 2000);
assert_eq!(stats.total_commands.get(), 1);
}
#[test]
fn test_metrics_collector_multiple_users() {
let collector = MetricsCollector::new(1);
collector.user_connection_opened(Some("alice"));
collector.user_bytes_sent(Some("alice"), 100);
collector.user_connection_opened(Some("bob"));
collector.user_bytes_sent(Some("bob"), 200);
let snapshot = collector.snapshot(None);
assert_eq!(snapshot.user_stats.len(), 2);
let alice = snapshot.user_stats.iter().find(|s| s.username == "alice");
let bob = snapshot.user_stats.iter().find(|s| s.username == "bob");
assert!(alice.is_some());
assert!(bob.is_some());
assert_eq!(alice.unwrap().bytes_sent.as_u64(), 100);
assert_eq!(bob.unwrap().bytes_sent.as_u64(), 200);
}
#[test]
fn test_metrics_collector_snapshot_aggregation() {
let collector = MetricsCollector::new(2);
collector.record_client_to_backend_bytes_for(BackendId::from_index(0), 1000);
collector.record_backend_to_client_bytes_for(BackendId::from_index(0), 2000);
collector.record_client_to_backend_bytes_for(BackendId::from_index(1), 500);
collector.record_backend_to_client_bytes_for(BackendId::from_index(1), 1500);
let snapshot = collector.snapshot(None);
assert_eq!(snapshot.client_to_backend_bytes.as_u64(), 1500);
assert_eq!(snapshot.backend_to_client_bytes.as_u64(), 3500);
}
#[test]
fn test_metrics_collector_clone() {
let collector1 = MetricsCollector::new(1);
collector1.record_command(BackendId::from_index(0));
let collector2 = collector1.clone();
collector2.record_command(BackendId::from_index(0));
assert_eq!(
collector1.snapshot(None).backend_stats[0]
.total_commands
.get(),
2
);
assert_eq!(
collector2.snapshot(None).backend_stats[0]
.total_commands
.get(),
2
);
}
#[test]
fn test_metrics_collector_with_store_records_to_correct_store() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let stats_path = temp_dir.path().join("stats.json");
let server_names = vec!["backend0".to_string(), "backend1".to_string()];
let initial_store = MetricsStore::new(2);
initial_store.backend_stores[0]
.total_commands
.store(100, Ordering::Relaxed);
initial_store.backend_stores[1]
.total_commands
.store(200, Ordering::Relaxed);
initial_store.save(&stats_path, &server_names).unwrap();
let loaded_store = MetricsStore::load(&stats_path, &server_names)
.unwrap()
.expect("Should load store");
let collector = MetricsCollector::with_store(loaded_store);
collector.record_command(BackendId::from_index(0)); collector.record_command(BackendId::from_index(1)); collector.record_command(BackendId::from_index(0));
let snapshot = collector.snapshot(None);
assert_eq!(
snapshot.backend_stats[0].total_commands.get(),
102,
"Backend 0 should have loaded 100 + recorded 2 = 102"
);
assert_eq!(
snapshot.backend_stats[1].total_commands.get(),
201,
"Backend 1 should have loaded 200 + recorded 1 = 201"
);
collector.save_to_disk(&stats_path, &server_names).unwrap();
let final_store = MetricsStore::load(&stats_path, &server_names)
.unwrap()
.expect("Should load store");
assert_eq!(
final_store.backend_stores[0]
.total_commands
.load(Ordering::Relaxed),
102,
"Backend 0 should have 102 persisted"
);
assert_eq!(
final_store.backend_stores[1]
.total_commands
.load(Ordering::Relaxed),
201,
"Backend 1 should have 201 persisted"
);
}
#[test]
fn test_out_of_bounds_backend_id_is_noop() {
let collector = MetricsCollector::new(2);
let bad_id = BackendId::from_index(2);
collector.record_command(bad_id);
collector.record_connection_failure(bad_id);
collector.record_error(bad_id);
collector.record_client_to_backend_bytes_for(bad_id, 1000);
collector.record_backend_to_client_bytes_for(bad_id, 2000);
collector.record_error_4xx(bad_id);
collector.record_error_5xx(bad_id);
collector.record_article(bad_id, 5000);
collector.record_ttfb_micros(bad_id, 100);
collector.record_send_recv_micros(bad_id, 50, 75);
let snapshot = collector.snapshot(None);
assert_eq!(snapshot.backend_stats[0].total_commands.get(), 0);
assert_eq!(snapshot.backend_stats[1].total_commands.get(), 0);
}
#[test]
fn test_record_error_4xx_increments_both_counters() {
let collector = MetricsCollector::new(1);
let backend_id = BackendId::from_index(0);
collector.record_error_4xx(backend_id);
collector.record_error_4xx(backend_id);
let snapshot = collector.snapshot(None);
assert_eq!(
snapshot.backend_stats[0].errors_4xx.get(),
2,
"errors_4xx should increment by 2"
);
assert_eq!(
snapshot.backend_stats[0].errors.get(),
2,
"errors total should also increment by 2"
);
}
#[test]
fn test_record_error_5xx_increments_both_counters() {
let collector = MetricsCollector::new(1);
let backend_id = BackendId::from_index(0);
collector.record_error_5xx(backend_id);
collector.record_error_5xx(backend_id);
collector.record_error_5xx(backend_id);
let snapshot = collector.snapshot(None);
assert_eq!(
snapshot.backend_stats[0].errors_5xx.get(),
3,
"errors_5xx should increment by 3"
);
assert_eq!(
snapshot.backend_stats[0].errors.get(),
3,
"errors total should also increment by 3"
);
}
#[test]
fn test_mixed_error_types_compose_correctly() {
let collector = MetricsCollector::new(1);
let backend_id = BackendId::from_index(0);
collector.record_error(backend_id); collector.record_error_4xx(backend_id); collector.record_error_5xx(backend_id); collector.record_error(backend_id);
let snapshot = collector.snapshot(None);
assert_eq!(
snapshot.backend_stats[0].errors.get(),
4,
"errors total: 2 plain + 1 from 4xx + 1 from 5xx = 4"
);
assert_eq!(
snapshot.backend_stats[0].errors_4xx.get(),
1,
"errors_4xx should be 1"
);
assert_eq!(
snapshot.backend_stats[0].errors_5xx.get(),
1,
"errors_5xx should be 1"
);
}
#[test]
fn test_concurrent_recording() {
use std::sync::Arc;
use std::thread;
let collector = Arc::new(MetricsCollector::new(1));
let backend_id = BackendId::from_index(0);
let mut handles = vec![];
for _ in 0..4 {
let collector = Arc::clone(&collector);
let handle = thread::spawn(move || {
for _ in 0..1000 {
collector.record_command(backend_id);
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
let snapshot = collector.snapshot(None);
assert_eq!(
snapshot.backend_stats[0].total_commands.get(),
4000,
"4 threads x 1000 iterations = 4000 total commands"
);
}
}