1use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use std::collections::{HashMap, VecDeque};
9use std::sync::Arc;
10use std::time::{Duration, Instant};
11use tokio::sync::RwLock;
12use tracing::{debug, info, trace, warn};
13use uuid::Uuid;
14
15use super::stats::{
16 calculate_iqr, is_outlier_iqr, is_outlier_zscore, mean, std_dev, z_score, CircularBuffer,
17};
18use super::traits::{
19 Analyzer, AnalyzerConfig, AnalyzerError, AnalyzerResult, AnalyzerState,
20};
21use super::types::{
22 Action, ActionType, Alert, AlertStatus, AnalysisReport, AnalyzerEvent, AnalyzerStats,
23 Confidence, Evidence, EvidenceType, Impact, ImpactMetric, Insight, InsightCategory, Priority,
24 Recommendation, ReportSummary, RiskLevel, Severity, Threshold, ThresholdOperator,
25};
26
27#[derive(Debug, Clone)]
29pub struct AnomalyAnalyzerConfig {
30 pub base: AnalyzerConfig,
32
33 pub zscore_threshold: f64,
35
36 pub iqr_multiplier: f64,
38
39 pub min_samples: usize,
41
42 pub consecutive_threshold: usize,
44
45 pub anomaly_window_size: usize,
47
48 pub enable_drift_detection: bool,
50
51 pub drift_window_size: usize,
53}
54
55impl Default for AnomalyAnalyzerConfig {
56 fn default() -> Self {
57 Self {
58 base: AnalyzerConfig::default(),
59 zscore_threshold: 3.0, iqr_multiplier: 1.5, min_samples: 30, consecutive_threshold: 2, anomaly_window_size: 1000,
64 enable_drift_detection: true,
65 drift_window_size: 100,
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum AnomalyType {
73 LatencyAnomaly,
75 CostAnomaly,
77 VolumeAnomaly,
79 ErrorRateAnomaly,
81 UnknownAnomaly,
83}
84
85impl AnomalyType {
86 fn as_str(&self) -> &str {
87 match self {
88 AnomalyType::LatencyAnomaly => "latency_anomaly",
89 AnomalyType::CostAnomaly => "cost_anomaly",
90 AnomalyType::VolumeAnomaly => "volume_anomaly",
91 AnomalyType::ErrorRateAnomaly => "error_rate_anomaly",
92 AnomalyType::UnknownAnomaly => "unknown_anomaly",
93 }
94 }
95}
96
97#[derive(Debug, Clone)]
99struct AnomalyRecord {
100 timestamp: DateTime<Utc>,
101 anomaly_type: AnomalyType,
102 value: f64,
103 zscore: f64,
104 severity: Severity,
105 consecutive_count: usize,
106}
107
108#[derive(Debug, Clone)]
110struct AnomalyMetrics {
111 latencies: CircularBuffer<f64>,
113
114 costs: CircularBuffer<f64>,
116
117 request_times: VecDeque<DateTime<Utc>>,
119
120 errors: CircularBuffer<u64>,
122 total_requests: CircularBuffer<u64>,
123
124 anomalies: VecDeque<AnomalyRecord>,
126
127 consecutive_latency: usize,
129 consecutive_cost: usize,
130 consecutive_volume: usize,
131 consecutive_error: usize,
132
133 baseline_latency_mean: Option<f64>,
135 baseline_cost_mean: Option<f64>,
136 baseline_volume_mean: Option<f64>,
137}
138
139impl AnomalyMetrics {
140 fn new(window_size: usize, drift_window_size: usize) -> Self {
141 Self {
142 latencies: CircularBuffer::new(window_size),
143 costs: CircularBuffer::new(window_size),
144 request_times: VecDeque::with_capacity(window_size),
145 errors: CircularBuffer::new(window_size),
146 total_requests: CircularBuffer::new(window_size),
147 anomalies: VecDeque::with_capacity(1000),
148 consecutive_latency: 0,
149 consecutive_cost: 0,
150 consecutive_volume: 0,
151 consecutive_error: 0,
152 baseline_latency_mean: None,
153 baseline_cost_mean: None,
154 baseline_volume_mean: None,
155 }
156 }
157
158 fn record_latency(&mut self, latency_ms: f64) {
159 self.latencies.push(latency_ms);
160 }
161
162 fn record_cost(&mut self, cost_usd: f64) {
163 self.costs.push(cost_usd);
164 }
165
166 fn record_request(&mut self, timestamp: DateTime<Utc>) {
167 self.request_times.push_back(timestamp);
168 while self.request_times.len() > 1000 {
170 self.request_times.pop_front();
171 }
172 }
173
174 fn record_error(&mut self, is_error: bool) {
175 let error_val = if is_error { 1 } else { 0 };
176 self.errors.push(error_val);
177 self.total_requests.push(1);
178 }
179
180 fn add_anomaly(&mut self, record: AnomalyRecord) {
181 self.anomalies.push_back(record);
182 while self.anomalies.len() > 1000 {
184 self.anomalies.pop_front();
185 }
186 }
187
188 fn get_recent_anomalies(&self, duration: Duration) -> Vec<&AnomalyRecord> {
189 let cutoff = Utc::now() - chrono::Duration::from_std(duration).unwrap();
190 self.anomalies
191 .iter()
192 .filter(|a| a.timestamp > cutoff)
193 .collect()
194 }
195
196 fn calculate_request_volume(&self, duration: Duration) -> f64 {
197 let cutoff = Utc::now() - chrono::Duration::from_std(duration).unwrap();
198 let count = self
199 .request_times
200 .iter()
201 .filter(|&&t| t > cutoff)
202 .count();
203 count as f64 / duration.as_secs_f64()
204 }
205
206 fn calculate_error_rate(&self) -> f64 {
207 if self.total_requests.is_empty() {
208 return 0.0;
209 }
210
211 let errors: u64 = self.errors.to_vec().iter().sum();
212 let total: u64 = self.total_requests.to_vec().iter().sum();
213
214 if total == 0 {
215 0.0
216 } else {
217 errors as f64 / total as f64
218 }
219 }
220}
221
222pub struct AnomalyAnalyzer {
224 config: AnomalyAnalyzerConfig,
225 state: AnalyzerState,
226 metrics: Arc<RwLock<AnomalyMetrics>>,
227 stats: Arc<RwLock<AnalyzerStats>>,
228 start_time: Instant,
229 last_report_time: Arc<RwLock<Instant>>,
230}
231
232impl AnomalyAnalyzer {
233 pub fn new(config: AnomalyAnalyzerConfig) -> Self {
235 let window_size = config.anomaly_window_size;
236 let drift_window_size = config.drift_window_size;
237
238 Self {
239 config,
240 state: AnalyzerState::Initialized,
241 metrics: Arc::new(RwLock::new(AnomalyMetrics::new(
242 window_size,
243 drift_window_size,
244 ))),
245 stats: Arc::new(RwLock::new(AnalyzerStats {
246 analyzer: "anomaly".to_string(),
247 ..Default::default()
248 })),
249 start_time: Instant::now(),
250 last_report_time: Arc::new(RwLock::new(Instant::now())),
251 }
252 }
253
254 pub fn with_defaults() -> Self {
256 Self::new(AnomalyAnalyzerConfig::default())
257 }
258
259 async fn process_response(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
261 if let AnalyzerEvent::Response {
262 timestamp,
263 latency_ms,
264 success,
265 ..
266 } = event
267 {
268 let mut metrics = self.metrics.write().await;
269 metrics.record_latency(latency_ms as f64);
270 metrics.record_request(timestamp);
271 metrics.record_error(!success);
272
273 trace!(
274 "Recorded response metrics: latency={}ms, success={}",
275 latency_ms,
276 success
277 );
278 }
279
280 Ok(())
281 }
282
283 async fn process_cost(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
285 if let AnalyzerEvent::Cost {
286 timestamp,
287 cost_usd,
288 ..
289 } = event
290 {
291 let mut metrics = self.metrics.write().await;
292 metrics.record_cost(cost_usd);
293 metrics.record_request(timestamp);
294
295 trace!("Recorded cost: ${:.4}", cost_usd);
296 }
297
298 Ok(())
299 }
300
301 async fn process_metric(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
303 if let AnalyzerEvent::Metric {
304 name,
305 value,
306 timestamp,
307 ..
308 } = event
309 {
310 let mut metrics = self.metrics.write().await;
311
312 if name.contains("latency") || name.contains("duration") {
313 metrics.record_latency(value);
314 metrics.record_request(timestamp);
315 } else if name.contains("cost") {
316 metrics.record_cost(value);
317 metrics.record_request(timestamp);
318 }
319 }
320
321 Ok(())
322 }
323
324 async fn detect_latency_anomalies(&self) -> Vec<AnomalyRecord> {
326 let mut metrics = self.metrics.write().await;
327 let latencies = metrics.latencies.to_vec();
328
329 if latencies.len() < self.config.min_samples {
330 return Vec::new();
331 }
332
333 let latest = latencies[latencies.len() - 1];
334 let m = mean(&latencies);
335 let sd = std_dev(&latencies);
336 let z = z_score(latest, m, sd);
337
338 let mut anomalies = Vec::new();
339
340 if is_outlier_zscore(latest, m, sd, self.config.zscore_threshold) {
342 metrics.consecutive_latency += 1;
343
344 if metrics.consecutive_latency >= self.config.consecutive_threshold {
345 let severity = if z.abs() > 5.0 {
346 Severity::Critical
347 } else if z.abs() > 4.0 {
348 Severity::Error
349 } else {
350 Severity::Warning
351 };
352
353 anomalies.push(AnomalyRecord {
354 timestamp: Utc::now(),
355 anomaly_type: AnomalyType::LatencyAnomaly,
356 value: latest,
357 zscore: z,
358 severity,
359 consecutive_count: metrics.consecutive_latency,
360 });
361 }
362 } else {
363 metrics.consecutive_latency = 0;
364 }
365
366 if is_outlier_iqr(latest, &latencies, self.config.iqr_multiplier) {
368 debug!("IQR outlier detected: latency={}ms", latest);
369 }
370
371 anomalies
372 }
373
374 async fn detect_cost_anomalies(&self) -> Vec<AnomalyRecord> {
376 let mut metrics = self.metrics.write().await;
377 let costs = metrics.costs.to_vec();
378
379 if costs.len() < self.config.min_samples {
380 return Vec::new();
381 }
382
383 let latest = costs[costs.len() - 1];
384 let m = mean(&costs);
385 let sd = std_dev(&costs);
386 let z = z_score(latest, m, sd);
387
388 let mut anomalies = Vec::new();
389
390 if is_outlier_zscore(latest, m, sd, self.config.zscore_threshold) {
391 metrics.consecutive_cost += 1;
392
393 if metrics.consecutive_cost >= self.config.consecutive_threshold {
394 let severity = if z.abs() > 5.0 {
395 Severity::Critical
396 } else if z.abs() > 4.0 {
397 Severity::Error
398 } else {
399 Severity::Warning
400 };
401
402 anomalies.push(AnomalyRecord {
403 timestamp: Utc::now(),
404 anomaly_type: AnomalyType::CostAnomaly,
405 value: latest,
406 zscore: z,
407 severity,
408 consecutive_count: metrics.consecutive_cost,
409 });
410 }
411 } else {
412 metrics.consecutive_cost = 0;
413 }
414
415 anomalies
416 }
417
418 async fn detect_volume_anomalies(&self) -> Vec<AnomalyRecord> {
420 let metrics = self.metrics.read().await;
421 let mut anomalies = Vec::new();
422
423 if metrics.request_times.len() < self.config.min_samples {
424 return anomalies;
425 }
426
427 let current_volume = metrics.calculate_request_volume(Duration::from_secs(60));
429
430 if let Some(baseline) = metrics.baseline_volume_mean {
432 let change_pct = ((current_volume - baseline) / baseline).abs() * 100.0;
433
434 if change_pct > 50.0 {
435 let severity = if change_pct > 100.0 {
437 Severity::Critical
438 } else if change_pct > 75.0 {
439 Severity::Error
440 } else {
441 Severity::Warning
442 };
443
444 anomalies.push(AnomalyRecord {
445 timestamp: Utc::now(),
446 anomaly_type: AnomalyType::VolumeAnomaly,
447 value: current_volume,
448 zscore: change_pct / 10.0, severity,
450 consecutive_count: 1,
451 });
452 }
453 }
454
455 anomalies
456 }
457
458 async fn detect_error_rate_anomalies(&self) -> Vec<AnomalyRecord> {
460 let metrics = self.metrics.read().await;
461 let mut anomalies = Vec::new();
462
463 if metrics.total_requests.len() < self.config.min_samples {
464 return anomalies;
465 }
466
467 let error_rate = metrics.calculate_error_rate();
468
469 if error_rate > 0.05 {
471 let severity = if error_rate > 0.20 {
473 Severity::Critical
474 } else if error_rate > 0.10 {
475 Severity::Error
476 } else {
477 Severity::Warning
478 };
479
480 anomalies.push(AnomalyRecord {
481 timestamp: Utc::now(),
482 anomaly_type: AnomalyType::ErrorRateAnomaly,
483 value: error_rate,
484 zscore: error_rate * 10.0, severity,
486 consecutive_count: 1,
487 });
488 }
489
490 anomalies
491 }
492
493 async fn analyze_metrics(&self) -> Vec<Insight> {
495 let mut insights = Vec::new();
496
497 let latency_anomalies = self.detect_latency_anomalies().await;
499 let cost_anomalies = self.detect_cost_anomalies().await;
500 let volume_anomalies = self.detect_volume_anomalies().await;
501 let error_anomalies = self.detect_error_rate_anomalies().await;
502
503 let mut metrics = self.metrics.write().await;
505 for anomaly in &latency_anomalies {
506 metrics.add_anomaly(anomaly.clone());
507 insights.push(self.create_anomaly_insight(anomaly));
508 }
509 for anomaly in &cost_anomalies {
510 metrics.add_anomaly(anomaly.clone());
511 insights.push(self.create_anomaly_insight(anomaly));
512 }
513 for anomaly in &volume_anomalies {
514 metrics.add_anomaly(anomaly.clone());
515 insights.push(self.create_anomaly_insight(anomaly));
516 }
517 for anomaly in &error_anomalies {
518 metrics.add_anomaly(anomaly.clone());
519 insights.push(self.create_anomaly_insight(anomaly));
520 }
521
522 let recent = metrics.get_recent_anomalies(Duration::from_secs(300)); if recent.len() >= 3 {
525 insights.push(self.create_collective_anomaly_insight(&recent));
526 }
527
528 insights
529 }
530
531 fn create_anomaly_insight(&self, anomaly: &AnomalyRecord) -> Insight {
533 let (title, description, category) = match anomaly.anomaly_type {
534 AnomalyType::LatencyAnomaly => (
535 format!("Latency Anomaly: {:.2}ms (z={:.2})", anomaly.value, anomaly.zscore),
536 format!(
537 "Detected latency anomaly with value {:.2}ms. Z-score of {:.2} indicates this is significantly outside normal range. {} consecutive anomalies detected.",
538 anomaly.value, anomaly.zscore, anomaly.consecutive_count
539 ),
540 InsightCategory::Anomaly,
541 ),
542 AnomalyType::CostAnomaly => (
543 format!("Cost Anomaly: ${:.4} (z={:.2})", anomaly.value, anomaly.zscore),
544 format!(
545 "Detected cost anomaly with value ${:.4}. Z-score of {:.2} indicates unusual spending pattern. {} consecutive anomalies detected.",
546 anomaly.value, anomaly.zscore, anomaly.consecutive_count
547 ),
548 InsightCategory::Cost,
549 ),
550 AnomalyType::VolumeAnomaly => (
551 format!("Volume Anomaly: {:.2} req/s", anomaly.value),
552 format!(
553 "Detected unusual request volume: {:.2} req/s. This represents a significant deviation from baseline traffic patterns.",
554 anomaly.value
555 ),
556 InsightCategory::Anomaly,
557 ),
558 AnomalyType::ErrorRateAnomaly => (
559 format!("Error Rate Anomaly: {:.1}%", anomaly.value * 100.0),
560 format!(
561 "Detected elevated error rate: {:.1}%. This is significantly higher than expected and requires investigation.",
562 anomaly.value * 100.0
563 ),
564 InsightCategory::Risk,
565 ),
566 AnomalyType::UnknownAnomaly => (
567 "Unknown Anomaly Detected".to_string(),
568 "Detected anomaly of unknown type.".to_string(),
569 InsightCategory::Anomaly,
570 ),
571 };
572
573 let confidence_val = (anomaly.zscore.abs() / 5.0)
575 .min(1.0)
576 .max(0.6)
577 * (anomaly.consecutive_count as f64 / (self.config.consecutive_threshold as f64 + 2.0))
578 .min(1.0)
579 .max(0.7);
580
581 Insight {
582 id: Uuid::new_v4().to_string(),
583 analyzer: "anomaly".to_string(),
584 timestamp: anomaly.timestamp,
585 severity: anomaly.severity,
586 confidence: Confidence::new(confidence_val),
587 title,
588 description,
589 category,
590 evidence: vec![Evidence {
591 evidence_type: EvidenceType::Anomaly,
592 description: format!(
593 "Z-score: {:.2}, Value: {:.2}, Type: {}",
594 anomaly.zscore,
595 anomaly.value,
596 anomaly.anomaly_type.as_str()
597 ),
598 data: serde_json::json!({
599 "zscore": anomaly.zscore,
600 "value": anomaly.value,
601 "anomaly_type": anomaly.anomaly_type.as_str(),
602 "consecutive_count": anomaly.consecutive_count,
603 }),
604 timestamp: anomaly.timestamp,
605 }],
606 metrics: {
607 let mut m = HashMap::new();
608 m.insert("zscore".to_string(), anomaly.zscore);
609 m.insert("value".to_string(), anomaly.value);
610 m.insert("consecutive_count".to_string(), anomaly.consecutive_count as f64);
611 m
612 },
613 tags: vec![
614 "anomaly".to_string(),
615 anomaly.anomaly_type.as_str().to_string(),
616 ],
617 }
618 }
619
620 fn create_collective_anomaly_insight(&self, anomalies: &[&AnomalyRecord]) -> Insight {
622 let anomaly_types: Vec<String> = anomalies
623 .iter()
624 .map(|a| a.anomaly_type.as_str().to_string())
625 .collect();
626
627 let highest_severity = anomalies
628 .iter()
629 .map(|a| a.severity)
630 .max()
631 .unwrap_or(Severity::Warning);
632
633 Insight {
634 id: Uuid::new_v4().to_string(),
635 analyzer: "anomaly".to_string(),
636 timestamp: Utc::now(),
637 severity: highest_severity,
638 confidence: Confidence::new(0.85),
639 title: format!("Collective Anomaly: {} anomalies detected", anomalies.len()),
640 description: format!(
641 "Detected {} anomalies occurring together in a short time window. Types: {}. This pattern suggests a systemic issue requiring investigation.",
642 anomalies.len(),
643 anomaly_types.join(", ")
644 ),
645 category: InsightCategory::Anomaly,
646 evidence: vec![Evidence {
647 evidence_type: EvidenceType::Pattern,
648 description: "Multiple concurrent anomalies detected".to_string(),
649 data: serde_json::json!({
650 "count": anomalies.len(),
651 "types": anomaly_types,
652 }),
653 timestamp: Utc::now(),
654 }],
655 metrics: {
656 let mut m = HashMap::new();
657 m.insert("anomaly_count".to_string(), anomalies.len() as f64);
658 m
659 },
660 tags: vec!["collective_anomaly".to_string(), "pattern".to_string()],
661 }
662 }
663
664 fn generate_recommendations(&self, insights: &[Insight]) -> Vec<Recommendation> {
666 let mut recommendations = Vec::new();
667
668 for insight in insights {
669 if insight.title.contains("Latency Anomaly") {
670 recommendations.push(Recommendation {
671 id: Uuid::new_v4().to_string(),
672 analyzer: "anomaly".to_string(),
673 timestamp: Utc::now(),
674 priority: if insight.severity == Severity::Critical {
675 Priority::Urgent
676 } else {
677 Priority::High
678 },
679 title: "Investigate latency anomaly root cause".to_string(),
680 description: "Latency anomaly detected. Investigate infrastructure, database queries, external API calls, and resource utilization.".to_string(),
681 action: Action {
682 action_type: ActionType::Investigate,
683 instructions: "1. Check infrastructure metrics (CPU, memory, network)\n2. Review recent deployments or configuration changes\n3. Analyze slow query logs\n4. Check external API response times\n5. Review application logs for errors".to_string(),
684 parameters: HashMap::new(),
685 estimated_effort: Some(2.0),
686 risk_level: RiskLevel::Low,
687 },
688 expected_impact: Impact {
689 performance: Some(ImpactMetric {
690 name: "latency".to_string(),
691 current: insight.metrics.get("value").copied().unwrap_or(0.0),
692 expected: insight.metrics.get("value").copied().unwrap_or(0.0) * 0.8,
693 unit: "ms".to_string(),
694 improvement_pct: 20.0,
695 }),
696 cost: None,
697 quality: None,
698 description: "Resolve latency anomaly and restore normal performance".to_string(),
699 },
700 confidence: Confidence::new(0.75),
701 related_insights: vec![insight.id.clone()],
702 tags: vec!["investigation".to_string(), "latency".to_string()],
703 });
704 }
705
706 if insight.title.contains("Cost Anomaly") {
707 recommendations.push(Recommendation {
708 id: Uuid::new_v4().to_string(),
709 analyzer: "anomaly".to_string(),
710 timestamp: Utc::now(),
711 priority: Priority::High,
712 title: "Review and adjust cost controls".to_string(),
713 description: "Cost anomaly detected. Review usage patterns and implement cost controls.".to_string(),
714 action: Action {
715 action_type: ActionType::Review,
716 instructions: "1. Review token usage patterns\n2. Check for unexpected traffic or abuse\n3. Implement rate limiting if needed\n4. Consider model switching for lower-priority tasks\n5. Set up cost alerts and budgets".to_string(),
717 parameters: HashMap::new(),
718 estimated_effort: Some(1.5),
719 risk_level: RiskLevel::Low,
720 },
721 expected_impact: Impact {
722 performance: None,
723 cost: Some(ImpactMetric {
724 name: "cost".to_string(),
725 current: insight.metrics.get("value").copied().unwrap_or(0.0),
726 expected: insight.metrics.get("value").copied().unwrap_or(0.0) * 0.7,
727 unit: "usd".to_string(),
728 improvement_pct: 30.0,
729 }),
730 quality: None,
731 description: "Reduce unexpected costs and establish controls".to_string(),
732 },
733 confidence: Confidence::new(0.80),
734 related_insights: vec![insight.id.clone()],
735 tags: vec!["cost".to_string(), "review".to_string()],
736 });
737 }
738
739 if insight.title.contains("Error Rate Anomaly") {
740 recommendations.push(Recommendation {
741 id: Uuid::new_v4().to_string(),
742 analyzer: "anomaly".to_string(),
743 timestamp: Utc::now(),
744 priority: Priority::Urgent,
745 title: "Investigate and resolve elevated error rate".to_string(),
746 description: "Elevated error rate detected. Immediate investigation required.".to_string(),
747 action: Action {
748 action_type: ActionType::Investigate,
749 instructions: "1. Review error logs and identify error patterns\n2. Check API status and rate limits\n3. Verify authentication and credentials\n4. Test with known-good requests\n5. Implement circuit breaker if needed".to_string(),
750 parameters: HashMap::new(),
751 estimated_effort: Some(1.0),
752 risk_level: RiskLevel::Medium,
753 },
754 expected_impact: Impact {
755 performance: None,
756 cost: None,
757 quality: Some(ImpactMetric {
758 name: "error_rate".to_string(),
759 current: insight.metrics.get("value").copied().unwrap_or(0.0),
760 expected: 0.01, unit: "rate".to_string(),
762 improvement_pct: 80.0,
763 }),
764 description: "Reduce error rate to normal levels".to_string(),
765 },
766 confidence: Confidence::new(0.85),
767 related_insights: vec![insight.id.clone()],
768 tags: vec!["errors".to_string(), "investigation".to_string()],
769 });
770 }
771
772 if insight.title.contains("Collective Anomaly") {
773 recommendations.push(Recommendation {
774 id: Uuid::new_v4().to_string(),
775 analyzer: "anomaly".to_string(),
776 timestamp: Utc::now(),
777 priority: Priority::Urgent,
778 title: "Add monitoring for correlated metrics".to_string(),
779 description: "Multiple anomalies detected together. Add monitoring to track correlated metrics.".to_string(),
780 action: Action {
781 action_type: ActionType::Monitor,
782 instructions: "1. Set up dashboards for correlated metrics\n2. Create alerts for anomaly patterns\n3. Implement automated runbooks for common patterns\n4. Review system architecture for single points of failure".to_string(),
783 parameters: HashMap::new(),
784 estimated_effort: Some(3.0),
785 risk_level: RiskLevel::Low,
786 },
787 expected_impact: Impact {
788 performance: None,
789 cost: None,
790 quality: None,
791 description: "Improved visibility and faster incident response".to_string(),
792 },
793 confidence: Confidence::new(0.70),
794 related_insights: vec![insight.id.clone()],
795 tags: vec!["monitoring".to_string(), "observability".to_string()],
796 });
797 }
798 }
799
800 recommendations
801 }
802
803 fn generate_alerts(&self, insights: &[Insight]) -> Vec<Alert> {
805 let mut alerts = Vec::new();
806
807 for insight in insights {
808 if insight.severity.is_urgent() {
809 alerts.push(Alert {
810 id: Uuid::new_v4().to_string(),
811 analyzer: "anomaly".to_string(),
812 timestamp: insight.timestamp,
813 severity: insight.severity,
814 title: insight.title.clone(),
815 message: insight.description.clone(),
816 status: AlertStatus::Firing,
817 threshold: None,
818 related_insights: vec![insight.id.clone()],
819 tags: insight.tags.clone(),
820 metadata: HashMap::new(),
821 });
822 }
823 }
824
825 alerts
826 }
827}
828
829#[async_trait]
830impl Analyzer for AnomalyAnalyzer {
831 fn name(&self) -> &str {
832 "anomaly"
833 }
834
835 fn config(&self) -> &AnalyzerConfig {
836 &self.config.base
837 }
838
839 fn state(&self) -> AnalyzerState {
840 self.state
841 }
842
843 async fn start(&mut self) -> AnalyzerResult<()> {
844 if self.state != AnalyzerState::Initialized && self.state != AnalyzerState::Stopped {
845 return Err(AnalyzerError::InvalidState(format!(
846 "Cannot start from state: {:?}",
847 self.state
848 )));
849 }
850
851 self.state = AnalyzerState::Starting;
852 info!("Starting Anomaly Analyzer");
853
854 self.state = AnalyzerState::Running;
855 info!("Anomaly Analyzer started successfully");
856
857 Ok(())
858 }
859
860 async fn stop(&mut self) -> AnalyzerResult<()> {
861 if !matches!(
862 self.state,
863 AnalyzerState::Running | AnalyzerState::Draining
864 ) {
865 return Err(AnalyzerError::InvalidState(format!(
866 "Cannot stop from state: {:?}",
867 self.state
868 )));
869 }
870
871 self.state = AnalyzerState::Draining;
872 info!("Stopping Anomaly Analyzer");
873
874 self.state = AnalyzerState::Stopped;
875 info!("Anomaly Analyzer stopped");
876
877 Ok(())
878 }
879
880 async fn process_event(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
881 if !self.state.can_accept_events() {
882 return Err(AnalyzerError::InvalidState(format!(
883 "Cannot process events in state: {:?}",
884 self.state
885 )));
886 }
887
888 let start = Instant::now();
889
890 let result = match &event {
891 AnalyzerEvent::Response { .. } => self.process_response(event).await,
892 AnalyzerEvent::Cost { .. } => self.process_cost(event).await,
893 AnalyzerEvent::Metric { .. } => self.process_metric(event).await,
894 _ => Ok(()), };
896
897 let mut stats = self.stats.write().await;
899 stats.events_processed += 1;
900 let elapsed = start.elapsed().as_micros() as u64;
901 stats.analysis_time_ms += elapsed / 1000;
902 stats.avg_analysis_time_us =
903 (stats.analysis_time_ms * 1000) as f64 / stats.events_processed as f64;
904
905 if result.is_err() {
906 stats.error_count += 1;
907 stats.last_error = result.as_ref().err().map(|e| e.to_string());
908 }
909
910 result
911 }
912
913 async fn generate_report(&self) -> AnalyzerResult<AnalysisReport> {
914 let metrics = self.metrics.read().await;
915 let stats = self.stats.read().await;
916 let last_report = *self.last_report_time.read().await;
917
918 let insights = self.analyze_metrics().await;
919 let recommendations = self.generate_recommendations(&insights);
920 let alerts = self.generate_alerts(&insights);
921
922 let recent_anomalies = metrics.get_recent_anomalies(Duration::from_secs(3600)); let mut report_metrics = HashMap::new();
925 report_metrics.insert("anomalies_detected".to_string(), recent_anomalies.len() as f64);
926 report_metrics.insert("latency_samples".to_string(), metrics.latencies.len() as f64);
927 report_metrics.insert("cost_samples".to_string(), metrics.costs.len() as f64);
928
929 let report = AnalysisReport {
930 analyzer: "anomaly".to_string(),
931 timestamp: Utc::now(),
932 period_start: Utc::now() - chrono::Duration::from_std(last_report.elapsed()).unwrap(),
933 period_end: Utc::now(),
934 summary: ReportSummary {
935 events_processed: stats.events_processed,
936 events_per_second: stats.events_processed as f64
937 / last_report.elapsed().as_secs_f64().max(1.0),
938 insights_count: insights.len() as u64,
939 recommendations_count: recommendations.len() as u64,
940 alerts_count: alerts.len() as u64,
941 analysis_duration_ms: stats.analysis_time_ms,
942 highlights: vec![
943 format!("Anomalies detected: {}", recent_anomalies.len()),
944 format!("Alerts triggered: {}", alerts.len()),
945 format!("Samples analyzed: {}", metrics.latencies.len()),
946 ],
947 },
948 insights,
949 recommendations,
950 alerts,
951 metrics: report_metrics,
952 };
953
954 *self.last_report_time.write().await = Instant::now();
956
957 Ok(report)
958 }
959
960 fn get_stats(&self) -> AnalyzerStats {
961 AnalyzerStats {
962 analyzer: "anomaly".to_string(),
963 uptime_seconds: self.start_time.elapsed().as_secs(),
964 ..Default::default()
965 }
966 }
967
968 async fn reset(&mut self) -> AnalyzerResult<()> {
969 let mut metrics = self.metrics.write().await;
970 *metrics = AnomalyMetrics::new(
971 self.config.anomaly_window_size,
972 self.config.drift_window_size,
973 );
974
975 let mut stats = self.stats.write().await;
976 *stats = AnalyzerStats {
977 analyzer: "anomaly".to_string(),
978 ..Default::default()
979 };
980
981 info!("Anomaly Analyzer reset");
982 Ok(())
983 }
984
985 async fn health_check(&self) -> AnalyzerResult<()> {
986 let stats = self.stats.read().await;
987
988 if stats.events_processed > 0 {
990 let error_rate = stats.error_count as f64 / stats.events_processed as f64;
991 if error_rate > 0.1 {
992 return Err(AnalyzerError::InternalError(format!(
993 "High error rate: {:.2}%",
994 error_rate * 100.0
995 )));
996 }
997 }
998
999 if stats.memory_usage_bytes > self.config.base.max_memory_bytes {
1001 return Err(AnalyzerError::ResourceExhausted(format!(
1002 "Memory usage {} exceeds limit {}",
1003 stats.memory_usage_bytes, self.config.base.max_memory_bytes
1004 )));
1005 }
1006
1007 Ok(())
1008 }
1009}
1010
1011#[cfg(test)]
1012mod tests {
1013 use super::*;
1014
1015 fn create_test_response_event(latency_ms: u64, success: bool) -> AnalyzerEvent {
1016 AnalyzerEvent::Response {
1017 timestamp: Utc::now(),
1018 request_id: Uuid::new_v4().to_string(),
1019 model: "gpt-4".to_string(),
1020 completion_tokens: 100,
1021 total_tokens: 200,
1022 latency_ms,
1023 success,
1024 error: if success {
1025 None
1026 } else {
1027 Some("Test error".to_string())
1028 },
1029 metadata: HashMap::new(),
1030 }
1031 }
1032
1033 fn create_test_cost_event(cost_usd: f64) -> AnalyzerEvent {
1034 AnalyzerEvent::Cost {
1035 timestamp: Utc::now(),
1036 model: "gpt-4".to_string(),
1037 tokens: 200,
1038 cost_usd,
1039 metadata: HashMap::new(),
1040 }
1041 }
1042
1043 #[tokio::test]
1044 async fn test_analyzer_lifecycle() {
1045 let mut analyzer = AnomalyAnalyzer::with_defaults();
1046
1047 assert_eq!(analyzer.state(), AnalyzerState::Initialized);
1048
1049 analyzer.start().await.unwrap();
1050 assert_eq!(analyzer.state(), AnalyzerState::Running);
1051
1052 analyzer.stop().await.unwrap();
1053 assert_eq!(analyzer.state(), AnalyzerState::Stopped);
1054 }
1055
1056 #[tokio::test]
1057 async fn test_process_response_event() {
1058 let mut analyzer = AnomalyAnalyzer::with_defaults();
1059 analyzer.start().await.unwrap();
1060
1061 let event = create_test_response_event(500, true);
1062 analyzer.process_event(event).await.unwrap();
1063
1064 let metrics = analyzer.metrics.read().await;
1065 assert_eq!(metrics.latencies.len(), 1);
1066 }
1067
1068 #[tokio::test]
1069 async fn test_latency_anomaly_detection() {
1070 let mut analyzer = AnomalyAnalyzer::with_defaults();
1071 analyzer.start().await.unwrap();
1072
1073 for _ in 0..50 {
1075 let event = create_test_response_event(100, true);
1076 analyzer.process_event(event).await.unwrap();
1077 }
1078
1079 for _ in 0..3 {
1081 let event = create_test_response_event(1000, true);
1082 analyzer.process_event(event).await.unwrap();
1083 }
1084
1085 let insights = analyzer.analyze_metrics().await;
1086 assert!(!insights.is_empty());
1087 assert!(insights
1088 .iter()
1089 .any(|i| i.title.contains("Latency Anomaly")));
1090 }
1091
1092 #[tokio::test]
1093 async fn test_cost_anomaly_detection() {
1094 let mut analyzer = AnomalyAnalyzer::with_defaults();
1095 analyzer.start().await.unwrap();
1096
1097 for _ in 0..50 {
1099 let event = create_test_cost_event(0.01);
1100 analyzer.process_event(event).await.unwrap();
1101 }
1102
1103 for _ in 0..3 {
1105 let event = create_test_cost_event(0.50);
1106 analyzer.process_event(event).await.unwrap();
1107 }
1108
1109 let insights = analyzer.analyze_metrics().await;
1110 assert!(insights.iter().any(|i| i.title.contains("Cost Anomaly")));
1111 }
1112
1113 #[tokio::test]
1114 async fn test_error_rate_anomaly_detection() {
1115 let mut analyzer = AnomalyAnalyzer::with_defaults();
1116 analyzer.start().await.unwrap();
1117
1118 for i in 0..50 {
1120 let event = create_test_response_event(500, i >= 40); analyzer.process_event(event).await.unwrap();
1122 }
1123
1124 let insights = analyzer.analyze_metrics().await;
1125 assert!(insights
1126 .iter()
1127 .any(|i| i.title.contains("Error Rate Anomaly")));
1128 }
1129
1130 #[tokio::test]
1131 async fn test_consecutive_threshold() {
1132 let mut config = AnomalyAnalyzerConfig::default();
1133 config.consecutive_threshold = 3;
1134
1135 let mut analyzer = AnomalyAnalyzer::new(config);
1136 analyzer.start().await.unwrap();
1137
1138 for _ in 0..50 {
1140 let event = create_test_response_event(100, true);
1141 analyzer.process_event(event).await.unwrap();
1142 }
1143
1144 for _ in 0..2 {
1146 let event = create_test_response_event(1000, true);
1147 analyzer.process_event(event).await.unwrap();
1148 }
1149
1150 let insights = analyzer.analyze_metrics().await;
1151 assert!(insights
1153 .iter()
1154 .filter(|i| i.title.contains("Latency Anomaly"))
1155 .count()
1156 == 0);
1157 }
1158
1159 #[tokio::test]
1160 async fn test_generate_report() {
1161 let mut analyzer = AnomalyAnalyzer::with_defaults();
1162 analyzer.start().await.unwrap();
1163
1164 for _ in 0..20 {
1166 let event = create_test_response_event(100, true);
1167 analyzer.process_event(event).await.unwrap();
1168 }
1169
1170 let report = analyzer.generate_report().await.unwrap();
1171 assert_eq!(report.analyzer, "anomaly");
1172 assert_eq!(report.summary.events_processed, 20);
1173 assert!(!report.metrics.is_empty());
1174 }
1175
1176 #[tokio::test]
1177 async fn test_reset() {
1178 let mut analyzer = AnomalyAnalyzer::with_defaults();
1179 analyzer.start().await.unwrap();
1180
1181 let event = create_test_response_event(500, true);
1182 analyzer.process_event(event).await.unwrap();
1183
1184 analyzer.reset().await.unwrap();
1185
1186 let metrics = analyzer.metrics.read().await;
1187 assert_eq!(metrics.latencies.len(), 0);
1188 assert_eq!(metrics.anomalies.len(), 0);
1189 }
1190
1191 #[tokio::test]
1192 async fn test_confidence_scoring() {
1193 let mut analyzer = AnomalyAnalyzer::with_defaults();
1194 analyzer.start().await.unwrap();
1195
1196 for _ in 0..50 {
1198 let event = create_test_response_event(100, true);
1199 analyzer.process_event(event).await.unwrap();
1200 }
1201
1202 for _ in 0..3 {
1204 let event = create_test_response_event(2000, true);
1205 analyzer.process_event(event).await.unwrap();
1206 }
1207
1208 let insights = analyzer.analyze_metrics().await;
1209 let anomaly_insights: Vec<_> = insights
1210 .iter()
1211 .filter(|i| i.title.contains("Latency Anomaly"))
1212 .collect();
1213
1214 assert!(!anomaly_insights.is_empty());
1215 assert!(anomaly_insights[0].confidence.value() > 0.6);
1217 }
1218}