Skip to main content

llm_optimizer_processor/analyzer/
anomaly_analyzer.rs

1//! Anomaly Analyzer
2//!
3//! Detects statistical anomalies, outliers, and drift in LLM metrics using
4//! Z-score and IQR methods. Provides confidence scoring and categorized alerts.
5
6use 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/// Configuration specific to the Anomaly Analyzer
28#[derive(Debug, Clone)]
29pub struct AnomalyAnalyzerConfig {
30    /// Base analyzer configuration
31    pub base: AnalyzerConfig,
32
33    /// Z-score threshold for anomaly detection (|z| > threshold)
34    pub zscore_threshold: f64,
35
36    /// IQR multiplier for outlier detection
37    pub iqr_multiplier: f64,
38
39    /// Minimum samples required for anomaly detection
40    pub min_samples: usize,
41
42    /// Number of consecutive anomalies required to trigger alert
43    pub consecutive_threshold: usize,
44
45    /// Window size for anomaly detection
46    pub anomaly_window_size: usize,
47
48    /// Enable drift detection
49    pub enable_drift_detection: bool,
50
51    /// Drift detection window size
52    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,      // 99.7% confidence interval
60            iqr_multiplier: 1.5,         // Standard outlier detection
61            min_samples: 30,             // Minimum for statistical significance
62            consecutive_threshold: 2,    // Reduce false positives
63            anomaly_window_size: 1000,
64            enable_drift_detection: true,
65            drift_window_size: 100,
66        }
67    }
68}
69
70/// Type of anomaly detected
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum AnomalyType {
73    /// Latency anomaly
74    LatencyAnomaly,
75    /// Cost anomaly
76    CostAnomaly,
77    /// Request volume anomaly
78    VolumeAnomaly,
79    /// Error rate anomaly
80    ErrorRateAnomaly,
81    /// Unknown anomaly type
82    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/// Detected anomaly record
98#[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/// Metrics tracked for anomaly detection
109#[derive(Debug, Clone)]
110struct AnomalyMetrics {
111    /// Latency samples (ms)
112    latencies: CircularBuffer<f64>,
113
114    /// Cost samples (USD)
115    costs: CircularBuffer<f64>,
116
117    /// Request timestamps for volume calculation
118    request_times: VecDeque<DateTime<Utc>>,
119
120    /// Error count over time
121    errors: CircularBuffer<u64>,
122    total_requests: CircularBuffer<u64>,
123
124    /// Detected anomalies
125    anomalies: VecDeque<AnomalyRecord>,
126
127    /// Consecutive anomaly counters
128    consecutive_latency: usize,
129    consecutive_cost: usize,
130    consecutive_volume: usize,
131    consecutive_error: usize,
132
133    /// Drift detection baseline
134    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        // Keep only recent requests for volume calculation
169        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        // Keep only recent anomalies
183        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
222/// Anomaly Analyzer implementation
223pub 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    /// Create a new Anomaly Analyzer
234    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    /// Create with default configuration
255    pub fn with_defaults() -> Self {
256        Self::new(AnomalyAnalyzerConfig::default())
257    }
258
259    /// Process a response event
260    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    /// Process a cost event
284    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    /// Process a metric event
302    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    /// Detect latency anomalies
325    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        // Z-score method
341        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        // IQR method
367        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    /// Detect cost anomalies
375    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    /// Detect volume anomalies
419    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        // Calculate volume over last minute
428        let current_volume = metrics.calculate_request_volume(Duration::from_secs(60));
429
430        // Compare with baseline if available
431        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                // 50% change threshold
436                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, // Approximate z-score
449                    severity,
450                    consecutive_count: 1,
451                });
452            }
453        }
454
455        anomalies
456    }
457
458    /// Detect error rate anomalies
459    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        // Alert if error rate exceeds thresholds
470        if error_rate > 0.05 {
471            // 5% error rate
472            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, // Approximate z-score
485                severity,
486                consecutive_count: 1,
487            });
488        }
489
490        anomalies
491    }
492
493    /// Analyze current metrics and generate insights
494    async fn analyze_metrics(&self) -> Vec<Insight> {
495        let mut insights = Vec::new();
496
497        // Detect all anomaly types
498        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        // Store anomalies
504        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        // Detect collective anomalies (multiple types occurring together)
523        let recent = metrics.get_recent_anomalies(Duration::from_secs(300)); // 5 minutes
524        if recent.len() >= 3 {
525            insights.push(self.create_collective_anomaly_insight(&recent));
526        }
527
528        insights
529    }
530
531    /// Create insight for an anomaly
532    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        // Calculate confidence based on z-score and consecutive count
574        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    /// Create insight for collective anomalies
621    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    /// Generate recommendations based on insights
665    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, // Target 1% error rate
761                            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    /// Generate alerts based on anomalies
804    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(()), // Ignore other event types
895        };
896
897        // Update stats
898        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)); // Last hour
923
924        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        // Update last report time
955        *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        // Check error rate
989        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        // Check memory usage
1000        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        // Add normal latencies
1074        for _ in 0..50 {
1075            let event = create_test_response_event(100, true);
1076            analyzer.process_event(event).await.unwrap();
1077        }
1078
1079        // Add anomalous latencies (consecutive to trigger detection)
1080        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        // Add normal costs
1098        for _ in 0..50 {
1099            let event = create_test_cost_event(0.01);
1100            analyzer.process_event(event).await.unwrap();
1101        }
1102
1103        // Add anomalous costs (consecutive)
1104        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        // Add events with high error rate
1119        for i in 0..50 {
1120            let event = create_test_response_event(500, i >= 40); // 80% failure
1121            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        // Add normal latencies
1139        for _ in 0..50 {
1140            let event = create_test_response_event(100, true);
1141            analyzer.process_event(event).await.unwrap();
1142        }
1143
1144        // Add only 2 anomalous latencies (below threshold)
1145        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        // Should not detect anomaly due to consecutive threshold
1152        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        // Add test events
1165        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        // Add normal latencies
1197        for _ in 0..50 {
1198            let event = create_test_response_event(100, true);
1199            analyzer.process_event(event).await.unwrap();
1200        }
1201
1202        // Add high z-score anomaly
1203        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        // Confidence should be reasonable (> 0.6)
1216        assert!(anomaly_insights[0].confidence.value() > 0.6);
1217    }
1218}