Skip to main content

llm_optimizer_processor/analyzer/
quality_analyzer.rs

1//! Quality Analyzer
2//!
3//! Monitors response quality, error rates, SLA compliance, and user feedback.
4//!
5//! # Features
6//!
7//! - **Error Rate Tracking**: Success/failure rates with thresholds
8//! - **SLA Compliance**: Latency-based SLA monitoring (P95, P99)
9//! - **Feedback Analysis**: User rating tracking and sentiment
10//! - **Response Quality**: Completeness, accuracy, relevance metrics
11//! - **Quality Degradation**: Detect declining quality trends
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use processor::analyzer::{QualityAnalyzer, QualityAnalyzerConfig, Analyzer, AnalyzerEvent};
17//! use chrono::Utc;
18//! use std::collections::HashMap;
19//!
20//! #[tokio::main]
21//! async fn main() -> anyhow::Result<()> {
22//!     // Create analyzer with SLA targets
23//!     let config = QualityAnalyzerConfig {
24//!         sla_p95_ms: 1000.0,
25//!         sla_p99_ms: 2000.0,
26//!         min_success_rate_pct: 99.5,
27//!         min_avg_rating: 4.0,
28//!         ..Default::default()
29//!     };
30//!
31//!     let mut analyzer = QualityAnalyzer::new(config);
32//!     analyzer.start().await?;
33//!
34//!     // Process response event
35//!     let event = AnalyzerEvent::Response {
36//!         timestamp: Utc::now(),
37//!         request_id: "req-123".to_string(),
38//!         model: "gpt-4".to_string(),
39//!         completion_tokens: 100,
40//!         total_tokens: 200,
41//!         latency_ms: 500,
42//!         success: true,
43//!         error: None,
44//!         metadata: HashMap::new(),
45//!     };
46//!
47//!     analyzer.process_event(event).await?;
48//!
49//!     // Generate quality report
50//!     let report = analyzer.generate_report().await?;
51//!     println!("Quality Insights: {}", report.insights.len());
52//!
53//!     Ok(())
54//! }
55//! ```
56
57use async_trait::async_trait;
58use chrono::{DateTime, Duration as ChronoDuration, Utc};
59use serde::{Deserialize, Serialize};
60use std::collections::HashMap;
61use std::sync::Arc;
62use std::time::Instant;
63use tokio::sync::RwLock;
64use tracing::{debug, info, warn};
65use uuid::Uuid;
66
67use super::stats::{
68    calculate_percentiles, mean, percentage_change, std_dev, CircularBuffer,
69    ExponentialMovingAverage,
70};
71use super::traits::{Analyzer, AnalyzerConfig, AnalyzerError, AnalyzerResult, AnalyzerState};
72use super::types::{
73    Action, ActionType, Alert, AlertStatus, AnalysisReport, AnalyzerEvent, AnalyzerStats,
74    Confidence, Evidence, EvidenceType, FeedbackType, Impact, ImpactMetric, Insight, InsightCategory, Priority,
75    Recommendation, ReportSummary, RiskLevel, Severity,
76};
77
78/// Configuration for the Quality Analyzer
79#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct QualityAnalyzerConfig {
81    /// Base analyzer configuration
82    #[serde(flatten)]
83    pub base: AnalyzerConfig,
84
85    /// SLA target for P95 latency (ms)
86    pub sla_p95_ms: f64,
87
88    /// SLA target for P99 latency (ms)
89    pub sla_p99_ms: f64,
90
91    /// Minimum success rate percentage (0-100)
92    pub min_success_rate_pct: f64,
93
94    /// Minimum average user rating (0-5)
95    pub min_avg_rating: f64,
96
97    /// Error rate warning threshold (percentage)
98    pub error_rate_warning_pct: f64,
99
100    /// Error rate critical threshold (percentage)
101    pub error_rate_critical_pct: f64,
102
103    /// Minimum samples for quality calculations
104    pub min_samples_for_quality: usize,
105
106    /// Quality degradation threshold (percentage drop)
107    pub quality_degradation_threshold_pct: f64,
108
109    /// Feedback window size (number of ratings to track)
110    pub feedback_window_size: usize,
111}
112
113impl Default for QualityAnalyzerConfig {
114    fn default() -> Self {
115        Self {
116            base: AnalyzerConfig {
117                id: "quality-analyzer".to_string(),
118                enabled: true,
119                event_filter: vec![
120                    "response".to_string(),
121                    "feedback".to_string(),
122                    "request".to_string(),
123                ],
124                window_duration: std::time::Duration::from_secs(300), // 5 minutes
125                slide_interval: std::time::Duration::from_secs(60),   // 1 minute
126                sample_rate: 1.0,
127                max_buffer_size: 10000,
128                max_memory_bytes: 100 * 1024 * 1024, // 100 MB
129                analysis_interval: std::time::Duration::from_secs(60),
130                enable_tracing: true,
131            },
132            sla_p95_ms: 1000.0,
133            sla_p99_ms: 2000.0,
134            min_success_rate_pct: 99.5,
135            min_avg_rating: 4.0,
136            error_rate_warning_pct: 1.0,
137            error_rate_critical_pct: 5.0,
138            min_samples_for_quality: 10,
139            quality_degradation_threshold_pct: 10.0,
140            feedback_window_size: 100,
141        }
142    }
143}
144
145/// Quality metrics tracked by the analyzer
146#[derive(Debug, Clone)]
147struct QualityMetrics {
148    /// Latency samples for SLA calculation
149    latencies: CircularBuffer<f64>,
150
151    /// Success/failure tracking
152    total_requests: u64,
153    successful_requests: u64,
154    failed_requests: u64,
155
156    /// Error tracking by type
157    error_counts: HashMap<String, u64>,
158
159    /// User feedback ratings (1-5 scale)
160    ratings: CircularBuffer<f64>,
161
162    /// Feedback sentiment tracking
163    positive_feedback: u64,
164    negative_feedback: u64,
165    neutral_feedback: u64,
166
167    /// Response completeness tracking (0-1 scale)
168    completeness_scores: CircularBuffer<f64>,
169
170    /// Moving averages for trend detection
171    success_rate_ema: ExponentialMovingAverage,
172    rating_ema: ExponentialMovingAverage,
173
174    /// Baseline metrics for degradation detection
175    baseline_success_rate: Option<f64>,
176    baseline_avg_rating: Option<f64>,
177    baseline_p95_latency: Option<f64>,
178
179    /// SLA compliance tracking
180    sla_violations_p95: u64,
181    sla_violations_p99: u64,
182    sla_checks: u64,
183
184    /// Per-model quality tracking
185    model_metrics: HashMap<String, ModelQualityMetrics>,
186
187    /// Timestamp tracking
188    last_quality_check: DateTime<Utc>,
189    first_event_time: Option<DateTime<Utc>>,
190}
191
192#[derive(Debug, Clone)]
193struct ModelQualityMetrics {
194    total_requests: u64,
195    successful_requests: u64,
196    avg_rating: f64,
197    rating_count: u64,
198}
199
200impl ModelQualityMetrics {
201    fn new() -> Self {
202        Self {
203            total_requests: 0,
204            successful_requests: 0,
205            avg_rating: 0.0,
206            rating_count: 0,
207        }
208    }
209
210    fn success_rate(&self) -> f64 {
211        if self.total_requests == 0 {
212            return 100.0;
213        }
214        (self.successful_requests as f64 / self.total_requests as f64) * 100.0
215    }
216}
217
218impl QualityMetrics {
219    fn new(config: &QualityAnalyzerConfig) -> Self {
220        Self {
221            latencies: CircularBuffer::new(1000),
222            total_requests: 0,
223            successful_requests: 0,
224            failed_requests: 0,
225            error_counts: HashMap::new(),
226            ratings: CircularBuffer::new(config.feedback_window_size),
227            positive_feedback: 0,
228            negative_feedback: 0,
229            neutral_feedback: 0,
230            completeness_scores: CircularBuffer::new(1000),
231            success_rate_ema: ExponentialMovingAverage::new(0.1),
232            rating_ema: ExponentialMovingAverage::new(0.1),
233            baseline_success_rate: None,
234            baseline_avg_rating: None,
235            baseline_p95_latency: None,
236            sla_violations_p95: 0,
237            sla_violations_p99: 0,
238            sla_checks: 0,
239            model_metrics: HashMap::new(),
240            last_quality_check: Utc::now(),
241            first_event_time: None,
242        }
243    }
244
245    fn current_success_rate(&self) -> f64 {
246        if self.total_requests == 0 {
247            return 100.0;
248        }
249        (self.successful_requests as f64 / self.total_requests as f64) * 100.0
250    }
251
252    fn current_error_rate(&self) -> f64 {
253        100.0 - self.current_success_rate()
254    }
255
256    fn average_rating(&self) -> Option<f64> {
257        let values = self.ratings.to_vec();
258        if values.is_empty() {
259            return None;
260        }
261        Some(mean(&values))
262    }
263
264    fn average_completeness(&self) -> Option<f64> {
265        let values = self.completeness_scores.to_vec();
266        if values.is_empty() {
267            return None;
268        }
269        Some(mean(&values))
270    }
271
272    fn sla_compliance_rate(&self) -> f64 {
273        if self.sla_checks == 0 {
274            return 100.0;
275        }
276        let violations = self.sla_violations_p95 + self.sla_violations_p99;
277        ((self.sla_checks - violations) as f64 / self.sla_checks as f64) * 100.0
278    }
279}
280
281/// Quality Analyzer implementation
282pub struct QualityAnalyzer {
283    config: QualityAnalyzerConfig,
284    state: AnalyzerState,
285    metrics: Arc<RwLock<QualityMetrics>>,
286    stats: Arc<RwLock<AnalyzerStats>>,
287    start_time: Instant,
288    last_report_time: Arc<RwLock<Instant>>,
289}
290
291impl QualityAnalyzer {
292    /// Create a new Quality Analyzer with the given configuration
293    pub fn new(config: QualityAnalyzerConfig) -> Self {
294        let metrics = QualityMetrics::new(&config);
295
296        Self {
297            config,
298            state: AnalyzerState::Initialized,
299            metrics: Arc::new(RwLock::new(metrics)),
300            stats: Arc::new(RwLock::new(AnalyzerStats::default())),
301            start_time: Instant::now(),
302            last_report_time: Arc::new(RwLock::new(Instant::now())),
303        }
304    }
305
306    /// Create a Quality Analyzer with default configuration
307    pub fn with_defaults() -> Self {
308        Self::new(QualityAnalyzerConfig::default())
309    }
310
311    /// Process a response event
312    async fn process_response_event(
313        &mut self,
314        timestamp: DateTime<Utc>,
315        model: String,
316        latency_ms: u64,
317        success: bool,
318        error: Option<String>,
319    ) -> AnalyzerResult<()> {
320        let mut metrics = self.metrics.write().await;
321
322        // Initialize first event time
323        if metrics.first_event_time.is_none() {
324            metrics.first_event_time = Some(timestamp);
325        }
326
327        // Update request counts
328        metrics.total_requests += 1;
329        if success {
330            metrics.successful_requests += 1;
331        } else {
332            metrics.failed_requests += 1;
333        }
334
335        // Track error types
336        if let Some(err) = error {
337            *metrics.error_counts.entry(err).or_insert(0) += 1;
338        }
339
340        // Update latency tracking
341        let latency_f64 = latency_ms as f64;
342        metrics.latencies.push(latency_f64);
343
344        // Update success rate EMA
345        let success_val = if success { 1.0 } else { 0.0 };
346        metrics.success_rate_ema.update(success_val);
347
348        // Update per-model metrics
349        let model_metric = metrics
350            .model_metrics
351            .entry(model)
352            .or_insert_with(ModelQualityMetrics::new);
353        model_metric.total_requests += 1;
354        if success {
355            model_metric.successful_requests += 1;
356        }
357
358        // Check SLA compliance
359        if metrics.latencies.len() >= 10 {
360            let latencies_vec = metrics.latencies.to_vec();
361            let percentiles = calculate_percentiles(latencies_vec.clone(), &[0.95, 0.99]);
362
363            metrics.sla_checks += 1;
364
365            if let Some((_, p95)) = percentiles.first() {
366                if *p95 > self.config.sla_p95_ms {
367                    metrics.sla_violations_p95 += 1;
368                }
369            }
370
371            if let Some((_, p99)) = percentiles.get(1) {
372                if *p99 > self.config.sla_p99_ms {
373                    metrics.sla_violations_p99 += 1;
374                }
375            }
376        }
377
378        // Establish baseline after sufficient samples
379        if metrics.total_requests == 100 && metrics.baseline_success_rate.is_none() {
380            metrics.baseline_success_rate = Some(metrics.current_success_rate());
381
382            if metrics.latencies.len() >= 10 {
383                let latencies_vec = metrics.latencies.to_vec();
384                let percentiles = calculate_percentiles(latencies_vec, &[0.95]);
385                if let Some((_, p95)) = percentiles.first() {
386                    metrics.baseline_p95_latency = Some(*p95);
387                }
388            }
389        }
390
391        Ok(())
392    }
393
394    /// Process a feedback event
395    async fn process_feedback_event(
396        &mut self,
397        timestamp: DateTime<Utc>,
398        model: String,
399        rating: Option<f64>,
400        feedback_type: Option<String>,
401    ) -> AnalyzerResult<()> {
402        let mut metrics = self.metrics.write().await;
403
404        // Process rating
405        if let Some(r) = rating {
406            // Normalize rating to 0-5 scale
407            let normalized_rating = r.max(0.0).min(5.0);
408            metrics.ratings.push(normalized_rating);
409            metrics.rating_ema.update(normalized_rating);
410
411            // Update per-model rating
412            if let Some(model_metric) = metrics.model_metrics.get_mut(&model) {
413                let count = model_metric.rating_count as f64;
414                model_metric.avg_rating =
415                    (model_metric.avg_rating * count + normalized_rating) / (count + 1.0);
416                model_metric.rating_count += 1;
417            }
418
419            // Categorize feedback
420            if normalized_rating >= 4.0 {
421                metrics.positive_feedback += 1;
422            } else if normalized_rating >= 3.0 {
423                metrics.neutral_feedback += 1;
424            } else {
425                metrics.negative_feedback += 1;
426            }
427
428            // Establish baseline
429            if metrics.ratings.len() == self.config.feedback_window_size
430                && metrics.baseline_avg_rating.is_none()
431            {
432                metrics.baseline_avg_rating = metrics.average_rating();
433            }
434        }
435
436        Ok(())
437    }
438
439    /// Generate quality insights
440    async fn generate_insights(&self) -> Vec<Insight> {
441        let metrics = self.metrics.read().await;
442        let mut insights = Vec::new();
443
444        // Skip if insufficient data
445        if metrics.total_requests < self.config.min_samples_for_quality as u64 {
446            return insights;
447        }
448
449        // Check error rate
450        let error_rate = metrics.current_error_rate();
451        if error_rate >= self.config.error_rate_critical_pct {
452            insights.push(self.create_error_rate_insight(
453                error_rate,
454                Severity::Critical,
455                &metrics,
456            ));
457        } else if error_rate >= self.config.error_rate_warning_pct {
458            insights.push(self.create_error_rate_insight(
459                error_rate,
460                Severity::Warning,
461                &metrics,
462            ));
463        }
464
465        // Check success rate
466        let success_rate = metrics.current_success_rate();
467        if success_rate < self.config.min_success_rate_pct {
468            insights.push(self.create_low_success_rate_insight(success_rate, &metrics));
469        }
470
471        // Check SLA compliance
472        let sla_compliance = metrics.sla_compliance_rate();
473        if sla_compliance < 95.0 {
474            insights.push(self.create_sla_violation_insight(sla_compliance, &metrics));
475        }
476
477        // Check average rating
478        if let Some(avg_rating) = metrics.average_rating() {
479            if avg_rating < self.config.min_avg_rating {
480                insights.push(self.create_low_rating_insight(avg_rating, &metrics));
481            }
482        }
483
484        // Check for quality degradation
485        if let Some(baseline_sr) = metrics.baseline_success_rate {
486            let degradation = percentage_change(baseline_sr, success_rate);
487            if degradation.abs() >= self.config.quality_degradation_threshold_pct {
488                insights.push(self.create_degradation_insight(degradation, &metrics));
489            }
490        }
491
492        insights
493    }
494
495    /// Generate quality recommendations
496    async fn generate_recommendations(&self) -> Vec<Recommendation> {
497        let metrics = self.metrics.read().await;
498        let mut recommendations = Vec::new();
499
500        // Skip if insufficient data
501        if metrics.total_requests < self.config.min_samples_for_quality as u64 {
502            return recommendations;
503        }
504
505        let error_rate = metrics.current_error_rate();
506        let success_rate = metrics.current_success_rate();
507
508        // High error rate recommendations
509        if error_rate >= self.config.error_rate_warning_pct {
510            recommendations.push(Recommendation {
511                id: Uuid::new_v4().to_string(),
512                analyzer: "quality-analyzer".to_string(),
513                timestamp: Utc::now(),
514                priority: if error_rate >= self.config.error_rate_critical_pct {
515                    Priority::Urgent
516                } else {
517                    Priority::High
518                },
519                title: "Investigate and fix error sources".to_string(),
520                description: format!(
521                    "Error rate is {:.2}%. Review error logs and implement fixes for top error types: {}",
522                    error_rate,
523                    self.format_top_errors(&metrics)
524                ),
525                action: Action {
526                    action_type: ActionType::Investigate,
527                    instructions: "Review error logs and implement error handling improvements"
528                        .to_string(),
529                    parameters: Default::default(),
530                    estimated_effort: Some(4.0),
531                    risk_level: RiskLevel::Low,
532                },
533                expected_impact: Impact {
534                    quality: Some(ImpactMetric {
535                        name: "error_rate".to_string(),
536                        current: error_rate,
537                        expected: self.config.error_rate_warning_pct / 2.0,
538                        unit: "%".to_string(),
539                        improvement_pct: ((error_rate - self.config.error_rate_warning_pct / 2.0) / error_rate) * 100.0,
540                    }),
541                    performance: None,
542                    cost: None,
543                    description: format!("Reduce error rate from {:.2}% to {:.2}%", error_rate, self.config.error_rate_warning_pct / 2.0),
544                },
545                confidence: Confidence::new(0.8),
546                related_insights: vec![],
547                tags: vec!["quality".to_string(), "errors".to_string()],
548            });
549        }
550
551        // Low success rate recommendations
552        if success_rate < self.config.min_success_rate_pct {
553            recommendations.push(Recommendation {
554                id: Uuid::new_v4().to_string(),
555                analyzer: "quality-analyzer".to_string(),
556                timestamp: Utc::now(),
557                priority: Priority::High,
558                title: "Improve request success rate".to_string(),
559                description: format!(
560                    "Success rate is {:.2}%, below target of {:.2}%. Implement retry logic and better error handling.",
561                    success_rate, self.config.min_success_rate_pct
562                ),
563                action: Action {
564                    action_type: ActionType::ConfigChange,
565                    instructions: "Enable exponential backoff retry logic".to_string(),
566                    parameters: Default::default(),
567                    estimated_effort: Some(2.0),
568                    risk_level: RiskLevel::Low,
569                },
570                expected_impact: Impact {
571                    quality: Some(ImpactMetric {
572                        name: "success_rate".to_string(),
573                        current: success_rate,
574                        expected: self.config.min_success_rate_pct,
575                        unit: "%".to_string(),
576                        improvement_pct: ((self.config.min_success_rate_pct - success_rate) / success_rate) * 100.0,
577                    }),
578                    performance: None,
579                    cost: None,
580                    description: format!("Improve success rate from {:.2}% to {:.2}%", success_rate, self.config.min_success_rate_pct),
581                },
582                confidence: Confidence::new(0.8),
583                related_insights: vec![],
584                tags: vec!["quality".to_string(), "reliability".to_string()],
585            });
586        }
587
588        // SLA compliance recommendations
589        if metrics.sla_compliance_rate() < 95.0 {
590            recommendations.push(Recommendation {
591                id: Uuid::new_v4().to_string(),
592                analyzer: "quality-analyzer".to_string(),
593                timestamp: Utc::now(),
594                priority: Priority::High,
595                title: "Improve SLA compliance".to_string(),
596                description: format!(
597                    "SLA compliance is {:.2}%. Optimize performance to meet P95 ({:.0}ms) and P99 ({:.0}ms) targets.",
598                    metrics.sla_compliance_rate(),
599                    self.config.sla_p95_ms,
600                    self.config.sla_p99_ms
601                ),
602                action: Action {
603                    action_type: ActionType::Optimize,
604                    instructions: "Enable caching and optimize slow queries".to_string(),
605                    parameters: Default::default(),
606                    estimated_effort: Some(8.0),
607                    risk_level: RiskLevel::Medium,
608                },
609                expected_impact: Impact {
610                    performance: Some(ImpactMetric {
611                        name: "p95_latency".to_string(),
612                        current: metrics.sla_violations_p95 as f64,
613                        expected: 0.0,
614                        unit: "violations".to_string(),
615                        improvement_pct: 100.0,
616                    }),
617                    cost: None,
618                    quality: None,
619                    description: "Reduce P95 latency SLA violations to zero".to_string(),
620                },
621                confidence: Confidence::new(0.6),
622                related_insights: vec![],
623                tags: vec!["quality".to_string(), "sla".to_string()],
624            });
625        }
626
627        // Low rating recommendations
628        if let Some(avg_rating) = metrics.average_rating() {
629            if avg_rating < self.config.min_avg_rating {
630                recommendations.push(Recommendation {
631                    id: Uuid::new_v4().to_string(),
632                    analyzer: "quality-analyzer".to_string(),
633                    timestamp: Utc::now(),
634                    priority: Priority::Medium,
635                    title: "Improve response quality based on user feedback".to_string(),
636                    description: format!(
637                        "Average rating is {:.2}/5.0, below target of {:.2}. Review negative feedback and improve response quality.",
638                        avg_rating, self.config.min_avg_rating
639                    ),
640                    action: Action {
641                        action_type: ActionType::Optimize,
642                        instructions: "Analyze negative feedback patterns and adjust prompts"
643                            .to_string(),
644                        parameters: Default::default(),
645                        estimated_effort: Some(6.0),
646                        risk_level: RiskLevel::Medium,
647                    },
648                    expected_impact: Impact {
649                        quality: Some(ImpactMetric {
650                            name: "quality_score".to_string(),
651                            current: avg_rating,
652                            expected: self.config.min_avg_rating,
653                            unit: "rating".to_string(),
654                            improvement_pct: ((self.config.min_avg_rating - avg_rating) / avg_rating) * 100.0,
655                        }),
656                        performance: None,
657                        cost: None,
658                        description: format!("Improve quality score from {:.2} to {:.2}", avg_rating, self.config.min_avg_rating),
659                    },
660                    confidence: Confidence::new(0.6),
661                    related_insights: vec![],
662                    tags: vec!["quality".to_string(), "feedback".to_string()],
663                });
664            }
665        }
666
667        recommendations
668    }
669
670    fn create_error_rate_insight(
671        &self,
672        error_rate: f64,
673        severity: Severity,
674        metrics: &QualityMetrics,
675    ) -> Insight {
676        Insight {
677            id: Uuid::new_v4().to_string(),
678            analyzer: "quality-analyzer".to_string(),
679            timestamp: Utc::now(),
680            severity,
681            confidence: Confidence::new(0.8),
682            title: format!("High error rate: {:.2}%", error_rate),
683            description: format!(
684                "Error rate is {:.2}%, which exceeds the {} threshold. Total failures: {}. Top errors: {}",
685                error_rate,
686                if severity == Severity::Critical { "critical" } else { "warning" },
687                metrics.failed_requests,
688                self.format_top_errors(metrics)
689            ),
690            category: InsightCategory::Quality,
691            evidence: vec![
692                Evidence {
693                    evidence_type: EvidenceType::Statistical,
694                    description: format!("Error rate: {:.2}%", error_rate),
695                    data: serde_json::json!({"value": error_rate}),
696                    timestamp: Utc::now(),
697                },
698                Evidence {
699                    evidence_type: EvidenceType::Statistical,
700                    description: format!("Failed requests: {}", metrics.failed_requests),
701                    data: serde_json::json!({"value": metrics.failed_requests as f64}),
702                    timestamp: Utc::now(),
703                },
704            ],
705            metrics: {
706                let mut m = HashMap::new();
707                m.insert("error_rate_pct".to_string(), error_rate);
708                m.insert("failed_requests".to_string(), metrics.failed_requests as f64);
709                m
710            },
711            tags: vec![
712                "quality".to_string(),
713                "errors".to_string(),
714                "reliability".to_string(),
715            ],
716        }
717    }
718
719    fn create_low_success_rate_insight(
720        &self,
721        success_rate: f64,
722        metrics: &QualityMetrics,
723    ) -> Insight {
724        Insight {
725            id: Uuid::new_v4().to_string(),
726            analyzer: "quality-analyzer".to_string(),
727            timestamp: Utc::now(),
728            severity: Severity::Warning,
729            confidence: Confidence::new(0.8),
730            title: format!(
731                "Success rate {:.2}% below target {:.2}%",
732                success_rate, self.config.min_success_rate_pct
733            ),
734            description: format!(
735                "Current success rate is {:.2}%, which is below the target of {:.2}%. {} successful out of {} total requests.",
736                success_rate,
737                self.config.min_success_rate_pct,
738                metrics.successful_requests,
739                metrics.total_requests
740            ),
741            category: InsightCategory::Quality,
742            evidence: vec![Evidence {
743                evidence_type: EvidenceType::Statistical,
744                description: format!("Success rate: {:.2}%", success_rate),
745                data: serde_json::json!({"value": success_rate}),
746                timestamp: Utc::now(),
747            }],
748            metrics: {
749                let mut m = HashMap::new();
750                m.insert("success_rate_pct".to_string(), success_rate);
751                m.insert(
752                    "successful_requests".to_string(),
753                    metrics.successful_requests as f64,
754                );
755                m.insert("total_requests".to_string(), metrics.total_requests as f64);
756                m
757            },
758            tags: vec!["quality".to_string(), "success-rate".to_string()],
759        }
760    }
761
762    fn create_sla_violation_insight(
763        &self,
764        sla_compliance: f64,
765        metrics: &QualityMetrics,
766    ) -> Insight {
767        Insight {
768            id: Uuid::new_v4().to_string(),
769            analyzer: "quality-analyzer".to_string(),
770            timestamp: Utc::now(),
771            severity: if sla_compliance < 90.0 {
772                Severity::Critical
773            } else {
774                Severity::Warning
775            },
776            confidence: Confidence::new(0.8),
777            title: format!("SLA compliance at {:.1}%", sla_compliance),
778            description: format!(
779                "SLA compliance is {:.1}%, with {} P95 violations and {} P99 violations. Targets: P95 < {:.0}ms, P99 < {:.0}ms",
780                sla_compliance,
781                metrics.sla_violations_p95,
782                metrics.sla_violations_p99,
783                self.config.sla_p95_ms,
784                self.config.sla_p99_ms
785            ),
786            category: InsightCategory::Quality,
787            evidence: vec![
788                Evidence {
789                    evidence_type: EvidenceType::Statistical,
790                    description: format!("SLA compliance: {:.1}%", sla_compliance),
791                    data: serde_json::json!({"value": sla_compliance}),
792                    timestamp: Utc::now(),
793                },
794                Evidence {
795                    evidence_type: EvidenceType::Statistical,
796                    description: format!("P95 violations: {}", metrics.sla_violations_p95),
797                    data: serde_json::json!({"value": metrics.sla_violations_p95 as f64}),
798                    timestamp: Utc::now(),
799                },
800            ],
801            metrics: {
802                let mut m = HashMap::new();
803                m.insert("sla_compliance_pct".to_string(), sla_compliance);
804                m.insert(
805                    "p95_violations".to_string(),
806                    metrics.sla_violations_p95 as f64,
807                );
808                m.insert(
809                    "p99_violations".to_string(),
810                    metrics.sla_violations_p99 as f64,
811                );
812                m
813            },
814            tags: vec!["quality".to_string(), "sla".to_string()],
815        }
816    }
817
818    fn create_low_rating_insight(&self, avg_rating: f64, metrics: &QualityMetrics) -> Insight {
819        let total_feedback =
820            metrics.positive_feedback + metrics.negative_feedback + metrics.neutral_feedback;
821
822        Insight {
823            id: Uuid::new_v4().to_string(),
824            analyzer: "quality-analyzer".to_string(),
825            timestamp: Utc::now(),
826            severity: if avg_rating < 3.0 {
827                Severity::Error
828            } else {
829                Severity::Warning
830            },
831            confidence: Confidence::new(0.6),
832            title: format!(
833                "Average rating {:.2}/5.0 below target {:.2}/5.0",
834                avg_rating, self.config.min_avg_rating
835            ),
836            description: format!(
837                "Average user rating is {:.2}/5.0, below target of {:.2}/5.0. {} positive, {} neutral, {} negative (total: {})",
838                avg_rating,
839                self.config.min_avg_rating,
840                metrics.positive_feedback,
841                metrics.neutral_feedback,
842                metrics.negative_feedback,
843                total_feedback
844            ),
845            category: InsightCategory::Quality,
846            evidence: vec![Evidence {
847                evidence_type: EvidenceType::Statistical,
848                description: format!("Average rating: {:.2}/5.0", avg_rating),
849                data: serde_json::json!({"value": avg_rating}),
850                timestamp: Utc::now(),
851            }],
852            metrics: {
853                let mut m = HashMap::new();
854                m.insert("avg_rating".to_string(), avg_rating);
855                m.insert("positive_feedback".to_string(), metrics.positive_feedback as f64);
856                m.insert("negative_feedback".to_string(), metrics.negative_feedback as f64);
857                m.insert("neutral_feedback".to_string(), metrics.neutral_feedback as f64);
858                m
859            },
860            tags: vec!["quality".to_string(), "feedback".to_string()],
861        }
862    }
863
864    fn create_degradation_insight(&self, degradation: f64, metrics: &QualityMetrics) -> Insight {
865        Insight {
866            id: Uuid::new_v4().to_string(),
867            analyzer: "quality-analyzer".to_string(),
868            timestamp: Utc::now(),
869            severity: Severity::Warning,
870            confidence: Confidence::new(0.6),
871            title: format!("Quality degradation detected: {:.1}%", degradation.abs()),
872            description: format!(
873                "Success rate has degraded by {:.1}% from baseline. Current: {:.2}%, Baseline: {:.2}%",
874                degradation.abs(),
875                metrics.current_success_rate(),
876                metrics.baseline_success_rate.unwrap_or(0.0)
877            ),
878            category: InsightCategory::Quality,
879            evidence: vec![Evidence {
880                evidence_type: EvidenceType::TimeSeries,
881                description: format!("Quality degradation: {:.1}%", degradation.abs()),
882                data: serde_json::json!({"value": degradation}),
883                timestamp: Utc::now(),
884            }],
885            metrics: {
886                let mut m = HashMap::new();
887                m.insert("degradation_pct".to_string(), degradation);
888                m.insert(
889                    "current_success_rate".to_string(),
890                    metrics.current_success_rate(),
891                );
892                m.insert(
893                    "baseline_success_rate".to_string(),
894                    metrics.baseline_success_rate.unwrap_or(0.0),
895                );
896                m
897            },
898            tags: vec!["quality".to_string(), "degradation".to_string()],
899        }
900    }
901
902    fn format_top_errors(&self, metrics: &QualityMetrics) -> String {
903        let mut errors: Vec<_> = metrics.error_counts.iter().collect();
904        errors.sort_by(|a, b| b.1.cmp(a.1));
905
906        errors
907            .iter()
908            .take(3)
909            .map(|(err, count)| format!("{} ({})", err, count))
910            .collect::<Vec<_>>()
911            .join(", ")
912    }
913}
914
915#[async_trait]
916impl Analyzer for QualityAnalyzer {
917    fn name(&self) -> &str {
918        "quality-analyzer"
919    }
920
921    fn config(&self) -> &AnalyzerConfig {
922        &self.config.base
923    }
924
925    fn state(&self) -> AnalyzerState {
926        self.state.clone()
927    }
928
929    async fn start(&mut self) -> AnalyzerResult<()> {
930        if self.state == AnalyzerState::Running {
931            return Err(AnalyzerError::InvalidStateTransition {
932                from: self.state.as_str().to_string(),
933                to: AnalyzerState::Starting.as_str().to_string(),
934            });
935        }
936
937        info!("Starting Quality Analyzer");
938        self.state = AnalyzerState::Starting;
939
940        // Perform any initialization here
941        self.state = AnalyzerState::Running;
942        info!("Quality Analyzer started successfully");
943
944        Ok(())
945    }
946
947    async fn stop(&mut self) -> AnalyzerResult<()> {
948        if self.state != AnalyzerState::Running {
949            return Err(AnalyzerError::InvalidStateTransition {
950                from: self.state.as_str().to_string(),
951                to: AnalyzerState::Draining.as_str().to_string(),
952            });
953        }
954
955        info!("Stopping Quality Analyzer");
956        self.state = AnalyzerState::Draining;
957
958        // Perform cleanup
959        self.state = AnalyzerState::Stopped;
960        info!("Quality Analyzer stopped");
961
962        Ok(())
963    }
964
965    async fn process_event(&mut self, event: AnalyzerEvent) -> AnalyzerResult<()> {
966        if self.state != AnalyzerState::Running {
967            return Err(AnalyzerError::NotRunning);
968        }
969
970        // Update stats
971        let mut stats = self.stats.write().await;
972        stats.events_processed += 1;
973        // Note: last_event_time field removed from AnalyzerStats
974        drop(stats);
975
976        // Process based on event type
977        match event {
978            AnalyzerEvent::Response {
979                timestamp,
980                model,
981                latency_ms,
982                success,
983                error,
984                ..
985            } => {
986                self.process_response_event(timestamp, model, latency_ms, success, error)
987                    .await?;
988            }
989            AnalyzerEvent::Feedback {
990                timestamp,
991                request_id,
992                rating,
993                feedback_type,
994                metadata,
995                ..
996            } => {
997                // Extract model from metadata or use request_id as fallback
998                let model = metadata
999                    .get("model")
1000                    .unwrap_or(&request_id)
1001                    .to_string();
1002
1003                let feedback_str = match feedback_type {
1004                    FeedbackType::Positive => "positive",
1005                    FeedbackType::Negative => "negative",
1006                    FeedbackType::Rating => "rating",
1007                    FeedbackType::Quality => "quality",
1008                    FeedbackType::Custom => "custom",
1009                };
1010                self.process_feedback_event(timestamp, model, Some(rating), Some(feedback_str.to_string()))
1011                    .await?;
1012            }
1013            _ => {
1014                debug!("Ignoring event type not relevant to quality analysis");
1015            }
1016        }
1017
1018        Ok(())
1019    }
1020
1021    async fn generate_report(&self) -> AnalyzerResult<AnalysisReport> {
1022        let metrics = self.metrics.read().await;
1023        let stats = self.stats.read().await;
1024
1025        let now = Utc::now();
1026        let period_start = metrics
1027            .first_event_time
1028            .unwrap_or_else(|| now - ChronoDuration::minutes(5));
1029
1030        let insights = self.generate_insights().await;
1031        let recommendations = self.generate_recommendations().await;
1032
1033        // Calculate report metrics
1034        let mut report_metrics = HashMap::new();
1035        report_metrics.insert(
1036            "success_rate_pct".to_string(),
1037            metrics.current_success_rate(),
1038        );
1039        report_metrics.insert("error_rate_pct".to_string(), metrics.current_error_rate());
1040        report_metrics.insert("total_requests".to_string(), metrics.total_requests as f64);
1041        report_metrics.insert(
1042            "successful_requests".to_string(),
1043            metrics.successful_requests as f64,
1044        );
1045        report_metrics.insert(
1046            "failed_requests".to_string(),
1047            metrics.failed_requests as f64,
1048        );
1049
1050        if let Some(avg_rating) = metrics.average_rating() {
1051            report_metrics.insert("avg_rating".to_string(), avg_rating);
1052        }
1053
1054        report_metrics.insert(
1055            "sla_compliance_pct".to_string(),
1056            metrics.sla_compliance_rate(),
1057        );
1058        report_metrics.insert(
1059            "positive_feedback".to_string(),
1060            metrics.positive_feedback as f64,
1061        );
1062        report_metrics.insert(
1063            "negative_feedback".to_string(),
1064            metrics.negative_feedback as f64,
1065        );
1066
1067        let critical_issues_count = insights
1068            .iter()
1069            .filter(|i| i.severity == Severity::Critical)
1070            .count();
1071        let warnings_count = insights
1072            .iter()
1073            .filter(|i| i.severity == Severity::Warning)
1074            .count();
1075
1076        let summary = ReportSummary {
1077            events_processed: stats.events_processed,
1078            events_per_second: 0.0,  // TODO: Calculate properly
1079            insights_count: insights.len() as u64,
1080            recommendations_count: recommendations.len() as u64,
1081            alerts_count: 0,
1082            analysis_duration_ms: 0,  // TODO: Calculate properly
1083            highlights: vec![
1084                format!("Critical issues: {}", critical_issues_count),
1085                format!("Warnings: {}", warnings_count),
1086            ],
1087        };
1088
1089        Ok(AnalysisReport {
1090            analyzer: self.name().to_string(),
1091            timestamp: now,
1092            period_start,
1093            period_end: now,
1094            summary,
1095            insights,
1096            recommendations,
1097            alerts: vec![],
1098            metrics: report_metrics,
1099        })
1100    }
1101
1102    fn get_stats(&self) -> AnalyzerStats {
1103        // This is synchronous, so we can't await. Return a default for now.
1104        // In a real implementation, we'd use try_read() or similar
1105        AnalyzerStats {
1106            analyzer: "QualityAnalyzer".to_string(),
1107            events_processed: 0,
1108            events_dropped: 0,
1109            insights_generated: 0,
1110            recommendations_generated: 0,
1111            alerts_triggered: 0,  // was: alerts_generated
1112            analysis_time_ms: 0,
1113            avg_analysis_time_us: 0.0,  // was: avg_processing_time_ms
1114            memory_usage_bytes: 0,
1115            error_count: 0,  // was: errors_encountered
1116            last_error: None,
1117            uptime_seconds: 0,
1118        }
1119    }
1120
1121    async fn reset(&mut self) -> AnalyzerResult<()> {
1122        info!("Resetting Quality Analyzer");
1123
1124        let mut metrics = self.metrics.write().await;
1125        *metrics = QualityMetrics::new(&self.config);
1126
1127        let mut stats = self.stats.write().await;
1128        *stats = AnalyzerStats::default();
1129
1130        Ok(())
1131    }
1132
1133    async fn health_check(&self) -> AnalyzerResult<()> {
1134        if self.state != AnalyzerState::Running {
1135            return Err(AnalyzerError::NotRunning);
1136        }
1137
1138        let metrics = self.metrics.read().await;
1139
1140        // Check if we're receiving events
1141        if let Some(last_event) = metrics.first_event_time {
1142            let elapsed = Utc::now() - last_event;
1143            if elapsed > ChronoDuration::minutes(10) {
1144                warn!("No events received in the last 10 minutes");
1145            }
1146        }
1147
1148        Ok(())
1149    }
1150}
1151
1152#[cfg(test)]
1153mod tests {
1154    use super::*;
1155    use std::collections::HashMap;
1156
1157    #[tokio::test]
1158    async fn test_quality_analyzer_lifecycle() {
1159        let mut analyzer = QualityAnalyzer::with_defaults();
1160
1161        assert_eq!(analyzer.state(), AnalyzerState::Initialized);
1162
1163        analyzer.start().await.unwrap();
1164        assert_eq!(analyzer.state(), AnalyzerState::Running);
1165
1166        analyzer.stop().await.unwrap();
1167        assert_eq!(analyzer.state(), AnalyzerState::Stopped);
1168    }
1169
1170    #[tokio::test]
1171    async fn test_response_event_processing() {
1172        let mut analyzer = QualityAnalyzer::with_defaults();
1173        analyzer.start().await.unwrap();
1174
1175        let event = AnalyzerEvent::Response {
1176            timestamp: Utc::now(),
1177            request_id: "req-1".to_string(),
1178            model: "gpt-4".to_string(),
1179            completion_tokens: 100,
1180            total_tokens: 200,
1181            latency_ms: 500,
1182            success: true,
1183            error: None,
1184            metadata: HashMap::new(),
1185        };
1186
1187        analyzer.process_event(event).await.unwrap();
1188
1189        let metrics = analyzer.metrics.read().await;
1190        assert_eq!(metrics.total_requests, 1);
1191        assert_eq!(metrics.successful_requests, 1);
1192        assert_eq!(metrics.failed_requests, 0);
1193    }
1194
1195    #[tokio::test]
1196    async fn test_error_tracking() {
1197        let mut analyzer = QualityAnalyzer::with_defaults();
1198        analyzer.start().await.unwrap();
1199
1200        // Process 5 successful and 2 failed requests
1201        for i in 0..7 {
1202            let success = i < 5;
1203            let error = if success {
1204                None
1205            } else {
1206                Some("timeout".to_string())
1207            };
1208
1209            let event = AnalyzerEvent::Response {
1210                timestamp: Utc::now(),
1211                request_id: format!("req-{}", i),
1212                model: "gpt-4".to_string(),
1213                completion_tokens: 100,
1214                total_tokens: 200,
1215                latency_ms: 500,
1216                success,
1217                error,
1218                metadata: HashMap::new(),
1219            };
1220
1221            analyzer.process_event(event).await.unwrap();
1222        }
1223
1224        let metrics = analyzer.metrics.read().await;
1225        assert_eq!(metrics.total_requests, 7);
1226        assert_eq!(metrics.successful_requests, 5);
1227        assert_eq!(metrics.failed_requests, 2);
1228        assert!((metrics.current_success_rate() - 71.43).abs() < 0.1);
1229    }
1230
1231    #[tokio::test]
1232    async fn test_feedback_processing() {
1233        let mut analyzer = QualityAnalyzer::with_defaults();
1234        analyzer.start().await.unwrap();
1235
1236        // Process some ratings
1237        for rating in vec![5.0, 4.5, 4.0, 3.5, 3.0] {
1238            let event = AnalyzerEvent::Feedback {
1239                timestamp: Utc::now(),
1240                request_id: "req-1".to_string(),
1241                rating: Some(rating),
1242                feedback_type: Some("positive".to_string()),
1243                comment: None,
1244                metadata: {
1245                    let mut m = HashMap::new();
1246                    m.insert("model".to_string(), serde_json::json!("gpt-4"));
1247                    m
1248                },
1249            };
1250
1251            analyzer.process_event(event).await.unwrap();
1252        }
1253
1254        let metrics = analyzer.metrics.read().await;
1255        let avg_rating = metrics.average_rating().unwrap();
1256        assert!((avg_rating - 4.0).abs() < 0.1);
1257    }
1258
1259    #[tokio::test]
1260    async fn test_high_error_rate_insight() {
1261        let config = QualityAnalyzerConfig {
1262            error_rate_warning_pct: 5.0,
1263            min_samples_for_quality: 10,
1264            ..Default::default()
1265        };
1266
1267        let mut analyzer = QualityAnalyzer::new(config);
1268        analyzer.start().await.unwrap();
1269
1270        // Process 10 requests with 15% error rate
1271        for i in 0..10 {
1272            let success = i < 8; // 8 success, 2 failures = 20% error rate
1273
1274            let event = AnalyzerEvent::Response {
1275                timestamp: Utc::now(),
1276                request_id: format!("req-{}", i),
1277                model: "gpt-4".to_string(),
1278                completion_tokens: 100,
1279                total_tokens: 200,
1280                latency_ms: 500,
1281                success,
1282                error: if success {
1283                    None
1284                } else {
1285                    Some("error".to_string())
1286                },
1287                metadata: HashMap::new(),
1288            };
1289
1290            analyzer.process_event(event).await.unwrap();
1291        }
1292
1293        let report = analyzer.generate_report().await.unwrap();
1294        assert!(!report.insights.is_empty());
1295        assert!(report
1296            .insights
1297            .iter()
1298            .any(|i| i.title.contains("error rate")));
1299    }
1300
1301    #[tokio::test]
1302    async fn test_low_rating_insight() {
1303        let config = QualityAnalyzerConfig {
1304            min_avg_rating: 4.0,
1305            min_samples_for_quality: 5,
1306            ..Default::default()
1307        };
1308
1309        let mut analyzer = QualityAnalyzer::new(config);
1310        analyzer.start().await.unwrap();
1311
1312        // Add some requests first to meet sample requirements
1313        for i in 0..10 {
1314            let event = AnalyzerEvent::Response {
1315                timestamp: Utc::now(),
1316                request_id: format!("req-{}", i),
1317                model: "gpt-4".to_string(),
1318                completion_tokens: 100,
1319                total_tokens: 200,
1320                latency_ms: 500,
1321                success: true,
1322                error: None,
1323                metadata: HashMap::new(),
1324            };
1325            analyzer.process_event(event).await.unwrap();
1326        }
1327
1328        // Process low ratings
1329        for rating in vec![2.0, 2.5, 3.0, 2.5, 3.0] {
1330            let event = AnalyzerEvent::Feedback {
1331                timestamp: Utc::now(),
1332                request_id: "req-1".to_string(),
1333                rating: Some(rating),
1334                feedback_type: Some("negative".to_string()),
1335                comment: None,
1336                metadata: {
1337                    let mut m = HashMap::new();
1338                    m.insert("model".to_string(), serde_json::json!("gpt-4"));
1339                    m
1340                },
1341            };
1342
1343            analyzer.process_event(event).await.unwrap();
1344        }
1345
1346        let report = analyzer.generate_report().await.unwrap();
1347        assert!(report.insights.iter().any(|i| i.title.contains("rating")));
1348    }
1349
1350    #[tokio::test]
1351    async fn test_sla_compliance_tracking() {
1352        let config = QualityAnalyzerConfig {
1353            sla_p95_ms: 500.0,
1354            sla_p99_ms: 1000.0,
1355            min_samples_for_quality: 10,
1356            ..Default::default()
1357        };
1358
1359        let mut analyzer = QualityAnalyzer::new(config);
1360        analyzer.start().await.unwrap();
1361
1362        // Process requests with varying latencies
1363        for latency in vec![100, 200, 300, 400, 600, 800, 1200, 300, 400, 500, 600, 700] {
1364            let event = AnalyzerEvent::Response {
1365                timestamp: Utc::now(),
1366                request_id: "req-1".to_string(),
1367                model: "gpt-4".to_string(),
1368                completion_tokens: 100,
1369                total_tokens: 200,
1370                latency_ms: latency,
1371                success: true,
1372                error: None,
1373                metadata: HashMap::new(),
1374            };
1375
1376            analyzer.process_event(event).await.unwrap();
1377        }
1378
1379        let metrics = analyzer.metrics.read().await;
1380        assert!(metrics.sla_checks > 0);
1381    }
1382
1383    #[tokio::test]
1384    async fn test_recommendation_generation() {
1385        let config = QualityAnalyzerConfig {
1386            error_rate_warning_pct: 5.0,
1387            min_samples_for_quality: 10,
1388            ..Default::default()
1389        };
1390
1391        let mut analyzer = QualityAnalyzer::new(config);
1392        analyzer.start().await.unwrap();
1393
1394        // Generate high error rate
1395        for i in 0..10 {
1396            let success = i < 7; // 30% error rate
1397
1398            let event = AnalyzerEvent::Response {
1399                timestamp: Utc::now(),
1400                request_id: format!("req-{}", i),
1401                model: "gpt-4".to_string(),
1402                completion_tokens: 100,
1403                total_tokens: 200,
1404                latency_ms: 500,
1405                success,
1406                error: if success {
1407                    None
1408                } else {
1409                    Some("error".to_string())
1410                },
1411                metadata: HashMap::new(),
1412            };
1413
1414            analyzer.process_event(event).await.unwrap();
1415        }
1416
1417        let report = analyzer.generate_report().await.unwrap();
1418        assert!(!report.recommendations.is_empty());
1419        assert!(report
1420            .recommendations
1421            .iter()
1422            .any(|r| r.title.contains("error") || r.title.contains("success")));
1423    }
1424
1425    #[tokio::test]
1426    async fn test_reset() {
1427        let mut analyzer = QualityAnalyzer::with_defaults();
1428        analyzer.start().await.unwrap();
1429
1430        // Process some events
1431        for _ in 0..5 {
1432            let event = AnalyzerEvent::Response {
1433                timestamp: Utc::now(),
1434                request_id: "req-1".to_string(),
1435                model: "gpt-4".to_string(),
1436                completion_tokens: 100,
1437                total_tokens: 200,
1438                latency_ms: 500,
1439                success: true,
1440                error: None,
1441                metadata: HashMap::new(),
1442            };
1443
1444            analyzer.process_event(event).await.unwrap();
1445        }
1446
1447        let metrics_before = analyzer.metrics.read().await;
1448        assert_eq!(metrics_before.total_requests, 5);
1449        drop(metrics_before);
1450
1451        // Reset
1452        analyzer.reset().await.unwrap();
1453
1454        let metrics_after = analyzer.metrics.read().await;
1455        assert_eq!(metrics_after.total_requests, 0);
1456    }
1457
1458    #[tokio::test]
1459    async fn test_health_check() {
1460        let mut analyzer = QualityAnalyzer::with_defaults();
1461
1462        // Health check should fail when not running
1463        assert!(analyzer.health_check().await.is_err());
1464
1465        // Start and health check should succeed
1466        analyzer.start().await.unwrap();
1467        assert!(analyzer.health_check().await.is_ok());
1468    }
1469}