1#![allow(clippy::cast_precision_loss, clippy::float_cmp)] use super::types::{ActiveConnections, BackendHealthStatus, ErrorRatePercent};
12use crate::types::{BackendId, BackendToClientBytes, ClientToBackendBytes};
13use std::sync::Arc;
14use std::time::Duration;
15
16use super::BackendStats;
17use super::UserStats;
18
19#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
33pub struct MetricsSnapshot {
34 pub total_connections: u64,
35 #[serde(skip, default)]
36 pub active_connections: usize,
37 #[serde(skip, default)]
38 pub stateful_sessions: usize,
39 pub client_to_backend_bytes: ClientToBackendBytes,
40 pub backend_to_client_bytes: BackendToClientBytes,
41 #[serde(skip, default)]
42 pub uptime: Duration,
43 pub backend_stats: Arc<[BackendStats]>,
44 pub user_stats: Vec<UserStats>,
45 #[serde(skip, default)]
46 pub cache_entries: u64,
47 #[serde(skip, default)]
48 pub cache_size_bytes: u64,
49 #[serde(skip, default)]
50 pub cache_hit_rate: f64,
51 #[serde(skip, default)]
53 pub disk_cache: Option<DiskCacheStats>,
54 pub pipeline_batches: u64,
56 pub pipeline_commands: u64,
58 pub pipeline_requests_queued: u64,
60 pub pipeline_requests_completed: u64,
62}
63
64#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
66pub struct DiskCacheStats {
67 pub disk_hits: u64,
69 pub disk_hit_rate: f64,
71 pub disk_capacity: u64,
73 pub bytes_written: u64,
75 pub bytes_read: u64,
77 pub write_ios: u64,
79 pub read_ios: u64,
81}
82
83impl MetricsSnapshot {
84 #[must_use]
91 pub fn with_pool_status(mut self, router: &crate::router::BackendSelector) -> Self {
92 use crate::pool::ConnectionProvider;
93
94 let backend_stats = Arc::make_mut(&mut self.backend_stats);
96
97 for stats in backend_stats {
98 if let Some(provider) = router.backend_provider(stats.backend_id) {
99 let pool_status = provider.status();
100 let active = pool_status
103 .created
104 .get()
105 .saturating_sub(pool_status.available.get());
106 stats.active_connections = ActiveConnections::new(active);
107 }
108 }
109 self
110 }
111
112 #[must_use]
114 pub fn format_uptime(&self) -> String {
115 let secs = self.uptime.as_secs();
116 let hours = secs / 3600;
117 let minutes = (secs % 3600) / 60;
118 let seconds = secs % 60;
119
120 if hours > 0 {
121 format!("{hours}h {minutes}m {seconds}s")
122 } else if minutes > 0 {
123 format!("{minutes}m {seconds}s")
124 } else {
125 format!("{seconds}s")
126 }
127 }
128
129 #[must_use]
133 #[inline]
134 pub const fn total_bytes(&self) -> u64 {
135 self.client_to_backend_bytes.as_u64() + self.backend_to_client_bytes.as_u64()
136 }
137
138 #[must_use]
143 pub fn throughput_bps(&self) -> f64 {
144 let secs = self.uptime.as_secs_f64();
145 if secs > 0.0 {
146 self.total_bytes() as f64 / secs
147 } else {
148 0.0
149 }
150 }
151
152 #[must_use]
156 #[inline]
157 pub fn total_commands(&self) -> u64 {
158 self.backend_stats
159 .iter()
160 .map(|stats| stats.total_commands.get())
161 .sum()
162 }
163
164 #[must_use]
168 #[inline]
169 pub fn total_errors(&self) -> u64 {
170 self.backend_stats
171 .iter()
172 .map(|stats| stats.errors.get())
173 .sum()
174 }
175
176 #[must_use]
181 pub fn error_rate_percent(&self) -> f64 {
182 let total_cmds = self.total_commands();
183 let total_errs = self.total_errors();
184 ErrorRatePercent::from_raw_counts(total_errs, total_cmds).get()
185 }
186
187 pub fn high_error_backends(&self) -> impl Iterator<Item = BackendId> + '_ {
192 self.backend_stats
193 .iter()
194 .filter(|stats| stats.has_high_error_rate())
195 .map(|stats| stats.backend_id)
196 }
197
198 pub fn healthy_backends(&self) -> impl Iterator<Item = BackendId> + '_ {
203 self.backend_stats
204 .iter()
205 .filter(|stats| stats.health_status == BackendHealthStatus::Healthy)
206 .map(|stats| stats.backend_id)
207 }
208
209 #[must_use]
213 pub fn backend(&self, backend_id: BackendId) -> Option<&BackendStats> {
214 self.backend_stats.get(backend_id.as_index())
215 }
216
217 #[must_use]
222 pub fn backend_health_counts(&self) -> (usize, usize, usize) {
223 self.backend_stats
224 .iter()
225 .fold((0, 0, 0), |(h, d, dn), stats| match stats.health_status {
226 BackendHealthStatus::Healthy => (h + 1, d, dn),
227 BackendHealthStatus::Degraded => (h, d + 1, dn),
228 BackendHealthStatus::Down => (h, d, dn + 1),
229 })
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::metrics::{
237 ArticleCount, CommandCount, ErrorCount, FailureCount, RecvMicros, SendMicros, TtfbMicros,
238 };
239 use crate::types::BackendId;
240
241 fn create_test_snapshot() -> MetricsSnapshot {
242 use crate::types::{ArticleBytesTotal, BytesReceived, BytesSent, TimingMeasurementCount};
243
244 let backend1 = BackendStats {
245 backend_id: BackendId::from_index(0),
246 total_commands: CommandCount::new(100),
247 errors: ErrorCount::new(5),
248 bytes_sent: BytesSent::new(1000),
249 bytes_received: BytesReceived::new(2000),
250 health_status: BackendHealthStatus::Healthy,
251 active_connections: ActiveConnections::new(3),
252 errors_4xx: ErrorCount::new(2),
253 errors_5xx: ErrorCount::new(3),
254 article_bytes_total: ArticleBytesTotal::new(5000),
255 article_count: ArticleCount::new(10),
256 ttfb_micros_total: TtfbMicros::new(1000),
257 ttfb_count: TimingMeasurementCount::new(10),
258 send_micros_total: SendMicros::new(500),
259 recv_micros_total: RecvMicros::new(1500),
260 connection_failures: FailureCount::new(0),
261 };
262
263 let backend2 = BackendStats {
264 backend_id: BackendId::from_index(1),
265 total_commands: CommandCount::new(50),
266 errors: ErrorCount::new(10),
267 bytes_sent: BytesSent::new(500),
268 bytes_received: BytesReceived::new(1500),
269 health_status: BackendHealthStatus::Degraded,
270 active_connections: ActiveConnections::new(2),
271 errors_4xx: ErrorCount::new(5),
272 errors_5xx: ErrorCount::new(5),
273 article_bytes_total: ArticleBytesTotal::new(2500),
274 article_count: ArticleCount::new(5),
275 ttfb_micros_total: TtfbMicros::new(500),
276 ttfb_count: TimingMeasurementCount::new(5),
277 send_micros_total: SendMicros::new(250),
278 recv_micros_total: RecvMicros::new(750),
279 connection_failures: FailureCount::new(1),
280 };
281
282 MetricsSnapshot {
283 total_connections: 5,
284 active_connections: 5,
285 stateful_sessions: 2,
286 client_to_backend_bytes: ClientToBackendBytes::new(1500),
287 backend_to_client_bytes: BackendToClientBytes::new(3500),
288 uptime: crate::constants::duration_polyfill::from_hours(1),
289 backend_stats: vec![backend1, backend2].into(),
290 user_stats: vec![],
291 cache_entries: 0,
292 cache_size_bytes: 0,
293 cache_hit_rate: 0.0,
294 disk_cache: None,
295 pipeline_batches: 0,
296 pipeline_commands: 0,
297 pipeline_requests_queued: 0,
298 pipeline_requests_completed: 0,
299 }
300 }
301
302 #[test]
303 fn test_format_uptime_hours() {
304 let snapshot = MetricsSnapshot {
305 uptime: Duration::from_secs(3661), ..Default::default()
307 };
308 assert_eq!(snapshot.format_uptime(), "1h 1m 1s");
309 }
310
311 #[test]
312 fn test_format_uptime_minutes() {
313 let snapshot = MetricsSnapshot {
314 uptime: Duration::from_secs(125), ..Default::default()
316 };
317 assert_eq!(snapshot.format_uptime(), "2m 5s");
318 }
319
320 #[test]
321 fn test_format_uptime_seconds() {
322 let snapshot = MetricsSnapshot {
323 uptime: Duration::from_secs(42),
324 ..Default::default()
325 };
326 assert_eq!(snapshot.format_uptime(), "42s");
327 }
328
329 #[test]
330 fn test_format_uptime_zero() {
331 let snapshot = MetricsSnapshot {
332 uptime: Duration::from_secs(0),
333 ..Default::default()
334 };
335 assert_eq!(snapshot.format_uptime(), "0s");
336 }
337
338 #[test]
339 fn test_total_bytes() {
340 let snapshot = create_test_snapshot();
341 assert_eq!(snapshot.total_bytes(), 5000); }
343
344 #[test]
345 fn test_total_bytes_zero() {
346 let snapshot = MetricsSnapshot::default();
347 assert_eq!(snapshot.total_bytes(), 0);
348 }
349
350 #[test]
351 fn test_throughput_bps() {
352 let snapshot = create_test_snapshot();
353 let expected = 5000.0 / 3600.0; assert!((snapshot.throughput_bps() - expected).abs() < 0.01);
355 }
356
357 #[test]
358 fn test_throughput_bps_zero_uptime() {
359 let snapshot = MetricsSnapshot {
360 client_to_backend_bytes: ClientToBackendBytes::new(1000),
361 backend_to_client_bytes: BackendToClientBytes::new(2000),
362 uptime: Duration::from_secs(0),
363 ..Default::default()
364 };
365 assert_eq!(snapshot.throughput_bps(), 0.0);
366 }
367
368 #[test]
369 fn test_total_commands() {
370 let snapshot = create_test_snapshot();
371 assert_eq!(snapshot.total_commands(), 150); }
373
374 #[test]
375 fn test_total_commands_empty() {
376 let snapshot = MetricsSnapshot::default();
377 assert_eq!(snapshot.total_commands(), 0);
378 }
379
380 #[test]
381 fn test_total_errors() {
382 let snapshot = create_test_snapshot();
383 assert_eq!(snapshot.total_errors(), 15); }
385
386 #[test]
387 fn test_total_errors_empty() {
388 let snapshot = MetricsSnapshot::default();
389 assert_eq!(snapshot.total_errors(), 0);
390 }
391
392 #[test]
393 fn test_error_rate_percent() {
394 let snapshot = create_test_snapshot();
395 let expected = 15.0 / 150.0 * 100.0; assert!((snapshot.error_rate_percent() - expected).abs() < 0.01);
397 }
398
399 #[test]
400 fn test_error_rate_percent_zero_commands() {
401 let snapshot = MetricsSnapshot::default();
402 assert_eq!(snapshot.error_rate_percent(), 0.0);
403 }
404
405 #[test]
406 fn test_high_error_backends() {
407 let snapshot = create_test_snapshot();
408 let high_error: Vec<_> = snapshot.high_error_backends().collect();
409 assert_eq!(high_error.len(), 1);
412 assert_eq!(high_error[0], BackendId::from_index(1));
413 }
414
415 #[test]
416 fn test_high_error_backends_empty() {
417 let snapshot = MetricsSnapshot::default();
418 assert_eq!(snapshot.high_error_backends().count(), 0);
419 }
420
421 #[test]
422 fn test_healthy_backends() {
423 let snapshot = create_test_snapshot();
424 let healthy: Vec<_> = snapshot.healthy_backends().collect();
425 assert_eq!(healthy.len(), 1);
426 assert_eq!(healthy[0], BackendId::from_index(0));
427 }
428
429 #[test]
430 fn test_healthy_backends_all_down() {
431 let backend = BackendStats {
432 health_status: BackendHealthStatus::Down,
433 ..Default::default()
434 };
435
436 let snapshot = MetricsSnapshot {
437 backend_stats: vec![backend].into(),
438 ..Default::default()
439 };
440
441 assert_eq!(snapshot.healthy_backends().count(), 0);
442 }
443
444 #[test]
445 fn test_backend_by_id() {
446 let snapshot = create_test_snapshot();
447
448 let backend0 = snapshot.backend(BackendId::from_index(0));
449 assert!(backend0.is_some());
450 assert_eq!(backend0.unwrap().backend_id, BackendId::from_index(0));
451 assert_eq!(backend0.unwrap().total_commands.get(), 100);
452
453 let backend1 = snapshot.backend(BackendId::from_index(1));
454 assert!(backend1.is_some());
455 assert_eq!(backend1.unwrap().backend_id, BackendId::from_index(1));
456 assert_eq!(backend1.unwrap().total_commands.get(), 50);
457 }
458
459 #[test]
460 fn test_backend_by_id_out_of_range() {
461 let snapshot = create_test_snapshot();
462 let backend = snapshot.backend(BackendId::from_index(2));
463 assert!(backend.is_none());
464 }
465
466 #[test]
467 fn test_backend_health_counts() {
468 let snapshot = create_test_snapshot();
469 let (healthy, degraded, down) = snapshot.backend_health_counts();
470 assert_eq!(healthy, 1);
471 assert_eq!(degraded, 1);
472 assert_eq!(down, 0);
473 }
474
475 #[test]
476 fn test_backend_health_counts_mixed() {
477 let backends = vec![
478 BackendStats {
479 backend_id: BackendId::from_index(0),
480 health_status: BackendHealthStatus::Healthy,
481 ..Default::default()
482 },
483 BackendStats {
484 backend_id: BackendId::from_index(1),
485 health_status: BackendHealthStatus::Healthy,
486 ..Default::default()
487 },
488 BackendStats {
489 backend_id: BackendId::from_index(2),
490 health_status: BackendHealthStatus::Down,
491 ..Default::default()
492 },
493 ];
494
495 let snapshot = MetricsSnapshot {
496 backend_stats: backends.into(),
497 ..Default::default()
498 };
499
500 let (healthy, degraded, down) = snapshot.backend_health_counts();
501 assert_eq!(healthy, 2);
502 assert_eq!(degraded, 0);
503 assert_eq!(down, 1);
504 }
505
506 #[test]
507 fn test_backend_health_counts_empty() {
508 let snapshot = MetricsSnapshot::default();
509 let (healthy, degraded, down) = snapshot.backend_health_counts();
510 assert_eq!(healthy, 0);
511 assert_eq!(degraded, 0);
512 assert_eq!(down, 0);
513 }
514
515 #[test]
516 fn with_pool_status_does_not_count_unopened_capacity_as_active() {
517 use crate::pool::DeadpoolConnectionProvider;
518 use crate::router::BackendSelector;
519 use crate::types::{BackendId, ServerName};
520
521 let provider = DeadpoolConnectionProvider::builder("127.0.0.1", 9)
522 .name("unused")
523 .max_connections(50)
524 .build()
525 .expect("provider should build without connecting");
526
527 let mut router = BackendSelector::new();
528 router.add_backend(
529 ServerName::try_new("unused".to_string()).unwrap(),
530 provider,
531 1,
532 );
533
534 let snapshot = MetricsSnapshot {
535 backend_stats: vec![BackendStats {
536 backend_id: BackendId::from_index(0),
537 ..Default::default()
538 }]
539 .into(),
540 ..Default::default()
541 }
542 .with_pool_status(&router);
543
544 assert_eq!(snapshot.backend_stats[0].active_connections.get(), 0);
545 }
546
547 #[test]
548 fn test_snapshot_default() {
549 let snapshot = MetricsSnapshot::default();
550 assert_eq!(snapshot.total_connections, 0);
551 assert_eq!(snapshot.active_connections, 0);
552 assert_eq!(snapshot.stateful_sessions, 0);
553 assert_eq!(snapshot.total_bytes(), 0);
554 assert_eq!(snapshot.uptime, Duration::from_secs(0));
555 assert_eq!(snapshot.backend_stats.len(), 0);
556 assert_eq!(snapshot.user_stats.len(), 0);
557 }
558
559 #[test]
560 fn test_snapshot_clone() {
561 let snapshot = create_test_snapshot();
562 let cloned = snapshot.clone();
563
564 assert_eq!(snapshot.total_connections, cloned.total_connections);
565 assert_eq!(snapshot.total_bytes(), cloned.total_bytes());
566 assert_eq!(snapshot.uptime, cloned.uptime);
567
568 assert!(Arc::ptr_eq(&snapshot.backend_stats, &cloned.backend_stats));
570 }
571}