1use crate::theme::Theme;
20use std::time::Duration;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum PoolHealth {
25 Healthy,
27 Busy,
29 Degraded,
31 Exhausted,
33}
34
35impl PoolHealth {
36 #[must_use]
38 pub fn as_str(&self) -> &'static str {
39 match self {
40 Self::Healthy => "HEALTHY",
41 Self::Busy => "BUSY",
42 Self::Degraded => "DEGRADED",
43 Self::Exhausted => "EXHAUSTED",
44 }
45 }
46
47 #[must_use]
49 pub fn color_code(&self) -> &'static str {
50 match self {
51 Self::Healthy => "\x1b[32m", Self::Busy => "\x1b[33m", Self::Degraded => "\x1b[38;5;208m", Self::Exhausted => "\x1b[31m", }
56 }
57}
58
59impl std::fmt::Display for PoolHealth {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 write!(f, "{}", self.as_str())
62 }
63}
64
65pub trait PoolStatsProvider {
70 fn active_connections(&self) -> usize;
72 fn idle_connections(&self) -> usize;
74 fn max_connections(&self) -> usize;
76 fn min_connections(&self) -> usize;
78 fn pending_requests(&self) -> usize;
80 fn connections_created(&self) -> u64;
82 fn connections_closed(&self) -> u64;
84 fn total_acquires(&self) -> u64;
86 fn total_timeouts(&self) -> u64;
88}
89
90#[derive(Debug, Clone)]
92pub struct PoolStatusDisplay {
93 active: usize,
95 idle: usize,
97 max: usize,
99 min: usize,
101 pending: usize,
103 created: u64,
105 closed: u64,
107 acquires: u64,
109 timeouts: u64,
111 theme: Theme,
113 width: Option<usize>,
115 uptime: Option<Duration>,
117 name: Option<String>,
119}
120
121impl PoolStatusDisplay {
122 #[must_use]
124 pub fn from_stats<S: PoolStatsProvider>(stats: &S) -> Self {
125 Self {
126 active: stats.active_connections(),
127 idle: stats.idle_connections(),
128 max: stats.max_connections(),
129 min: stats.min_connections(),
130 pending: stats.pending_requests(),
131 created: stats.connections_created(),
132 closed: stats.connections_closed(),
133 acquires: stats.total_acquires(),
134 timeouts: stats.total_timeouts(),
135 theme: Theme::default(),
136 width: None,
137 uptime: None,
138 name: None,
139 }
140 }
141
142 #[must_use]
144 pub fn new(active: usize, idle: usize, max: usize, min: usize, pending: usize) -> Self {
145 Self {
146 active,
147 idle,
148 max,
149 min,
150 pending,
151 created: 0,
152 closed: 0,
153 acquires: 0,
154 timeouts: 0,
155 theme: Theme::default(),
156 width: None,
157 uptime: None,
158 name: None,
159 }
160 }
161
162 #[must_use]
164 pub fn theme(mut self, theme: Theme) -> Self {
165 self.theme = theme;
166 self
167 }
168
169 #[must_use]
171 pub fn width(mut self, width: usize) -> Self {
172 self.width = Some(width);
173 self
174 }
175
176 #[must_use]
178 pub fn uptime(mut self, uptime: Duration) -> Self {
179 self.uptime = Some(uptime);
180 self
181 }
182
183 #[must_use]
185 pub fn name<S: Into<String>>(mut self, name: S) -> Self {
186 self.name = Some(name.into());
187 self
188 }
189
190 #[must_use]
192 pub fn with_acquisition_stats(mut self, acquires: u64, timeouts: u64) -> Self {
193 self.acquires = acquires;
194 self.timeouts = timeouts;
195 self
196 }
197
198 #[must_use]
200 pub fn with_lifetime_stats(mut self, created: u64, closed: u64) -> Self {
201 self.created = created;
202 self.closed = closed;
203 self
204 }
205
206 #[must_use]
208 pub fn total(&self) -> usize {
209 self.active + self.idle
210 }
211
212 #[must_use]
214 pub fn utilization(&self) -> f64 {
215 if self.max == 0 {
216 0.0
217 } else {
218 (self.active as f64 / self.max as f64) * 100.0
219 }
220 }
221
222 #[must_use]
224 pub fn health(&self) -> PoolHealth {
225 let utilization = self.utilization();
226
227 if self.pending > 0 {
228 if self.pending >= self.max || self.active >= self.max {
229 PoolHealth::Exhausted
230 } else {
231 PoolHealth::Degraded
232 }
233 } else if utilization >= 80.0 {
234 PoolHealth::Busy
235 } else {
236 PoolHealth::Healthy
237 }
238 }
239
240 fn format_uptime(duration: Duration) -> String {
242 let secs = duration.as_secs();
243 if secs < 60 {
244 format!("{}s", secs)
245 } else if secs < 3600 {
246 let mins = secs / 60;
247 let secs = secs % 60;
248 if secs == 0 {
249 format!("{}m", mins)
250 } else {
251 format!("{}m {}s", mins, secs)
252 }
253 } else if secs < 86400 {
254 let hours = secs / 3600;
255 let mins = (secs % 3600) / 60;
256 if mins == 0 {
257 format!("{}h", hours)
258 } else {
259 format!("{}h {}m", hours, mins)
260 }
261 } else {
262 let days = secs / 86400;
263 let hours = (secs % 86400) / 3600;
264 if hours == 0 {
265 format!("{}d", days)
266 } else {
267 format!("{}d {}h", days, hours)
268 }
269 }
270 }
271
272 #[must_use]
274 pub fn render_plain(&self) -> String {
275 let health = self.health();
276 let utilization = self.utilization();
277 let total = self.total();
278
279 let mut lines = Vec::new();
280
281 let name_prefix = self
283 .name
284 .as_ref()
285 .map(|n| format!("{}: ", n))
286 .unwrap_or_default();
287
288 lines.push(format!(
289 "{}Pool: {}/{} active ({:.0}%), {} waiting, {}",
290 name_prefix, self.active, self.max, utilization, self.pending, health
291 ));
292
293 lines.push(format!(
295 " Active: {}, Idle: {}, Total: {}, Max: {}, Min: {}",
296 self.active, self.idle, total, self.max, self.min
297 ));
298
299 if self.acquires > 0 || self.timeouts > 0 || self.created > 0 {
301 let mut stats_parts = Vec::new();
302
303 if self.acquires > 0 {
304 stats_parts.push(format!("Acquires: {}", self.acquires));
305 }
306 if self.timeouts > 0 {
307 stats_parts.push(format!("Timeouts: {}", self.timeouts));
308 }
309 if self.created > 0 {
310 stats_parts.push(format!("Created: {}", self.created));
311 }
312 if self.closed > 0 {
313 stats_parts.push(format!("Closed: {}", self.closed));
314 }
315
316 if !stats_parts.is_empty() {
317 lines.push(format!(" {}", stats_parts.join(", ")));
318 }
319 }
320
321 if let Some(uptime) = self.uptime {
323 lines.push(format!(" Uptime: {}", Self::format_uptime(uptime)));
324 }
325
326 lines.join("\n")
327 }
328
329 #[must_use]
331 #[allow(clippy::cast_possible_truncation)]
332 pub fn render_styled(&self) -> String {
333 let health = self.health();
334 let utilization = self.utilization();
335 let width = self.width.unwrap_or(60);
336
337 let mut lines = Vec::new();
338
339 let title = self.name.as_ref().map_or_else(
341 || "Connection Pool Status".to_string(),
342 |n| format!("Connection Pool: {}", n),
343 );
344
345 let top_border = format!("┌{}┐", "─".repeat(width - 2));
347 let bottom_border = format!("└{}┘", "─".repeat(width - 2));
348
349 lines.push(top_border);
350 lines.push(format!("│ {:<width$}│", title, width = width - 3));
351 lines.push(format!("├{}┤", "─".repeat(width - 2)));
352
353 let bar_width = width - 20;
355 let filled = ((utilization / 100.0) * bar_width as f64) as usize;
357 let empty = bar_width.saturating_sub(filled);
358 let bar = format!("{}{}", "█".repeat(filled), "░".repeat(empty));
359
360 lines.push(format!(
362 "│ [{}] {:.0}% {:<width$}│",
363 bar,
364 utilization,
365 health.as_str(),
366 width = width - bar_width - 12
367 ));
368
369 lines.push(
371 format!(
372 "│ Active: {:>4} │ Idle: {:>4} │ Max: {:>4} │",
373 self.active, self.idle, self.max
374 )
375 .chars()
376 .take(width - 1)
377 .collect::<String>()
378 + "│",
379 );
380
381 if self.pending > 0 {
383 lines.push(format!(
384 "│ ⚠ Waiting requests: {:<width$}│",
385 self.pending,
386 width = width - 24
387 ));
388 }
389
390 if self.acquires > 0 || self.timeouts > 0 {
392 let timeout_rate = if self.acquires > 0 {
393 (self.timeouts as f64 / self.acquires as f64) * 100.0
394 } else {
395 0.0
396 };
397 lines.push(format!(
398 "│ Acquires: {} | Timeouts: {} ({:.1}%){:>width$}│",
399 self.acquires,
400 self.timeouts,
401 timeout_rate,
402 "",
403 width = width.saturating_sub(40)
404 ));
405 }
406
407 if let Some(uptime) = self.uptime {
409 lines.push(format!(
410 "│ Uptime: {:<width$}│",
411 Self::format_uptime(uptime),
412 width = width - 12
413 ));
414 }
415
416 lines.push(bottom_border);
417
418 lines.join("\n")
419 }
420}
421
422#[cfg(test)]
423mod tests {
424 use super::*;
425
426 #[derive(Debug, Clone)]
428 struct MockPoolStats {
429 active: usize,
430 idle: usize,
431 max: usize,
432 min: usize,
433 pending: usize,
434 created: u64,
435 closed: u64,
436 acquires: u64,
437 timeouts: u64,
438 }
439
440 impl MockPoolStats {
441 fn healthy() -> Self {
442 Self {
443 active: 3,
444 idle: 2,
445 max: 20,
446 min: 2,
447 pending: 0,
448 created: 100,
449 closed: 95,
450 acquires: 1000,
451 timeouts: 5,
452 }
453 }
454
455 fn busy() -> Self {
456 Self {
457 active: 17,
458 idle: 1,
459 max: 20,
460 min: 2,
461 pending: 0,
462 created: 200,
463 closed: 182,
464 acquires: 5000,
465 timeouts: 10,
466 }
467 }
468
469 fn degraded() -> Self {
470 Self {
472 active: 15,
473 idle: 0,
474 max: 20,
475 min: 2,
476 pending: 3,
477 created: 300,
478 closed: 280,
479 acquires: 8000,
480 timeouts: 50,
481 }
482 }
483
484 fn exhausted() -> Self {
485 Self {
486 active: 20,
487 idle: 0,
488 max: 20,
489 min: 2,
490 pending: 25,
491 created: 500,
492 closed: 480,
493 acquires: 10000,
494 timeouts: 200,
495 }
496 }
497 }
498
499 impl PoolStatsProvider for MockPoolStats {
500 fn active_connections(&self) -> usize {
501 self.active
502 }
503 fn idle_connections(&self) -> usize {
504 self.idle
505 }
506 fn max_connections(&self) -> usize {
507 self.max
508 }
509 fn min_connections(&self) -> usize {
510 self.min
511 }
512 fn pending_requests(&self) -> usize {
513 self.pending
514 }
515 fn connections_created(&self) -> u64 {
516 self.created
517 }
518 fn connections_closed(&self) -> u64 {
519 self.closed
520 }
521 fn total_acquires(&self) -> u64 {
522 self.acquires
523 }
524 fn total_timeouts(&self) -> u64 {
525 self.timeouts
526 }
527 }
528
529 #[test]
530 fn test_pool_health_healthy() {
531 let stats = MockPoolStats::healthy();
532 let display = PoolStatusDisplay::from_stats(&stats);
533 assert_eq!(display.health(), PoolHealth::Healthy);
534 }
535
536 #[test]
537 fn test_pool_health_busy() {
538 let stats = MockPoolStats::busy();
539 let display = PoolStatusDisplay::from_stats(&stats);
540 assert_eq!(display.health(), PoolHealth::Busy);
541 }
542
543 #[test]
544 fn test_pool_health_degraded() {
545 let stats = MockPoolStats::degraded();
546 let display = PoolStatusDisplay::from_stats(&stats);
547 assert_eq!(display.health(), PoolHealth::Degraded);
548 }
549
550 #[test]
551 fn test_pool_health_exhausted() {
552 let stats = MockPoolStats::exhausted();
553 let display = PoolStatusDisplay::from_stats(&stats);
554 assert_eq!(display.health(), PoolHealth::Exhausted);
555 }
556
557 #[test]
558 fn test_pool_health_as_str() {
559 assert_eq!(PoolHealth::Healthy.as_str(), "HEALTHY");
560 assert_eq!(PoolHealth::Busy.as_str(), "BUSY");
561 assert_eq!(PoolHealth::Degraded.as_str(), "DEGRADED");
562 assert_eq!(PoolHealth::Exhausted.as_str(), "EXHAUSTED");
563 }
564
565 #[test]
566 fn test_pool_health_display() {
567 assert_eq!(format!("{}", PoolHealth::Healthy), "HEALTHY");
568 assert_eq!(format!("{}", PoolHealth::Exhausted), "EXHAUSTED");
569 }
570
571 #[test]
572 fn test_utilization_calculation() {
573 let display = PoolStatusDisplay::new(10, 5, 20, 2, 0);
574 assert!((display.utilization() - 50.0).abs() < 0.01);
575 }
576
577 #[test]
578 fn test_utilization_zero_max() {
579 let display = PoolStatusDisplay::new(0, 0, 0, 0, 0);
580 assert!(display.utilization().abs() < f64::EPSILON);
581 }
582
583 #[test]
584 fn test_total_connections() {
585 let display = PoolStatusDisplay::new(8, 4, 20, 2, 0);
586 assert_eq!(display.total(), 12);
587 }
588
589 #[test]
590 fn test_render_plain_healthy() {
591 let stats = MockPoolStats::healthy();
592 let display = PoolStatusDisplay::from_stats(&stats);
593 let output = display.render_plain();
594
595 assert!(output.contains("Pool:"));
596 assert!(output.contains("HEALTHY"));
597 assert!(output.contains("Active: 3"));
598 assert!(output.contains("Idle: 2"));
599 }
600
601 #[test]
602 fn test_render_plain_with_name() {
603 let display = PoolStatusDisplay::new(5, 3, 20, 2, 0).name("PostgreSQL Main");
604 let output = display.render_plain();
605
606 assert!(output.contains("PostgreSQL Main:"));
607 }
608
609 #[test]
610 fn test_render_plain_with_uptime() {
611 let display = PoolStatusDisplay::new(5, 3, 20, 2, 0).uptime(Duration::from_secs(3725)); let output = display.render_plain();
613
614 assert!(output.contains("Uptime:"));
615 assert!(output.contains("1h 2m"));
616 }
617
618 #[test]
619 fn test_render_plain_with_waiting() {
620 let display = PoolStatusDisplay::new(20, 0, 20, 2, 5);
621 let output = display.render_plain();
622
623 assert!(output.contains("5 waiting"));
624 assert!(output.contains("DEGRADED") || output.contains("EXHAUSTED"));
625 }
626
627 #[test]
628 fn test_format_uptime_seconds() {
629 assert_eq!(
630 PoolStatusDisplay::format_uptime(Duration::from_secs(45)),
631 "45s"
632 );
633 }
634
635 #[test]
636 fn test_format_uptime_minutes() {
637 assert_eq!(
638 PoolStatusDisplay::format_uptime(Duration::from_secs(125)),
639 "2m 5s"
640 );
641 assert_eq!(
642 PoolStatusDisplay::format_uptime(Duration::from_secs(120)),
643 "2m"
644 );
645 }
646
647 #[test]
648 fn test_format_uptime_hours() {
649 assert_eq!(
650 PoolStatusDisplay::format_uptime(Duration::from_secs(3725)),
651 "1h 2m"
652 );
653 assert_eq!(
654 PoolStatusDisplay::format_uptime(Duration::from_secs(3600)),
655 "1h"
656 );
657 }
658
659 #[test]
660 fn test_format_uptime_days() {
661 assert_eq!(
662 PoolStatusDisplay::format_uptime(Duration::from_secs(90000)),
663 "1d 1h"
664 );
665 assert_eq!(
666 PoolStatusDisplay::format_uptime(Duration::from_secs(86400)),
667 "1d"
668 );
669 }
670
671 #[test]
672 fn test_new_with_explicit_values() {
673 let display = PoolStatusDisplay::new(10, 5, 30, 3, 2);
674
675 assert_eq!(display.active, 10);
676 assert_eq!(display.idle, 5);
677 assert_eq!(display.max, 30);
678 assert_eq!(display.min, 3);
679 assert_eq!(display.pending, 2);
680 }
681
682 #[test]
683 fn test_builder_pattern() {
684 let display = PoolStatusDisplay::new(5, 3, 20, 2, 0)
685 .theme(Theme::light())
686 .width(80)
687 .name("TestPool")
688 .uptime(Duration::from_secs(60))
689 .with_acquisition_stats(100, 5)
690 .with_lifetime_stats(50, 45);
691
692 assert_eq!(display.width, Some(80));
693 assert_eq!(display.name, Some("TestPool".to_string()));
694 assert!(display.uptime.is_some());
695 assert_eq!(display.acquires, 100);
696 assert_eq!(display.timeouts, 5);
697 assert_eq!(display.created, 50);
698 assert_eq!(display.closed, 45);
699 }
700
701 #[test]
702 fn test_health_color_codes() {
703 assert!(PoolHealth::Healthy.color_code().contains("32")); assert!(PoolHealth::Busy.color_code().contains("33")); assert!(PoolHealth::Exhausted.color_code().contains("31")); }
707
708 #[test]
709 fn test_render_styled_contains_box_drawing() {
710 let display = PoolStatusDisplay::new(5, 3, 20, 2, 0).width(60);
711 let output = display.render_styled();
712
713 assert!(output.contains("┌"));
714 assert!(output.contains("┐"));
715 assert!(output.contains("└"));
716 assert!(output.contains("┘"));
717 assert!(output.contains("│"));
718 }
719
720 #[test]
721 fn test_render_styled_contains_progress_bar() {
722 let display = PoolStatusDisplay::new(10, 5, 20, 2, 0).width(60);
723 let output = display.render_styled();
724
725 assert!(output.contains("█") || output.contains("░"));
727 assert!(output.contains("50%")); }
729
730 #[test]
731 fn test_from_stats_captures_all_values() {
732 let stats = MockPoolStats {
733 active: 7,
734 idle: 3,
735 max: 25,
736 min: 5,
737 pending: 1,
738 created: 150,
739 closed: 140,
740 acquires: 2000,
741 timeouts: 15,
742 };
743
744 let display = PoolStatusDisplay::from_stats(&stats);
745
746 assert_eq!(display.active, 7);
747 assert_eq!(display.idle, 3);
748 assert_eq!(display.max, 25);
749 assert_eq!(display.min, 5);
750 assert_eq!(display.pending, 1);
751 assert_eq!(display.created, 150);
752 assert_eq!(display.closed, 140);
753 assert_eq!(display.acquires, 2000);
754 assert_eq!(display.timeouts, 15);
755 }
756}