1use 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#[derive(Debug, Clone)]
30pub struct PatternAnalyzerConfig {
31 pub base: AnalyzerConfig,
33
34 pub burst_threshold_multiplier: f64,
36
37 pub growth_rate_threshold_pct: f64,
39
40 pub min_requests_for_patterns: usize,
42
43 pub moving_average_window: usize,
45
46 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum PatternType {
66 PeakHours,
68 LowTraffic,
70 TrafficBurst,
72 SteadyGrowth,
74 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#[derive(Debug, Clone)]
92struct TemporalMetrics {
93 hourly_counts: [u64; 24],
95
96 daily_counts: [u64; 7],
98
99 recent_requests: CircularBuffer<DateTime<Utc>>,
101
102 hourly_series: Vec<(f64, f64)>, moving_average: SimpleMovingAverage,
107
108 total_requests: u64,
110
111 user_requests: HashMap<String, u64>,
113
114 endpoint_requests: HashMap<String, u64>,
116
117 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 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 if let Some(user_id) = metadata.get("user_id") {
148 *self.user_requests.entry(user_id.clone()).or_insert(0) += 1;
149 }
150
151 if let Some(endpoint) = metadata.get("endpoint") {
153 *self.endpoint_requests.entry(endpoint.clone()).or_insert(0) += 1;
154 }
155
156 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 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 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 if self.hourly_series.len() < 2 {
239 return None;
240 }
241
242 if let Some(trend) = calculate_linear_trend(&self.hourly_series) {
243 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 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
297pub 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 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 pub fn with_defaults() -> Self {
328 Self::new(PatternAnalyzerConfig::default())
329 }
330
331 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 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 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 async fn analyze_patterns(&self) -> Vec<Insight> {
394 let metrics = self.metrics.read().await;
395 let mut insights = Vec::new();
396
397 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 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 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 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 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 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 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 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 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 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 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 fn generate_recommendations(&self, insights: &[Insight], metrics: &TemporalMetrics) -> Vec<Recommendation> {
677 let mut recommendations = Vec::new();
678
679 for insight in insights {
680 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 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 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 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 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 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 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(()), };
951
952 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(); 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 for (hour, count) in metrics.hourly_counts.iter().enumerate() {
986 report_metrics.insert(format!("hour_{}", hour), *count as f64);
987 }
988
989 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 *self.last_report_time.write().await = Instant::now();
1022
1023 Ok(report)
1024 }
1025
1026 fn get_stats(&self) -> AnalyzerStats {
1027 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 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 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 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 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 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 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 let mut metrics = analyzer.metrics.write().await;
1216
1217 for day in 0..5 {
1219 metrics.daily_counts[day] = 100;
1220 }
1221
1222 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 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 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}