Files
foxhunt/testing/stress/src/metrics.rs
jgrusewski 9c3d741a08 refactor: restructure repo — crates/, bin/, testing/ layout
Move 17 library crates into crates/, CLI binary into bin/fxt,
consolidate 10 test crates into testing/, split config crate
from deployment config files.

Root directory reduced from 38+ to ~17 directories.
All Cargo.toml paths and build.rs proto refs updated.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 11:56:00 +01:00

247 lines
7.5 KiB
Rust

//! Resilience and Recovery Metrics Collection
//!
//! Tracks recovery times, error rates, and system behavior under stress.
use hdrhistogram::Histogram;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::RwLock;
/// Recovery metrics for measuring system resilience
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecoveryMetrics {
/// Time taken to detect the failure
pub detection_time: Duration,
/// Time taken to recover from the failure
pub recovery_time: Duration,
/// Total downtime experienced
pub total_downtime: Duration,
/// Number of retry attempts before recovery
pub retry_attempts: u32,
/// Whether circuit breaker activated
pub circuit_breaker_activated: bool,
/// Whether graceful degradation occurred
pub graceful_degradation: bool,
/// Data consistency maintained during failure
pub data_consistency_maintained: bool,
}
impl RecoveryMetrics {
/// Create new recovery metrics
pub fn new() -> Self {
Self {
detection_time: Duration::ZERO,
recovery_time: Duration::ZERO,
total_downtime: Duration::ZERO,
retry_attempts: 0,
circuit_breaker_activated: false,
graceful_degradation: false,
data_consistency_maintained: true,
}
}
/// Calculate uptime percentage
pub fn uptime_percentage(&self) -> f64 {
let total_time = self.total_downtime + Duration::from_secs(1); // Avoid div by zero
let uptime = total_time.saturating_sub(self.total_downtime);
(uptime.as_secs_f64() / total_time.as_secs_f64()) * 100.0
}
/// Check if meets 99.9% uptime SLA
pub fn meets_sla(&self, target_uptime: f64) -> bool {
self.uptime_percentage() >= target_uptime
}
}
impl Default for RecoveryMetrics {
fn default() -> Self {
Self::new()
}
}
/// Comprehensive resilience metrics
pub struct ResilienceMetrics {
/// Recovery time histogram (microseconds)
recovery_times: Arc<RwLock<Histogram<u64>>>,
/// Detection time histogram (microseconds)
detection_times: Arc<RwLock<Histogram<u64>>>,
/// Error count by category
error_counts: Arc<RwLock<std::collections::HashMap<String, u64>>>,
/// Circuit breaker activations
circuit_breaker_count: Arc<RwLock<u64>>,
/// Total test scenarios run
total_scenarios: Arc<RwLock<u64>>,
/// Successful recoveries
successful_recoveries: Arc<RwLock<u64>>,
}
impl ResilienceMetrics {
/// Create new resilience metrics collector
pub fn new() -> Self {
Self {
recovery_times: Arc::new(RwLock::new(
Histogram::<u64>::new(5).expect("Failed to create recovery histogram"),
)),
detection_times: Arc::new(RwLock::new(
Histogram::<u64>::new(5).expect("Failed to create detection histogram"),
)),
error_counts: Arc::new(RwLock::new(std::collections::HashMap::new())),
circuit_breaker_count: Arc::new(RwLock::new(0)),
total_scenarios: Arc::new(RwLock::new(0)),
successful_recoveries: Arc::new(RwLock::new(0)),
}
}
/// Record recovery metrics from a test scenario
pub async fn record_recovery(&self, metrics: &RecoveryMetrics) {
// Record recovery time
if self
.recovery_times
.write()
.await
.record(u64::try_from(metrics.recovery_time.as_micros()).unwrap_or(u64::MAX))
.is_ok()
{
// Recorded successfully
}
// Record detection time
if self
.detection_times
.write()
.await
.record(u64::try_from(metrics.detection_time.as_micros()).unwrap_or(u64::MAX))
.is_ok()
{
// Recorded successfully
}
// Increment circuit breaker count if activated
if metrics.circuit_breaker_activated {
*self.circuit_breaker_count.write().await += 1;
}
// Increment successful recoveries if recovered
if metrics.recovery_time > Duration::ZERO {
*self.successful_recoveries.write().await += 1;
}
*self.total_scenarios.write().await += 1;
}
/// Record an error occurrence
pub async fn record_error(&self, category: &str) {
let mut counts = self.error_counts.write().await;
*counts.entry(category.to_string()).or_insert(0) += 1;
}
/// Get mean recovery time
pub async fn mean_recovery_time(&self) -> Duration {
let hist = self.recovery_times.read().await;
// hist.mean() returns f64, convert safely
let mean_micros = hist.mean();
let mean_u64 = mean_micros as u64;
Duration::from_micros(mean_u64)
}
/// Get p99 recovery time
pub async fn p99_recovery_time(&self) -> Duration {
let hist = self.recovery_times.read().await;
Duration::from_micros(hist.value_at_quantile(0.99))
}
/// Get success rate
pub async fn success_rate(&self) -> f64 {
let total = *self.total_scenarios.read().await as f64;
if total == 0.0 {
return 0.0;
}
let successful = *self.successful_recoveries.read().await as f64;
(successful / total) * 100.0
}
/// Get circuit breaker activation rate
pub async fn circuit_breaker_rate(&self) -> f64 {
let total = *self.total_scenarios.read().await as f64;
if total == 0.0 {
return 0.0;
}
let activations = *self.circuit_breaker_count.read().await as f64;
(activations / total) * 100.0
}
/// Generate summary report
pub async fn generate_report(&self) -> String {
let mean_recovery = self.mean_recovery_time().await;
let p99_recovery = self.p99_recovery_time().await;
let success_rate = self.success_rate().await;
let cb_rate = self.circuit_breaker_rate().await;
let total = *self.total_scenarios.read().await;
format!(
r#"
Resilience Metrics Summary
==========================
Total Scenarios: {}
Success Rate: {:.2}%
Circuit Breaker Activation Rate: {:.2}%
Recovery Times:
Mean: {:?}
P99: {:?}
Error Counts:
"#,
total, success_rate, cb_rate, mean_recovery, p99_recovery,
)
}
}
impl Default for ResilienceMetrics {
fn default() -> Self {
Self::new()
}
}
/// Recovery timer for measuring recovery phases
pub struct RecoveryTimer {
start: Instant,
detection_time: Option<Duration>,
recovery_time: Option<Duration>,
}
impl RecoveryTimer {
/// Start a new recovery timer
pub fn start() -> Self {
Self {
start: Instant::now(),
detection_time: None,
recovery_time: None,
}
}
/// Mark failure detection
pub fn mark_detection(&mut self) {
self.detection_time = Some(self.start.elapsed());
}
/// Mark recovery completion
pub fn mark_recovery(&mut self) {
self.recovery_time = Some(self.start.elapsed());
}
/// Build recovery metrics
pub fn build_metrics(self) -> RecoveryMetrics {
RecoveryMetrics {
detection_time: self.detection_time.unwrap_or(Duration::ZERO),
recovery_time: self.recovery_time.unwrap_or(Duration::ZERO),
total_downtime: self.recovery_time.unwrap_or(Duration::ZERO),
retry_attempts: 0,
circuit_breaker_activated: false,
graceful_degradation: false,
data_consistency_maintained: true,
}
}
}