llm_optimizer_processor/analyzer/
pattern_analyzer.rs

1//! Pattern Analyzer
2//!
3//! Analyzes temporal, traffic, and usage patterns to identify trends, bursts,
4//! seasonality, and forecast future behavior.
5
6use async_trait::async_trait;
7use chrono::{DateTime, Datelike, Timelike, Utc};
8use std::collections::HashMap;
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_linear_trend, mean, percentage_change, std_dev, CircularBuffer,
17    SimpleMovingAverage,
18};
19use super::traits::{
20    Analyzer, AnalyzerConfig, AnalyzerError, AnalyzerResult, AnalyzerState,
21};
22use super::types::{
23    Action, ActionType, Alert, AlertStatus, AnalysisReport, AnalyzerEvent, AnalyzerStats,
24    Confidence, Evidence, EvidenceType, Impact, ImpactMetric, Insight, InsightCategory,
25    Priority, Recommendation, ReportSummary, RiskLevel, Severity, Threshold, ThresholdOperator,
26};
27
28/// Configuration specific to the Pattern Analyzer
29#[derive(Debug, Clone)]
30pub struct PatternAnalyzerConfig {
31    /// Base analyzer configuration
32    pub base: AnalyzerConfig,
33
34    /// Burst detection threshold (multiplier of average)
35    pub burst_threshold_multiplier: f64,
36
37    /// Growth rate threshold for alerting (percentage)
38    pub growth_rate_threshold_pct: f64,
39
40    /// Minimum requests for pattern detection
41    pub min_requests_for_patterns: usize,
42
43    /// Window size for moving averages
44    pub moving_average_window: usize,
45
46    /// Enable forecasting
47    pub enable_forecasting: bool,
48}
49
50impl Default for PatternAnalyzerConfig {
51    fn default() -> Self {
52        Self {
53            base: AnalyzerConfig::default(),
54            burst_threshold_multiplier: 2.0,
55            growth_rate_threshold_pct: 20.0,
56            min_requests_for_patterns: 50,
57            moving_average_window: 10,
58            enable_forecasting: true,
59        }
60    }
61}
62
63/// Types of patterns that can be detected
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum PatternType {
66    /// High traffic periods
67    PeakHours,
68    /// Low traffic periods
69    LowTraffic,
70    /// Sudden traffic spikes
71    TrafficBurst,
72    /// Consistent increase over time
73    SteadyGrowth,
74    /// Weekly/daily cycles
75    SeasonalPattern,
76}
77
78impl PatternType {
79    fn as_str(&self) -> &str {
80        match self {
81            PatternType::PeakHours => "peak_hours",
82            PatternType::LowTraffic => "low_traffic",
83            PatternType::TrafficBurst => "traffic_burst",
84            PatternType::SteadyGrowth => "steady_growth",
85            PatternType::SeasonalPattern => "seasonal_pattern",
86        }
87    }
88}
89
90/// Temporal pattern metrics
91#[derive(Debug, Clone)]
92struct TemporalMetrics {
93    /// Request count by hour of day (0-23)
94    hourly_counts: [u64; 24],
95
96    /// Request count by day of week (0-6, Monday=0)
97    daily_counts: [u64; 7],
98
99    /// Recent request timestamps for burst detection
100    recent_requests: CircularBuffer<DateTime<Utc>>,
101
102    /// Hourly request counts for trend analysis
103    hourly_series: Vec<(f64, f64)>, // (hour_index, count)
104
105    /// Moving average tracker
106    moving_average: SimpleMovingAverage,
107
108    /// Total requests processed
109    total_requests: u64,
110
111    /// Request counts by user ID
112    user_requests: HashMap<String, u64>,
113
114    /// Request counts by endpoint
115    endpoint_requests: HashMap<String, u64>,
116
117    /// Request counts by model
118    model_requests: HashMap<String, u64>,
119}
120
121impl TemporalMetrics {
122    fn new(window_size: usize, history_size: usize) -> Self {
123        Self {
124            hourly_counts: [0; 24],
125            daily_counts: [0; 7],
126            recent_requests: CircularBuffer::new(history_size),
127            hourly_series: Vec::new(),
128            moving_average: SimpleMovingAverage::new(window_size),
129            total_requests: 0,
130            user_requests: HashMap::new(),
131            endpoint_requests: HashMap::new(),
132            model_requests: HashMap::new(),
133        }
134    }
135
136    fn record_request(&mut self, timestamp: DateTime<Utc>, metadata: &HashMap<String, String>) {
137        // Record temporal patterns
138        let hour = timestamp.hour() as usize;
139        let day = timestamp.weekday().num_days_from_monday() as usize;
140
141        self.hourly_counts[hour] += 1;
142        self.daily_counts[day] += 1;
143        self.recent_requests.push(timestamp);
144        self.total_requests += 1;
145
146        // Record by user
147        if let Some(user_id) = metadata.get("user_id") {
148            *self.user_requests.entry(user_id.clone()).or_insert(0) += 1;
149        }
150
151        // Record by endpoint
152        if let Some(endpoint) = metadata.get("endpoint") {
153            *self.endpoint_requests.entry(endpoint.clone()).or_insert(0) += 1;
154        }
155
156        // Update moving average (requests per minute)
157        self.update_moving_average();
158    }
159
160    fn record_model(&mut self, model: &str) {
161        *self.model_requests.entry(model.to_string()).or_insert(0) += 1;
162    }
163
164    fn update_moving_average(&mut self) {
165        // Calculate requests in the last minute from recent_requests
166        let now = Utc::now();
167        let one_minute_ago = now - chrono::Duration::minutes(1);
168
169        let recent_count = self
170            .recent_requests
171            .as_slice()
172            .iter()
173            .filter(|&&ts| ts > one_minute_ago)
174            .count();
175
176        self.moving_average.update(recent_count as f64);
177    }
178
179    fn get_peak_hours(&self, threshold_multiplier: f64) -> Vec<usize> {
180        if self.total_requests < 24 {
181            return Vec::new();
182        }
183
184        let avg = self.hourly_counts.iter().sum::<u64>() as f64 / 24.0;
185        let threshold = avg * threshold_multiplier;
186
187        self.hourly_counts
188            .iter()
189            .enumerate()
190            .filter(|(_, &count)| count as f64 > threshold)
191            .map(|(hour, _)| hour)
192            .collect()
193    }
194
195    fn get_low_traffic_hours(&self, threshold_multiplier: f64) -> Vec<usize> {
196        if self.total_requests < 24 {
197            return Vec::new();
198        }
199
200        let avg = self.hourly_counts.iter().sum::<u64>() as f64 / 24.0;
201        let threshold = avg / threshold_multiplier;
202
203        self.hourly_counts
204            .iter()
205            .enumerate()
206            .filter(|(_, &count)| count > 0 && (count as f64) < threshold)
207            .map(|(hour, _)| hour)
208            .collect()
209    }
210
211    fn detect_burst(&self, threshold_multiplier: f64) -> Option<f64> {
212        if let Some(avg) = self.moving_average.value() {
213            if avg == 0.0 {
214                return None;
215            }
216
217            // Count requests in the last minute
218            let now = Utc::now();
219            let one_minute_ago = now - chrono::Duration::minutes(1);
220
221            let recent_count = self
222                .recent_requests
223                .as_slice()
224                .iter()
225                .filter(|&&ts| ts > one_minute_ago)
226                .count() as f64;
227
228            if recent_count > avg * threshold_multiplier {
229                return Some(recent_count / avg);
230            }
231        }
232
233        None
234    }
235
236    fn calculate_growth_rate(&self) -> Option<f64> {
237        // Use hourly series to calculate trend
238        if self.hourly_series.len() < 2 {
239            return None;
240        }
241
242        if let Some(trend) = calculate_linear_trend(&self.hourly_series) {
243            // Convert slope to percentage growth
244            if self.hourly_series.first().map(|(_, v)| *v).unwrap_or(0.0) > 0.0 {
245                let baseline = self.hourly_series.first().unwrap().1;
246                let growth_pct = (trend.slope / baseline) * 100.0;
247                return Some(growth_pct);
248            }
249        }
250
251        None
252    }
253
254    fn detect_weekday_pattern(&self) -> Option<f64> {
255        if self.total_requests < 100 {
256            return None;
257        }
258
259        // Compare weekday (Mon-Fri) vs weekend (Sat-Sun) traffic
260        let weekday_total: u64 = self.daily_counts[0..5].iter().sum();
261        let weekend_total: u64 = self.daily_counts[5..7].iter().sum();
262
263        if weekend_total == 0 {
264            return None;
265        }
266
267        let weekday_avg = weekday_total as f64 / 5.0;
268        let weekend_avg = weekend_total as f64 / 2.0;
269
270        if weekend_avg > 0.0 {
271            let difference_pct = ((weekday_avg - weekend_avg) / weekend_avg) * 100.0;
272            Some(difference_pct)
273        } else {
274            None
275        }
276    }
277
278    fn get_top_users(&self, limit: usize) -> Vec<(String, u64)> {
279        let mut users: Vec<_> = self.user_requests.iter()
280            .map(|(k, v)| (k.clone(), *v))
281            .collect();
282        users.sort_by(|a, b| b.1.cmp(&a.1));
283        users.truncate(limit);
284        users
285    }
286
287    fn get_top_endpoints(&self, limit: usize) -> Vec<(String, u64)> {
288        let mut endpoints: Vec<_> = self.endpoint_requests.iter()
289            .map(|(k, v)| (k.clone(), *v))
290            .collect();
291        endpoints.sort_by(|a, b| b.1.cmp(&a.1));
292        endpoints.truncate(limit);
293        endpoints
294    }
295}
296
297/// Pattern Analyzer implementation
298pub struct PatternAnalyzer {
299    config: PatternAnalyzerConfig,
300    state: AnalyzerState,
301    metrics: Arc<RwLock<TemporalMetrics>>,
302    stats: Arc<RwLock<AnalyzerStats>>,
303    start_time: Instant,
304    last_report_time: Arc<RwLock<Instant>>,
305}
306
307impl PatternAnalyzer {
308    /// Create a new Pattern Analyzer
309    pub fn new(config: PatternAnalyzerConfig) -> Self {
310        let window_size = config.moving_average_window;
311        let history_size = 1000;
312
313        Self {
314            config,
315            state: AnalyzerState::Initialized,
316            metrics: Arc::new(RwLock::new(TemporalMetrics::new(window_size, history_size))),
317            stats: Arc::new(RwLock::new(AnalyzerStats {
318                analyzer: "pattern".to_string(),
319                ..Default::default()
320            })),
321            start_time: Instant::now(),
322            last_report_time: Arc::new(RwLock::new(Instant::now())),
323        }
324    }
325
326    /// Create with default configuration
327    pub fn with_defaults() -> Self {
328        Self::new(PatternAnalyzerConfig::default())
329    }
330
331    /// Process a request event
332    async fn process_request(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
333        if let AnalyzerEvent::Request {
334            timestamp,
335            model,
336            metadata,
337            ..
338        } = event
339        {
340            let mut metrics = self.metrics.write().await;
341            metrics.record_request(timestamp, &metadata);
342            metrics.record_model(&model);
343
344            trace!(
345                "Recorded pattern metrics: timestamp={}, model={}",
346                timestamp,
347                model
348            );
349        }
350
351        Ok(())
352    }
353
354    /// Process a response event
355    async fn process_response(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
356        if let AnalyzerEvent::Response {
357            timestamp,
358            model,
359            metadata,
360            ..
361        } = event
362        {
363            let mut metrics = self.metrics.write().await;
364            metrics.record_request(timestamp, &metadata);
365            metrics.record_model(&model);
366
367            trace!(
368                "Recorded pattern metrics from response: timestamp={}, model={}",
369                timestamp,
370                model
371            );
372        }
373
374        Ok(())
375    }
376
377    /// Process a metric event
378    async fn process_metric(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
379        if let AnalyzerEvent::Metric {
380            timestamp,
381            labels,
382            ..
383        } = event
384        {
385            let mut metrics = self.metrics.write().await;
386            metrics.record_request(timestamp, &labels);
387        }
388
389        Ok(())
390    }
391
392    /// Analyze patterns and generate insights
393    async fn analyze_patterns(&self) -> Vec<Insight> {
394        let metrics = self.metrics.read().await;
395        let mut insights = Vec::new();
396
397        // Check if we have enough data
398        if metrics.total_requests < self.config.min_requests_for_patterns as u64 {
399            debug!(
400                "Insufficient data for pattern analysis: {} requests (minimum: {})",
401                metrics.total_requests, self.config.min_requests_for_patterns
402            );
403            return insights;
404        }
405
406        // Detect peak hours
407        let peak_hours = metrics.get_peak_hours(1.5);
408        if !peak_hours.is_empty() {
409            insights.push(self.create_peak_hours_insight(&peak_hours, &metrics));
410        }
411
412        // Detect low traffic hours
413        let low_traffic_hours = metrics.get_low_traffic_hours(2.0);
414        if !low_traffic_hours.is_empty() {
415            insights.push(self.create_low_traffic_insight(&low_traffic_hours, &metrics));
416        }
417
418        // Detect traffic bursts
419        if let Some(burst_ratio) = metrics.detect_burst(self.config.burst_threshold_multiplier) {
420            insights.push(self.create_burst_insight(burst_ratio));
421        }
422
423        // Detect growth trends
424        if let Some(growth_rate) = metrics.calculate_growth_rate() {
425            if growth_rate.abs() > self.config.growth_rate_threshold_pct {
426                insights.push(self.create_growth_insight(growth_rate));
427            }
428        }
429
430        // Detect weekday patterns
431        if let Some(weekday_diff) = metrics.detect_weekday_pattern() {
432            if weekday_diff.abs() > 30.0 {
433                insights.push(self.create_seasonal_insight(weekday_diff));
434            }
435        }
436
437        insights
438    }
439
440    /// Create insight for peak hours
441    fn create_peak_hours_insight(&self, peak_hours: &[usize], metrics: &TemporalMetrics) -> Insight {
442        let hours_str = if peak_hours.len() > 3 {
443            format!(
444                "{}:00-{}:00",
445                peak_hours.first().unwrap(),
446                peak_hours.last().unwrap()
447            )
448        } else {
449            peak_hours
450                .iter()
451                .map(|h| format!("{}:00", h))
452                .collect::<Vec<_>>()
453                .join(", ")
454        };
455
456        let peak_count: u64 = peak_hours.iter().map(|&h| metrics.hourly_counts[h]).sum();
457        let avg_count = metrics.hourly_counts.iter().sum::<u64>() as f64 / 24.0;
458        let peak_avg = peak_count as f64 / peak_hours.len() as f64;
459
460        Insight {
461            id: Uuid::new_v4().to_string(),
462            analyzer: "pattern".to_string(),
463            timestamp: Utc::now(),
464            severity: Severity::Info,
465            confidence: Confidence::new(0.90),
466            title: format!("Peak traffic hours: {}", hours_str),
467            description: format!(
468                "Traffic peaks during {} with {:.0}% higher volume than average. Peak hours average {:.0} requests/hour vs overall average of {:.0} requests/hour.",
469                hours_str,
470                ((peak_avg - avg_count) / avg_count) * 100.0,
471                peak_avg,
472                avg_count
473            ),
474            category: InsightCategory::Pattern,
475            evidence: vec![Evidence {
476                evidence_type: EvidenceType::Pattern,
477                description: format!("Hourly distribution analysis"),
478                data: serde_json::json!({
479                    "peak_hours": peak_hours,
480                    "peak_avg_requests": peak_avg,
481                    "overall_avg_requests": avg_count,
482                    "increase_pct": ((peak_avg - avg_count) / avg_count) * 100.0
483                }),
484                timestamp: Utc::now(),
485            }],
486            metrics: {
487                let mut m = HashMap::new();
488                m.insert("peak_avg_requests".to_string(), peak_avg);
489                m.insert("overall_avg_requests".to_string(), avg_count);
490                m
491            },
492            tags: vec!["temporal".to_string(), "peak_hours".to_string()],
493        }
494    }
495
496    /// Create insight for low traffic periods
497    fn create_low_traffic_insight(&self, low_hours: &[usize], metrics: &TemporalMetrics) -> Insight {
498        let hours_str = if low_hours.len() > 3 {
499            format!(
500                "{}:00-{}:00",
501                low_hours.first().unwrap(),
502                low_hours.last().unwrap()
503            )
504        } else {
505            low_hours
506                .iter()
507                .map(|h| format!("{}:00", h))
508                .collect::<Vec<_>>()
509                .join(", ")
510        };
511
512        let low_count: u64 = low_hours.iter().map(|&h| metrics.hourly_counts[h]).sum();
513        let low_avg = low_count as f64 / low_hours.len() as f64;
514
515        Insight {
516            id: Uuid::new_v4().to_string(),
517            analyzer: "pattern".to_string(),
518            timestamp: Utc::now(),
519            severity: Severity::Info,
520            confidence: Confidence::new(0.85),
521            title: format!("Low traffic period: {}", hours_str),
522            description: format!(
523                "Traffic is significantly lower during {} with an average of {:.0} requests/hour. Consider cost optimization during these periods.",
524                hours_str, low_avg
525            ),
526            category: InsightCategory::Pattern,
527            evidence: vec![Evidence {
528                evidence_type: EvidenceType::Pattern,
529                description: "Low traffic period detected".to_string(),
530                data: serde_json::json!({
531                    "low_traffic_hours": low_hours,
532                    "avg_requests": low_avg
533                }),
534                timestamp: Utc::now(),
535            }],
536            metrics: {
537                let mut m = HashMap::new();
538                m.insert("low_traffic_avg_requests".to_string(), low_avg);
539                m
540            },
541            tags: vec!["temporal".to_string(), "low_traffic".to_string()],
542        }
543    }
544
545    /// Create insight for traffic bursts
546    fn create_burst_insight(&self, burst_ratio: f64) -> Insight {
547        let severity = if burst_ratio > 5.0 {
548            Severity::Warning
549        } else {
550            Severity::Info
551        };
552
553        Insight {
554            id: Uuid::new_v4().to_string(),
555            analyzer: "pattern".to_string(),
556            timestamp: Utc::now(),
557            severity,
558            confidence: Confidence::new(0.92),
559            title: format!("Traffic burst detected: {:.1}x normal", burst_ratio),
560            description: format!(
561                "Current traffic is {:.1}x higher than the moving average. This sudden spike may indicate increased user activity, viral content, or potential issues.",
562                burst_ratio
563            ),
564            category: InsightCategory::Anomaly,
565            evidence: vec![Evidence {
566                evidence_type: EvidenceType::Anomaly,
567                description: "Burst detection via moving average".to_string(),
568                data: serde_json::json!({
569                    "burst_ratio": burst_ratio,
570                    "threshold": self.config.burst_threshold_multiplier
571                }),
572                timestamp: Utc::now(),
573            }],
574            metrics: {
575                let mut m = HashMap::new();
576                m.insert("burst_ratio".to_string(), burst_ratio);
577                m
578            },
579            tags: vec!["burst".to_string(), "traffic".to_string(), "anomaly".to_string()],
580        }
581    }
582
583    /// Create insight for growth trends
584    fn create_growth_insight(&self, growth_rate: f64) -> Insight {
585        let is_positive = growth_rate > 0.0;
586        let severity = if growth_rate.abs() > 50.0 {
587            Severity::Warning
588        } else {
589            Severity::Info
590        };
591
592        Insight {
593            id: Uuid::new_v4().to_string(),
594            analyzer: "pattern".to_string(),
595            timestamp: Utc::now(),
596            severity,
597            confidence: Confidence::new(0.82),
598            title: format!(
599                "{} trend: {:.1}% growth",
600                if is_positive { "Growth" } else { "Decline" },
601                growth_rate.abs()
602            ),
603            description: format!(
604                "Traffic shows a {} trend with {:.1}% {} over the analyzed period. {} capacity planning to accommodate this trend.",
605                if is_positive { "positive growth" } else { "declining" },
606                growth_rate.abs(),
607                if is_positive { "increase" } else { "decrease" },
608                if is_positive { "Consider" } else { "Review" }
609            ),
610            category: InsightCategory::Pattern,
611            evidence: vec![Evidence {
612                evidence_type: EvidenceType::TimeSeries,
613                description: "Linear trend analysis".to_string(),
614                data: serde_json::json!({
615                    "growth_rate_pct": growth_rate,
616                    "trend_type": if is_positive { "growth" } else { "decline" }
617                }),
618                timestamp: Utc::now(),
619            }],
620            metrics: {
621                let mut m = HashMap::new();
622                m.insert("growth_rate_pct".to_string(), growth_rate);
623                m
624            },
625            tags: vec![
626                "growth".to_string(),
627                "trend".to_string(),
628                if is_positive { "increasing".to_string() } else { "decreasing".to_string() }
629            ],
630        }
631    }
632
633    /// Create insight for seasonal patterns
634    fn create_seasonal_insight(&self, weekday_diff: f64) -> Insight {
635        let is_higher_weekday = weekday_diff > 0.0;
636
637        Insight {
638            id: Uuid::new_v4().to_string(),
639            analyzer: "pattern".to_string(),
640            timestamp: Utc::now(),
641            severity: Severity::Info,
642            confidence: Confidence::new(0.88),
643            title: format!(
644                "Weekly pattern: {:.0}% {} traffic on {}",
645                weekday_diff.abs(),
646                if is_higher_weekday { "higher" } else { "lower" },
647                if is_higher_weekday { "weekdays" } else { "weekends" }
648            ),
649            description: format!(
650                "Traffic shows a clear weekly pattern with {:.0}% {} volume on {} compared to {}. This seasonal pattern can be used for capacity planning and cost optimization.",
651                weekday_diff.abs(),
652                if is_higher_weekday { "higher" } else { "lower" },
653                if is_higher_weekday { "weekdays (Mon-Fri)" } else { "weekends (Sat-Sun)" },
654                if is_higher_weekday { "weekends" } else { "weekdays" }
655            ),
656            category: InsightCategory::Pattern,
657            evidence: vec![Evidence {
658                evidence_type: EvidenceType::Pattern,
659                description: "Weekday vs weekend comparison".to_string(),
660                data: serde_json::json!({
661                    "weekday_weekend_diff_pct": weekday_diff,
662                    "pattern_type": "weekly"
663                }),
664                timestamp: Utc::now(),
665            }],
666            metrics: {
667                let mut m = HashMap::new();
668                m.insert("weekday_weekend_diff_pct".to_string(), weekday_diff.abs());
669                m
670            },
671            tags: vec!["seasonal".to_string(), "weekly".to_string(), "pattern".to_string()],
672        }
673    }
674
675    /// Generate recommendations based on insights
676    fn generate_recommendations(&self, insights: &[Insight], metrics: &TemporalMetrics) -> Vec<Recommendation> {
677        let mut recommendations = Vec::new();
678
679        for insight in insights {
680            // Peak hours recommendations
681            if insight.tags.contains(&"peak_hours".to_string()) {
682                recommendations.push(Recommendation {
683                    id: Uuid::new_v4().to_string(),
684                    analyzer: "pattern".to_string(),
685                    timestamp: Utc::now(),
686                    priority: Priority::Medium,
687                    title: "Scale resources for peak hours".to_string(),
688                    description: "Configure auto-scaling to handle peak traffic periods efficiently.".to_string(),
689                    action: Action {
690                        action_type: ActionType::Scale,
691                        instructions: "1. Set up auto-scaling policies for peak hours\n2. Pre-warm resources before peak periods\n3. Configure rate limiting to protect during spikes".to_string(),
692                        parameters: HashMap::new(),
693                        estimated_effort: Some(3.0),
694                        risk_level: RiskLevel::Low,
695                    },
696                    expected_impact: Impact {
697                        performance: Some(ImpactMetric {
698                            name: "latency_during_peaks".to_string(),
699                            current: 0.0,
700                            expected: 0.0,
701                            unit: "ms".to_string(),
702                            improvement_pct: 25.0,
703                        }),
704                        cost: Some(ImpactMetric {
705                            name: "infrastructure_cost".to_string(),
706                            current: 100.0,
707                            expected: 110.0,
708                            unit: "usd".to_string(),
709                            improvement_pct: -10.0,
710                        }),
711                        quality: None,
712                        description: "Better performance during peaks with slightly higher cost".to_string(),
713                    },
714                    confidence: Confidence::new(0.85),
715                    related_insights: vec![insight.id.clone()],
716                    tags: vec!["scaling".to_string(), "capacity".to_string()],
717                });
718            }
719
720            // Low traffic recommendations
721            if insight.tags.contains(&"low_traffic".to_string()) {
722                recommendations.push(Recommendation {
723                    id: Uuid::new_v4().to_string(),
724                    analyzer: "pattern".to_string(),
725                    timestamp: Utc::now(),
726                    priority: Priority::Low,
727                    title: "Optimize costs during low-traffic periods".to_string(),
728                    description: "Scale down resources during consistently low-traffic hours to reduce costs.".to_string(),
729                    action: Action {
730                        action_type: ActionType::ConfigChange,
731                        instructions: "1. Configure auto-scaling to reduce capacity during low-traffic hours\n2. Consider spot instances for non-critical workloads\n3. Schedule batch jobs during these periods".to_string(),
732                        parameters: HashMap::new(),
733                        estimated_effort: Some(2.0),
734                        risk_level: RiskLevel::Low,
735                    },
736                    expected_impact: Impact {
737                        performance: None,
738                        cost: Some(ImpactMetric {
739                            name: "infrastructure_cost".to_string(),
740                            current: 100.0,
741                            expected: 85.0,
742                            unit: "usd".to_string(),
743                            improvement_pct: 15.0,
744                        }),
745                        quality: None,
746                        description: "15% cost savings with no performance impact".to_string(),
747                    },
748                    confidence: Confidence::new(0.80),
749                    related_insights: vec![insight.id.clone()],
750                    tags: vec!["cost_optimization".to_string(), "scaling".to_string()],
751                });
752            }
753
754            // Burst recommendations
755            if insight.tags.contains(&"burst".to_string()) {
756                recommendations.push(Recommendation {
757                    id: Uuid::new_v4().to_string(),
758                    analyzer: "pattern".to_string(),
759                    timestamp: Utc::now(),
760                    priority: Priority::High,
761                    title: "Implement burst handling strategies".to_string(),
762                    description: "Deploy caching and rate limiting to handle traffic bursts efficiently.".to_string(),
763                    action: Action {
764                        action_type: ActionType::ConfigChange,
765                        instructions: "1. Enable CDN caching for static responses\n2. Implement request coalescing for identical prompts\n3. Set up rate limiting per user/API key\n4. Configure burst capacity in load balancers".to_string(),
766                        parameters: HashMap::new(),
767                        estimated_effort: Some(4.0),
768                        risk_level: RiskLevel::Medium,
769                    },
770                    expected_impact: Impact {
771                        performance: Some(ImpactMetric {
772                            name: "burst_handling".to_string(),
773                            current: 0.0,
774                            expected: 0.0,
775                            unit: "requests".to_string(),
776                            improvement_pct: 50.0,
777                        }),
778                        cost: Some(ImpactMetric {
779                            name: "api_cost".to_string(),
780                            current: 100.0,
781                            expected: 80.0,
782                            unit: "usd".to_string(),
783                            improvement_pct: 20.0,
784                        }),
785                        quality: None,
786                        description: "Better burst handling with 20% cost reduction via caching".to_string(),
787                    },
788                    confidence: Confidence::new(0.82),
789                    related_insights: vec![insight.id.clone()],
790                    tags: vec!["caching".to_string(), "rate_limiting".to_string()],
791                });
792            }
793
794            // Growth recommendations
795            if insight.tags.contains(&"growth".to_string()) && insight.tags.contains(&"increasing".to_string()) {
796                recommendations.push(Recommendation {
797                    id: Uuid::new_v4().to_string(),
798                    analyzer: "pattern".to_string(),
799                    timestamp: Utc::now(),
800                    priority: Priority::High,
801                    title: "Plan for continued growth".to_string(),
802                    description: "Proactively scale infrastructure to accommodate growth trend.".to_string(),
803                    action: Action {
804                        action_type: ActionType::Review,
805                        instructions: "1. Review capacity projections for next 30-90 days\n2. Negotiate volume discounts with LLM providers\n3. Consider reserved capacity for predictable baseline\n4. Implement aggressive caching strategies".to_string(),
806                        parameters: HashMap::new(),
807                        estimated_effort: Some(6.0),
808                        risk_level: RiskLevel::Medium,
809                    },
810                    expected_impact: Impact {
811                        performance: None,
812                        cost: Some(ImpactMetric {
813                            name: "future_cost".to_string(),
814                            current: 100.0,
815                            expected: 120.0,
816                            unit: "usd".to_string(),
817                            improvement_pct: -20.0,
818                        }),
819                        quality: None,
820                        description: "Ensure capacity for growth with optimized pricing".to_string(),
821                    },
822                    confidence: Confidence::new(0.75),
823                    related_insights: vec![insight.id.clone()],
824                    tags: vec!["capacity_planning".to_string(), "growth".to_string()],
825                });
826            }
827        }
828
829        // Add caching recommendation for common patterns
830        let top_endpoints = metrics.get_top_endpoints(5);
831        if !top_endpoints.is_empty() {
832            let top_endpoint_pct = (top_endpoints.first().unwrap().1 as f64 / metrics.total_requests as f64) * 100.0;
833            if top_endpoint_pct > 20.0 {
834                recommendations.push(Recommendation {
835                    id: Uuid::new_v4().to_string(),
836                    analyzer: "pattern".to_string(),
837                    timestamp: Utc::now(),
838                    priority: Priority::Medium,
839                    title: "Cache responses for common endpoints".to_string(),
840                    description: format!(
841                        "Top endpoint accounts for {:.1}% of traffic. Implement semantic caching.",
842                        top_endpoint_pct
843                    ),
844                    action: Action {
845                        action_type: ActionType::Optimize,
846                        instructions: "1. Implement semantic caching for similar prompts\n2. Cache responses with TTL based on content type\n3. Monitor cache hit rates and adjust strategies".to_string(),
847                        parameters: HashMap::new(),
848                        estimated_effort: Some(5.0),
849                        risk_level: RiskLevel::Low,
850                    },
851                    expected_impact: Impact {
852                        performance: Some(ImpactMetric {
853                            name: "latency".to_string(),
854                            current: 1000.0,
855                            expected: 100.0,
856                            unit: "ms".to_string(),
857                            improvement_pct: 90.0,
858                        }),
859                        cost: Some(ImpactMetric {
860                            name: "api_cost".to_string(),
861                            current: 100.0,
862                            expected: 70.0,
863                            unit: "usd".to_string(),
864                            improvement_pct: 30.0,
865                        }),
866                        quality: None,
867                        description: "90% latency reduction and 30% cost savings via caching".to_string(),
868                    },
869                    confidence: Confidence::new(0.88),
870                    related_insights: vec![],
871                    tags: vec!["caching".to_string(), "optimization".to_string()],
872                });
873            }
874        }
875
876        recommendations
877    }
878}
879
880#[async_trait]
881impl Analyzer for PatternAnalyzer {
882    fn name(&self) -> &str {
883        "pattern"
884    }
885
886    fn config(&self) -> &AnalyzerConfig {
887        &self.config.base
888    }
889
890    fn state(&self) -> AnalyzerState {
891        self.state
892    }
893
894    async fn start(&mut self) -> AnalyzerResult<()> {
895        if self.state != AnalyzerState::Initialized && self.state != AnalyzerState::Stopped {
896            return Err(AnalyzerError::InvalidState(format!(
897                "Cannot start from state: {:?}",
898                self.state
899            )));
900        }
901
902        self.state = AnalyzerState::Starting;
903        info!("Starting Pattern Analyzer");
904
905        // Initialization logic here
906
907        self.state = AnalyzerState::Running;
908        info!("Pattern Analyzer started successfully");
909
910        Ok(())
911    }
912
913    async fn stop(&mut self) -> AnalyzerResult<()> {
914        if !matches!(
915            self.state,
916            AnalyzerState::Running | AnalyzerState::Draining
917        ) {
918            return Err(AnalyzerError::InvalidState(format!(
919                "Cannot stop from state: {:?}",
920                self.state
921            )));
922        }
923
924        self.state = AnalyzerState::Draining;
925        info!("Stopping Pattern Analyzer");
926
927        // Cleanup logic here
928
929        self.state = AnalyzerState::Stopped;
930        info!("Pattern Analyzer stopped");
931
932        Ok(())
933    }
934
935    async fn process_event(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
936        if !self.state.can_accept_events() {
937            return Err(AnalyzerError::InvalidState(format!(
938                "Cannot process events in state: {:?}",
939                self.state
940            )));
941        }
942
943        let start = Instant::now();
944
945        let result = match &event {
946            AnalyzerEvent::Request { .. } => self.process_request(event).await,
947            AnalyzerEvent::Response { .. } => self.process_response(event).await,
948            AnalyzerEvent::Metric { .. } => self.process_metric(event).await,
949            _ => Ok(()), // Ignore other event types
950        };
951
952        // Update stats
953        let mut stats = self.stats.write().await;
954        stats.events_processed += 1;
955        let elapsed = start.elapsed().as_micros() as u64;
956        stats.analysis_time_ms += elapsed / 1000;
957        stats.avg_analysis_time_us =
958            (stats.analysis_time_ms * 1000) as f64 / stats.events_processed as f64;
959
960        if result.is_err() {
961            stats.error_count += 1;
962            stats.last_error = result.as_ref().err().map(|e| e.to_string());
963        }
964
965        result
966    }
967
968    async fn generate_report(&self) -> AnalyzerResult<AnalysisReport> {
969        let metrics = self.metrics.read().await;
970        let stats = self.stats.read().await;
971        let last_report = *self.last_report_time.read().await;
972
973        let insights = self.analyze_patterns().await;
974        let recommendations = self.generate_recommendations(&insights, &metrics);
975        let alerts = Vec::new(); // Alerts would be generated based on thresholds
976
977        let mut report_metrics = HashMap::new();
978        report_metrics.insert("total_requests".to_string(), metrics.total_requests as f64);
979
980        if let Some(ma) = metrics.moving_average.value() {
981            report_metrics.insert("requests_per_minute".to_string(), ma);
982        }
983
984        // Add hourly distribution
985        for (hour, count) in metrics.hourly_counts.iter().enumerate() {
986            report_metrics.insert(format!("hour_{}", hour), *count as f64);
987        }
988
989        // Add daily distribution
990        let days = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
991        for (day, count) in metrics.daily_counts.iter().enumerate() {
992            report_metrics.insert(format!("day_{}", days[day]), *count as f64);
993        }
994
995        let report = AnalysisReport {
996            analyzer: "pattern".to_string(),
997            timestamp: Utc::now(),
998            period_start: Utc::now() - chrono::Duration::from_std(last_report.elapsed()).unwrap(),
999            period_end: Utc::now(),
1000            summary: ReportSummary {
1001                events_processed: stats.events_processed,
1002                events_per_second: stats.events_processed as f64
1003                    / last_report.elapsed().as_secs_f64(),
1004                insights_count: insights.len() as u64,
1005                recommendations_count: recommendations.len() as u64,
1006                alerts_count: alerts.len() as u64,
1007                analysis_duration_ms: stats.analysis_time_ms,
1008                highlights: vec![
1009                    format!("Total requests: {}", metrics.total_requests),
1010                    format!("Unique users: {}", metrics.user_requests.len()),
1011                    format!("Unique endpoints: {}", metrics.endpoint_requests.len()),
1012                ],
1013            },
1014            insights,
1015            recommendations,
1016            alerts,
1017            metrics: report_metrics,
1018        };
1019
1020        // Update last report time
1021        *self.last_report_time.write().await = Instant::now();
1022
1023        Ok(report)
1024    }
1025
1026    fn get_stats(&self) -> AnalyzerStats {
1027        // This is synchronous, so we can't use .await
1028        AnalyzerStats {
1029            analyzer: "pattern".to_string(),
1030            uptime_seconds: self.start_time.elapsed().as_secs(),
1031            ..Default::default()
1032        }
1033    }
1034
1035    async fn reset(&mut self) -> AnalyzerResult<()> {
1036        let mut metrics = self.metrics.write().await;
1037        *metrics = TemporalMetrics::new(
1038            self.config.moving_average_window,
1039            1000,
1040        );
1041
1042        let mut stats = self.stats.write().await;
1043        *stats = AnalyzerStats {
1044            analyzer: "pattern".to_string(),
1045            ..Default::default()
1046        };
1047
1048        info!("Pattern Analyzer reset");
1049        Ok(())
1050    }
1051
1052    async fn health_check(&self) -> AnalyzerResult<()> {
1053        let stats = self.stats.read().await;
1054
1055        // Check error rate
1056        if stats.events_processed > 0 {
1057            let error_rate = stats.error_count as f64 / stats.events_processed as f64;
1058            if error_rate > 0.1 {
1059                return Err(AnalyzerError::InternalError(format!(
1060                    "High error rate: {:.2}%",
1061                    error_rate * 100.0
1062                )));
1063            }
1064        }
1065
1066        // Check memory usage
1067        if stats.memory_usage_bytes > self.config.base.max_memory_bytes {
1068            return Err(AnalyzerError::ResourceExhausted(format!(
1069                "Memory usage {} exceeds limit {}",
1070                stats.memory_usage_bytes, self.config.base.max_memory_bytes
1071            )));
1072        }
1073
1074        Ok(())
1075    }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081
1082    fn create_test_request_event(hour: u32, metadata: HashMap<String, String>) -> AnalyzerEvent {
1083        let timestamp = Utc::now()
1084            .date_naive()
1085            .and_hms_opt(hour, 0, 0)
1086            .unwrap()
1087            .and_utc();
1088
1089        AnalyzerEvent::Request {
1090            timestamp,
1091            request_id: Uuid::new_v4().to_string(),
1092            model: "gpt-4".to_string(),
1093            prompt_tokens: 100,
1094            max_tokens: 1000,
1095            temperature: 0.7,
1096            metadata,
1097        }
1098    }
1099
1100    fn create_test_response_event(hour: u32) -> AnalyzerEvent {
1101        let timestamp = Utc::now()
1102            .date_naive()
1103            .and_hms_opt(hour, 0, 0)
1104            .unwrap()
1105            .and_utc();
1106
1107        AnalyzerEvent::Response {
1108            timestamp,
1109            request_id: Uuid::new_v4().to_string(),
1110            model: "gpt-4".to_string(),
1111            completion_tokens: 100,
1112            total_tokens: 200,
1113            latency_ms: 500,
1114            success: true,
1115            error: None,
1116            metadata: HashMap::new(),
1117        }
1118    }
1119
1120    #[tokio::test]
1121    async fn test_analyzer_lifecycle() {
1122        let mut analyzer = PatternAnalyzer::with_defaults();
1123
1124        assert_eq!(analyzer.state(), AnalyzerState::Initialized);
1125
1126        analyzer.start().await.unwrap();
1127        assert_eq!(analyzer.state(), AnalyzerState::Running);
1128
1129        analyzer.stop().await.unwrap();
1130        assert_eq!(analyzer.state(), AnalyzerState::Stopped);
1131    }
1132
1133    #[tokio::test]
1134    async fn test_process_request_event() {
1135        let mut analyzer = PatternAnalyzer::with_defaults();
1136        analyzer.start().await.unwrap();
1137
1138        let event = create_test_request_event(14, HashMap::new());
1139        analyzer.process_event(event).await.unwrap();
1140
1141        let metrics = analyzer.metrics.read().await;
1142        assert_eq!(metrics.total_requests, 1);
1143        assert_eq!(metrics.hourly_counts[14], 1);
1144    }
1145
1146    #[tokio::test]
1147    async fn test_peak_hours_detection() {
1148        let mut analyzer = PatternAnalyzer::with_defaults();
1149        analyzer.start().await.unwrap();
1150
1151        // Create traffic pattern with peak at 2-4 PM
1152        for _ in 0..20 {
1153            for hour in 0..24 {
1154                let count = if (14..=16).contains(&hour) { 10 } else { 2 };
1155                for _ in 0..count {
1156                    let event = create_test_response_event(hour);
1157                    analyzer.process_event(event).await.unwrap();
1158                }
1159            }
1160        }
1161
1162        let insights = analyzer.analyze_patterns().await;
1163        assert!(!insights.is_empty());
1164        assert!(insights.iter().any(|i| i.title.contains("Peak traffic")));
1165    }
1166
1167    #[tokio::test]
1168    async fn test_low_traffic_detection() {
1169        let mut analyzer = PatternAnalyzer::with_defaults();
1170        analyzer.start().await.unwrap();
1171
1172        // Create traffic pattern with low traffic at night
1173        for _ in 0..20 {
1174            for hour in 0..24 {
1175                let count = if (0..=5).contains(&hour) { 1 } else { 10 };
1176                for _ in 0..count {
1177                    let event = create_test_response_event(hour);
1178                    analyzer.process_event(event).await.unwrap();
1179                }
1180            }
1181        }
1182
1183        let insights = analyzer.analyze_patterns().await;
1184        assert!(insights.iter().any(|i| i.title.contains("Low traffic")));
1185    }
1186
1187    #[tokio::test]
1188    async fn test_burst_detection() {
1189        let mut analyzer = PatternAnalyzer::with_defaults();
1190        analyzer.start().await.unwrap();
1191
1192        // Establish baseline
1193        for _ in 0..100 {
1194            let event = create_test_response_event(12);
1195            analyzer.process_event(event).await.unwrap();
1196            tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
1197        }
1198
1199        // Create burst
1200        for _ in 0..500 {
1201            let event = create_test_response_event(12);
1202            analyzer.process_event(event).await.unwrap();
1203        }
1204
1205        let insights = analyzer.analyze_patterns().await;
1206        assert!(insights.iter().any(|i| i.title.contains("burst")));
1207    }
1208
1209    #[tokio::test]
1210    async fn test_seasonal_pattern_detection() {
1211        let mut analyzer = PatternAnalyzer::with_defaults();
1212        analyzer.start().await.unwrap();
1213
1214        // Create weekday/weekend pattern
1215        let mut metrics = analyzer.metrics.write().await;
1216
1217        // Weekdays (Mon-Fri): high traffic
1218        for day in 0..5 {
1219            metrics.daily_counts[day] = 100;
1220        }
1221
1222        // Weekends (Sat-Sun): low traffic
1223        metrics.daily_counts[5] = 30;
1224        metrics.daily_counts[6] = 30;
1225
1226        metrics.total_requests = 560;
1227        drop(metrics);
1228
1229        let insights = analyzer.analyze_patterns().await;
1230        assert!(insights.iter().any(|i| i.title.contains("Weekly pattern")));
1231    }
1232
1233    #[tokio::test]
1234    async fn test_user_tracking() {
1235        let mut analyzer = PatternAnalyzer::with_defaults();
1236        analyzer.start().await.unwrap();
1237
1238        let mut metadata = HashMap::new();
1239        metadata.insert("user_id".to_string(), "user123".to_string());
1240
1241        for _ in 0..10 {
1242            let event = create_test_request_event(12, metadata.clone());
1243            analyzer.process_event(event).await.unwrap();
1244        }
1245
1246        let metrics = analyzer.metrics.read().await;
1247        assert_eq!(metrics.user_requests.get("user123"), Some(&10));
1248    }
1249
1250    #[tokio::test]
1251    async fn test_endpoint_tracking() {
1252        let mut analyzer = PatternAnalyzer::with_defaults();
1253        analyzer.start().await.unwrap();
1254
1255        let mut metadata = HashMap::new();
1256        metadata.insert("endpoint".to_string(), "/api/chat".to_string());
1257
1258        for _ in 0..15 {
1259            let event = create_test_request_event(12, metadata.clone());
1260            analyzer.process_event(event).await.unwrap();
1261        }
1262
1263        let metrics = analyzer.metrics.read().await;
1264        assert_eq!(metrics.endpoint_requests.get("/api/chat"), Some(&15));
1265    }
1266
1267    #[tokio::test]
1268    async fn test_generate_report() {
1269        let mut analyzer = PatternAnalyzer::with_defaults();
1270        analyzer.start().await.unwrap();
1271
1272        // Add test events
1273        for hour in 0..24 {
1274            for _ in 0..5 {
1275                let event = create_test_response_event(hour);
1276                analyzer.process_event(event).await.unwrap();
1277            }
1278        }
1279
1280        let report = analyzer.generate_report().await.unwrap();
1281        assert_eq!(report.analyzer, "pattern");
1282        assert!(report.summary.events_processed > 0);
1283        assert!(!report.metrics.is_empty());
1284    }
1285
1286    #[tokio::test]
1287    async fn test_reset() {
1288        let mut analyzer = PatternAnalyzer::with_defaults();
1289        analyzer.start().await.unwrap();
1290
1291        let event = create_test_response_event(12);
1292        analyzer.process_event(event).await.unwrap();
1293
1294        analyzer.reset().await.unwrap();
1295
1296        let metrics = analyzer.metrics.read().await;
1297        assert_eq!(metrics.total_requests, 0);
1298        assert_eq!(metrics.hourly_counts.iter().sum::<u64>(), 0);
1299    }
1300
1301    #[tokio::test]
1302    async fn test_recommendations_generation() {
1303        let mut analyzer = PatternAnalyzer::with_defaults();
1304        analyzer.start().await.unwrap();
1305
1306        // Create pattern with peak hours
1307        for _ in 0..20 {
1308            for hour in 0..24 {
1309                let count = if (14..=16).contains(&hour) { 10 } else { 2 };
1310                for _ in 0..count {
1311                    let event = create_test_response_event(hour);
1312                    analyzer.process_event(event).await.unwrap();
1313                }
1314            }
1315        }
1316
1317        let report = analyzer.generate_report().await.unwrap();
1318        assert!(!report.recommendations.is_empty());
1319        assert!(report.recommendations.iter().any(|r| r.title.contains("Scale")));
1320    }
1321}