use super::circuit_breaker::CircuitBreakerState;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct DeduplicationStats {
pub total_checked: u64,
pub duplicates_found: u64,
pub unique_events: u64,
pub redis_errors: u64,
pub extraction_failures: u64,
pub fallback_count: u64,
pub total_latency_us: u64,
pub avg_latency_us: u64,
pub p50_latency_us: u64,
pub p95_latency_us: u64,
pub p99_latency_us: u64,
pub last_check_time: Option<DateTime<Utc>>,
pub last_error_time: Option<DateTime<Utc>>,
pub circuit_breaker_state: CircuitBreakerState,
}
impl DeduplicationStats {
pub fn duplication_rate(&self) -> f64 {
if self.total_checked == 0 {
0.0
} else {
self.duplicates_found as f64 / self.total_checked as f64
}
}
pub fn error_rate(&self) -> f64 {
if self.total_checked == 0 {
0.0
} else {
(self.redis_errors + self.extraction_failures) as f64 / self.total_checked as f64
}
}
pub fn fallback_rate(&self) -> f64 {
if self.total_checked == 0 {
0.0
} else {
self.fallback_count as f64 / self.total_checked as f64
}
}
pub fn success_rate(&self) -> f64 {
1.0 - self.error_rate()
}
pub fn is_healthy(&self) -> bool {
self.error_rate() < 0.05 && matches!(self.circuit_breaker_state, CircuitBreakerState::Closed)
}
pub fn summary(&self) -> String {
format!(
"Dedup Stats: checked={}, unique={}, duplicates={} ({:.1}%), errors={} ({:.1}%), fallbacks={} ({:.1}%), avg_latency={}us",
self.total_checked,
self.unique_events,
self.duplicates_found,
self.duplication_rate() * 100.0,
self.redis_errors + self.extraction_failures,
self.error_rate() * 100.0,
self.fallback_count,
self.fallback_rate() * 100.0,
self.avg_latency_us,
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub status: HealthState,
pub redis_available: bool,
pub circuit_breaker_state: CircuitBreakerState,
pub last_check_time: Option<DateTime<Utc>>,
pub last_error_time: Option<DateTime<Utc>>,
pub stats: DeduplicationStats,
pub messages: Vec<String>,
}
impl HealthStatus {
pub fn is_healthy(&self) -> bool {
matches!(self.status, HealthState::Healthy)
}
pub fn is_degraded(&self) -> bool {
matches!(self.status, HealthState::Degraded)
}
pub fn is_unhealthy(&self) -> bool {
matches!(self.status, HealthState::Unhealthy)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HealthState {
Healthy,
Degraded,
Unhealthy,
}
impl std::fmt::Display for HealthState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HealthState::Healthy => write!(f, "healthy"),
HealthState::Degraded => write!(f, "degraded"),
HealthState::Unhealthy => write!(f, "unhealthy"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stats_default() {
let stats = DeduplicationStats::default();
assert_eq!(stats.total_checked, 0);
assert_eq!(stats.duplicates_found, 0);
assert_eq!(stats.duplication_rate(), 0.0);
}
#[test]
fn test_duplication_rate() {
let mut stats = DeduplicationStats::default();
stats.total_checked = 100;
stats.duplicates_found = 10;
stats.unique_events = 90;
assert_eq!(stats.duplication_rate(), 0.1);
}
#[test]
fn test_error_rate() {
let mut stats = DeduplicationStats::default();
stats.total_checked = 100;
stats.redis_errors = 5;
stats.extraction_failures = 3;
assert_eq!(stats.error_rate(), 0.08);
}
#[test]
fn test_fallback_rate() {
let mut stats = DeduplicationStats::default();
stats.total_checked = 100;
stats.fallback_count = 15;
assert_eq!(stats.fallback_rate(), 0.15);
}
#[test]
fn test_success_rate() {
let mut stats = DeduplicationStats::default();
stats.total_checked = 100;
stats.redis_errors = 2;
stats.extraction_failures = 3;
assert_eq!(stats.success_rate(), 0.95);
}
#[test]
fn test_is_healthy() {
let mut stats = DeduplicationStats::default();
stats.total_checked = 1000;
stats.redis_errors = 10;
assert!(stats.is_healthy());
stats.redis_errors = 60; assert!(!stats.is_healthy());
}
#[test]
fn test_summary() {
let mut stats = DeduplicationStats::default();
stats.total_checked = 100;
stats.duplicates_found = 10;
stats.unique_events = 90;
stats.avg_latency_us = 1500;
let summary = stats.summary();
assert!(summary.contains("checked=100"));
assert!(summary.contains("duplicates=10"));
}
#[test]
fn test_health_state_display() {
assert_eq!(HealthState::Healthy.to_string(), "healthy");
assert_eq!(HealthState::Degraded.to_string(), "degraded");
assert_eq!(HealthState::Unhealthy.to_string(), "unhealthy");
}
#[test]
fn test_health_status_checks() {
let status = HealthStatus {
status: HealthState::Healthy,
redis_available: true,
circuit_breaker_state: CircuitBreakerState::Closed,
last_check_time: None,
last_error_time: None,
stats: DeduplicationStats::default(),
messages: vec![],
};
assert!(status.is_healthy());
assert!(!status.is_degraded());
assert!(!status.is_unhealthy());
}
}