use crate::core::{Error, ErrorCode};
use std::collections::{HashMap, VecDeque};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime};
use tokio::sync::RwLock;
use tracing::{error, info, warn};
pub struct ErrorTelemetry {
error_counts: HashMap<ErrorCode, AtomicU64>,
recent_errors: Arc<RwLock<VecDeque<ErrorRecord>>>,
recovery_rates: Arc<RwLock<HashMap<ErrorCode, RecoveryMetrics>>>,
config: TelemetryConfig,
start_time: Instant,
}
#[derive(Debug, Clone)]
pub struct ErrorRecord {
pub error: String,
pub code: ErrorCode,
pub timestamp: SystemTime,
pub operation: String,
pub context: HashMap<String, String>,
pub stack_trace: Option<String>,
pub recovery_attempted: bool,
pub recovery_successful: bool,
}
#[derive(Debug, Clone)]
pub struct RecoveryMetrics {
pub attempts: u64,
pub successes: u64,
pub failures: u64,
pub avg_recovery_time: Duration,
pub last_attempt: Option<Instant>,
}
impl RecoveryMetrics {
fn new() -> Self {
Self {
attempts: 0,
successes: 0,
failures: 0,
avg_recovery_time: Duration::from_secs(0),
last_attempt: None,
}
}
fn success_rate(&self) -> f64 {
if self.attempts == 0 {
0.0
} else {
(self.successes as f64 / self.attempts as f64) * 100.0
}
}
}
#[derive(Debug, Clone)]
pub struct TelemetryConfig {
pub max_recent_errors: usize,
pub error_rate_threshold: f64,
pub capture_stack_traces: bool,
pub external_logging: bool,
pub sampling_rate: f64,
}
impl Default for TelemetryConfig {
fn default() -> Self {
Self {
max_recent_errors: 1000,
error_rate_threshold: 10.0,
capture_stack_traces: true,
external_logging: false,
sampling_rate: 1.0,
}
}
}
impl ErrorTelemetry {
pub fn new(config: TelemetryConfig) -> Self {
let mut error_counts = HashMap::new();
for code in 1..=13 {
if let Ok(error_code) = Self::u16_to_error_code(code) {
error_counts.insert(error_code, AtomicU64::new(0));
}
}
Self {
error_counts,
recent_errors: Arc::new(RwLock::new(VecDeque::with_capacity(
config.max_recent_errors,
))),
recovery_rates: Arc::new(RwLock::new(HashMap::new())),
config,
start_time: Instant::now(),
}
}
pub async fn record_error(
&self,
error: &Error,
operation: &str,
context: HashMap<String, String>,
) {
let code = error.code();
if let Some(counter) = self.error_counts.get(&code) {
counter.fetch_add(1, Ordering::SeqCst);
}
let should_log_details = rand::random::<f64>() <= self.config.sampling_rate;
if should_log_details {
let record = ErrorRecord {
error: error.to_string(),
code,
timestamp: SystemTime::now(),
operation: operation.to_string(),
context,
stack_trace: if self.config.capture_stack_traces {
Some(Self::capture_stack_trace())
} else {
None
},
recovery_attempted: false,
recovery_successful: false,
};
let mut recent = self.recent_errors.write().await;
if recent.len() >= self.config.max_recent_errors {
recent.pop_front();
}
recent.push_back(record.clone());
match code {
ErrorCode::BinaryNotFound | ErrorCode::NotAuthenticated => {
error!(
error_code = %code,
operation = %operation,
"Critical error occurred: {}",
error
);
}
ErrorCode::Timeout | ErrorCode::RateLimitExceeded => {
warn!(
error_code = %code,
operation = %operation,
"Recoverable error occurred: {}",
error
);
}
_ => {
info!(
error_code = %code,
operation = %operation,
"Error occurred: {}",
error
);
}
}
if self.should_alert().await {
self.send_alert().await;
}
}
}
pub async fn record_recovery_attempt(
&self,
error_code: ErrorCode,
success: bool,
duration: Duration,
) {
let mut rates = self.recovery_rates.write().await;
let metrics = rates.entry(error_code).or_insert_with(RecoveryMetrics::new);
metrics.attempts += 1;
if success {
metrics.successes += 1;
} else {
metrics.failures += 1;
}
let total_time = metrics.avg_recovery_time.as_millis() as u64 * metrics.attempts;
let new_total = total_time + duration.as_millis() as u64;
metrics.avg_recovery_time = Duration::from_millis(new_total / metrics.attempts);
metrics.last_attempt = Some(Instant::now());
info!(
error_code = %error_code,
success = success,
duration_ms = duration.as_millis(),
success_rate = metrics.success_rate(),
"Recovery attempt recorded"
);
}
pub async fn get_statistics(&self) -> ErrorStatistics {
let mut error_counts_map = HashMap::new();
let mut total_errors = 0u64;
for (code, counter) in &self.error_counts {
let count = counter.load(Ordering::SeqCst);
error_counts_map.insert(*code, count);
total_errors += count;
}
let recent_errors = self.recent_errors.read().await;
let error_rate = self.calculate_error_rate(&recent_errors);
let recovery_rates = self.recovery_rates.read().await.clone();
ErrorStatistics {
total_errors,
error_counts: error_counts_map,
error_rate,
recent_errors: recent_errors.iter().cloned().collect(),
recovery_rates,
uptime: self.start_time.elapsed(),
}
}
fn calculate_error_rate(&self, recent_errors: &VecDeque<ErrorRecord>) -> f64 {
if recent_errors.is_empty() {
return 0.0;
}
let now = SystemTime::now();
let one_minute_ago = now - Duration::from_secs(60);
let errors_in_last_minute = recent_errors
.iter()
.filter(|e| e.timestamp > one_minute_ago)
.count();
errors_in_last_minute as f64
}
async fn should_alert(&self) -> bool {
let recent = self.recent_errors.read().await;
let error_rate = self.calculate_error_rate(&recent);
error_rate > self.config.error_rate_threshold
}
async fn send_alert(&self) {
error!(
threshold = self.config.error_rate_threshold,
"Error rate threshold exceeded! Alerting configured monitoring systems"
);
}
fn capture_stack_trace() -> String {
"Stack trace capture not implemented".to_string()
}
fn u16_to_error_code(code: u16) -> Result<ErrorCode, ()> {
match code {
1 => Ok(ErrorCode::BinaryNotFound),
2 => Ok(ErrorCode::SessionNotFound),
3 => Ok(ErrorCode::PermissionDenied),
4 => Ok(ErrorCode::McpError),
5 => Ok(ErrorCode::ConfigError),
6 => Ok(ErrorCode::InvalidInput),
7 => Ok(ErrorCode::Timeout),
8 => Ok(ErrorCode::SerializationError),
9 => Ok(ErrorCode::IoError),
10 => Ok(ErrorCode::ProcessError),
11 => Ok(ErrorCode::StreamClosed),
12 => Ok(ErrorCode::NotAuthenticated),
13 => Ok(ErrorCode::RateLimitExceeded),
_ => Err(()),
}
}
pub async fn export_prometheus_metrics(&self) -> String {
let mut output = String::new();
output.push_str("# HELP claude_errors_total Total number of errors by error code\n");
output.push_str("# TYPE claude_errors_total counter\n");
for (code, counter) in &self.error_counts {
let count = counter.load(Ordering::SeqCst);
output.push_str(&format!(
"claude_errors_total{{code=\"{}\"}} {}\n",
code, count
));
}
output.push_str(
"\n# HELP claude_recovery_success_rate Recovery success rate by error code\n",
);
output.push_str("# TYPE claude_recovery_success_rate gauge\n");
let recovery_rates = self.recovery_rates.read().await;
for (code, metrics) in recovery_rates.iter() {
output.push_str(&format!(
"claude_recovery_success_rate{{code=\"{}\"}} {:.2}\n",
code,
metrics.success_rate()
));
}
let recent = self.recent_errors.read().await;
let error_rate = self.calculate_error_rate(&recent);
output.push_str("\n# HELP claude_error_rate_per_minute Current error rate per minute\n");
output.push_str("# TYPE claude_error_rate_per_minute gauge\n");
output.push_str(&format!("claude_error_rate_per_minute {:.2}\n", error_rate));
output.push_str("\n# HELP claude_uptime_seconds Uptime in seconds\n");
output.push_str("# TYPE claude_uptime_seconds counter\n");
output.push_str(&format!(
"claude_uptime_seconds {}\n",
self.start_time.elapsed().as_secs()
));
output
}
}
#[derive(Debug, Clone)]
pub struct ErrorStatistics {
pub total_errors: u64,
pub error_counts: HashMap<ErrorCode, u64>,
pub error_rate: f64,
pub recent_errors: Vec<ErrorRecord>,
pub recovery_rates: HashMap<ErrorCode, RecoveryMetrics>,
pub uptime: Duration,
}
impl ErrorStatistics {
pub fn top_errors(&self, limit: usize) -> Vec<(ErrorCode, u64)> {
let mut errors: Vec<_> = self
.error_counts
.iter()
.map(|(code, count)| (*code, *count))
.filter(|(_, count)| *count > 0)
.collect();
errors.sort_by(|a, b| b.1.cmp(&a.1));
errors.truncate(limit);
errors
}
pub fn error_trend(&self, code: ErrorCode, window: Duration) -> Vec<(SystemTime, u64)> {
let cutoff = SystemTime::now() - window;
let mut hourly_counts: HashMap<u64, u64> = HashMap::new();
for error in &self.recent_errors {
if error.code == code && error.timestamp > cutoff {
if let Ok(duration) = error.timestamp.duration_since(SystemTime::UNIX_EPOCH) {
let hour = duration.as_secs() / 3600;
*hourly_counts.entry(hour).or_insert(0) += 1;
}
}
}
let mut trend: Vec<_> = hourly_counts
.into_iter()
.map(|(hour, count)| {
let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(hour * 3600);
(timestamp, count)
})
.collect();
trend.sort_by_key(|(timestamp, _)| *timestamp);
trend
}
}
static TELEMETRY: once_cell::sync::OnceCell<Arc<ErrorTelemetry>> = once_cell::sync::OnceCell::new();
pub fn init_telemetry(config: TelemetryConfig) {
let telemetry = Arc::new(ErrorTelemetry::new(config));
if TELEMETRY.set(telemetry).is_err() {
warn!("Error telemetry already initialized");
}
}
pub fn telemetry() -> Option<Arc<ErrorTelemetry>> {
TELEMETRY.get().cloned()
}
pub async fn record_error(error: &Error, operation: &str, context: HashMap<String, String>) {
if let Some(telemetry) = telemetry() {
telemetry.record_error(error, operation, context).await;
}
}
pub async fn record_recovery(error_code: ErrorCode, success: bool, duration: Duration) {
if let Some(telemetry) = telemetry() {
telemetry
.record_recovery_attempt(error_code, success, duration)
.await;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
#[ignore] async fn test_error_telemetry() {
let config = TelemetryConfig::default();
let telemetry = ErrorTelemetry::new(config);
let mut context = HashMap::new();
context.insert("test".to_string(), "value".to_string());
telemetry
.record_error(&Error::Timeout(30), "test_operation", context.clone())
.await;
telemetry
.record_error(&Error::RateLimitExceeded, "test_operation", context)
.await;
telemetry
.record_recovery_attempt(ErrorCode::Timeout, true, Duration::from_millis(500))
.await;
telemetry
.record_recovery_attempt(
ErrorCode::RateLimitExceeded,
false,
Duration::from_millis(1000),
)
.await;
let stats = telemetry.get_statistics().await;
assert_eq!(stats.total_errors, 2);
assert_eq!(stats.error_counts.get(&ErrorCode::Timeout), Some(&1));
assert_eq!(
stats.error_counts.get(&ErrorCode::RateLimitExceeded),
Some(&1)
);
let timeout_recovery = stats.recovery_rates.get(&ErrorCode::Timeout).unwrap();
assert_eq!(timeout_recovery.attempts, 1);
assert_eq!(timeout_recovery.successes, 1);
assert_eq!(timeout_recovery.success_rate(), 100.0);
}
#[tokio::test]
#[ignore] async fn test_error_rate_calculation() {
use tokio::time::{timeout, Duration};
let test_future = async {
let config = TelemetryConfig {
error_rate_threshold: 5.0,
..Default::default()
};
let telemetry = ErrorTelemetry::new(config);
for i in 0..10 {
let mut context = HashMap::new();
context.insert("iteration".to_string(), i.to_string());
telemetry
.record_error(
&Error::ProcessError("test error".to_string()),
"rapid_errors",
context,
)
.await;
}
let stats = telemetry.get_statistics().await;
assert!(stats.error_rate >= 10.0);
};
timeout(Duration::from_secs(5), test_future)
.await
.expect("Test should complete within 5 seconds");
}
#[tokio::test]
#[ignore] async fn test_prometheus_export() {
use tokio::time::{timeout, Duration};
let test_future = async {
let telemetry = ErrorTelemetry::new(TelemetryConfig::default());
telemetry
.record_error(&Error::Timeout(30), "test", HashMap::new())
.await;
let metrics = telemetry.export_prometheus_metrics().await;
assert!(metrics.contains("claude_errors_total"));
assert!(metrics.contains("code=\"C007\""));
assert!(metrics.contains("claude_error_rate_per_minute"));
assert!(metrics.contains("claude_uptime_seconds"));
};
timeout(Duration::from_secs(5), test_future)
.await
.expect("Test should complete within 5 seconds");
}
}