1use anyhow::Result;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::sync::Arc;
11use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12use tokio::sync::{Mutex as AsyncMutex, RwLock as AsyncRwLock};
13use tracing::info;
14
15use crate::performance::OperationMetrics;
16use crate::system_monitor;
17
18#[derive(Debug, Clone)]
20pub struct ObservabilityConfig {
21 pub enable_opentelemetry: bool,
22 pub enable_custom_metrics: bool,
23 pub enable_distributed_tracing: bool,
24 pub enable_real_time_monitoring: bool,
25 pub enable_alerting: bool,
26 pub enable_log_aggregation: bool,
27 pub metrics_collection_interval: Duration,
28 pub trace_sampling_rate: f64,
29 pub alert_thresholds: AlertThresholds,
30 pub retention_period: Duration,
31 pub export_endpoint: Option<String>,
32 pub service_name: String,
33 pub service_version: String,
34 pub environment: String,
35}
36
37impl Default for ObservabilityConfig {
38 fn default() -> Self {
39 Self {
40 enable_opentelemetry: true,
41 enable_custom_metrics: true,
42 enable_distributed_tracing: true,
43 enable_real_time_monitoring: true,
44 enable_alerting: true,
45 enable_log_aggregation: true,
46 metrics_collection_interval: Duration::from_secs(10),
47 trace_sampling_rate: 1.0,
48 alert_thresholds: AlertThresholds::default(),
49 retention_period: Duration::from_secs(86400), export_endpoint: None,
51 service_name: "oxirs-gql".to_string(),
52 service_version: "0.1.0".to_string(),
53 environment: "development".to_string(),
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct AlertThresholds {
61 pub max_response_time_ms: u64,
62 pub max_error_rate: f64,
63 pub max_memory_usage_mb: u64,
64 pub max_cpu_usage_percent: f64,
65 pub min_cache_hit_ratio: f64,
66 pub max_concurrent_requests: usize,
67}
68
69impl Default for AlertThresholds {
70 fn default() -> Self {
71 Self {
72 max_response_time_ms: 5000,
73 max_error_rate: 0.05, max_memory_usage_mb: 1024,
75 max_cpu_usage_percent: 80.0,
76 min_cache_hit_ratio: 0.8,
77 max_concurrent_requests: 1000,
78 }
79 }
80}
81
82pub struct ObservabilitySystem {
84 config: ObservabilityConfig,
85 metrics_collector: Arc<AsyncRwLock<MetricsCollector>>,
86 trace_collector: Arc<AsyncRwLock<TraceCollector>>,
87 alert_manager: Arc<AsyncMutex<AlertManager>>,
88 real_time_monitor: Arc<AsyncRwLock<RealTimeMonitor>>,
89}
90
91impl ObservabilitySystem {
92 pub fn new(config: ObservabilityConfig) -> Self {
93 Self {
94 metrics_collector: Arc::new(AsyncRwLock::new(MetricsCollector::new(&config))),
95 trace_collector: Arc::new(AsyncRwLock::new(TraceCollector::new(&config))),
96 alert_manager: Arc::new(AsyncMutex::new(AlertManager::new(&config))),
97 real_time_monitor: Arc::new(AsyncRwLock::new(RealTimeMonitor::new(&config))),
98 config,
99 }
100 }
101
102 pub async fn record_operation(&self, metrics: &OperationMetrics) -> Result<()> {
104 if self.config.enable_custom_metrics {
106 self.metrics_collector
107 .write()
108 .await
109 .record_operation(metrics)?;
110 }
111
112 if self.config.enable_distributed_tracing {
114 self.trace_collector
115 .write()
116 .await
117 .create_operation_span(metrics)?;
118 }
119
120 if self.config.enable_real_time_monitoring {
122 self.real_time_monitor
123 .write()
124 .await
125 .update_metrics(metrics)
126 .await?;
127 }
128
129 if self.config.enable_alerting {
131 self.alert_manager.lock().await.check_alerts(metrics)?;
132 }
133
134 Ok(())
135 }
136
137 pub async fn get_dashboard(&self) -> Result<ObservabilityDashboard> {
139 let metrics = if self.config.enable_custom_metrics {
140 Some(self.metrics_collector.read().await.get_summary())
141 } else {
142 None
143 };
144
145 let traces = if self.config.enable_distributed_tracing {
146 Some(self.trace_collector.read().await.get_recent_traces(100))
147 } else {
148 None
149 };
150
151 let alerts = if self.config.enable_alerting {
152 Some(self.alert_manager.lock().await.get_active_alerts())
153 } else {
154 None
155 };
156
157 let real_time_data = if self.config.enable_real_time_monitoring {
158 Some(self.real_time_monitor.read().await.get_current_state())
159 } else {
160 None
161 };
162
163 Ok(ObservabilityDashboard {
164 service_info: ServiceInfo {
165 name: self.config.service_name.clone(),
166 version: self.config.service_version.clone(),
167 environment: self.config.environment.clone(),
168 uptime: SystemTime::now()
169 .duration_since(UNIX_EPOCH)
170 .unwrap_or_default(),
171 },
172 metrics,
173 traces,
174 alerts,
175 real_time_data,
176 health_status: self.calculate_health_status().await,
177 })
178 }
179
180 async fn calculate_health_status(&self) -> HealthStatus {
182 let mut health_score = 100.0;
183 let mut issues = Vec::new();
184
185 if self.config.enable_alerting {
186 let active_alerts = self.alert_manager.lock().await.get_active_alerts();
187 if !active_alerts.is_empty() {
188 health_score -= active_alerts.len() as f64 * 10.0;
189 issues.push(format!("{} active alerts", active_alerts.len()));
190 }
191 }
192
193 if self.config.enable_real_time_monitoring {
194 let monitor = self.real_time_monitor.read().await;
195 let current_state = monitor.get_current_state();
196
197 if current_state.error_rate > self.config.alert_thresholds.max_error_rate {
198 health_score -= 20.0;
199 issues.push("High error rate detected".to_string());
200 }
201
202 if current_state.avg_response_time.as_millis()
203 > self.config.alert_thresholds.max_response_time_ms as u128
204 {
205 health_score -= 15.0;
206 issues.push("High response time detected".to_string());
207 }
208 }
209
210 let status = if health_score >= 90.0 {
211 HealthStatusLevel::Healthy
212 } else if health_score >= 70.0 {
213 HealthStatusLevel::Warning
214 } else {
215 HealthStatusLevel::Critical
216 };
217
218 HealthStatus {
219 status,
220 score: health_score.max(0.0),
221 issues,
222 last_check: SystemTime::now(),
223 }
224 }
225
226 pub async fn export_metrics(&self) -> Result<()> {
228 if let Some(endpoint) = &self.config.export_endpoint {
229 let dashboard = self.get_dashboard().await?;
230 let serialized = serde_json::to_string(&dashboard)?;
231
232 info!(
234 "Exporting metrics to {}: {} bytes",
235 endpoint,
236 serialized.len()
237 );
238 }
239 Ok(())
240 }
241}
242
243pub struct MetricsCollector {
245 operation_metrics: VecDeque<OperationMetrics>,
246 aggregated_metrics: AggregatedMetrics,
247 custom_counters: HashMap<String, u64>,
248 custom_gauges: HashMap<String, f64>,
249 custom_histograms: HashMap<String, Histogram>,
250}
251
252impl MetricsCollector {
253 fn new(_config: &ObservabilityConfig) -> Self {
254 Self {
255 operation_metrics: VecDeque::new(),
256 aggregated_metrics: AggregatedMetrics::new(),
257 custom_counters: HashMap::new(),
258 custom_gauges: HashMap::new(),
259 custom_histograms: HashMap::new(),
260 }
261 }
262
263 fn record_operation(&mut self, metrics: &OperationMetrics) -> Result<()> {
264 self.operation_metrics.push_back(metrics.clone());
265 self.aggregated_metrics.update(metrics);
266
267 let counter_key = format!("operations.{:?}", metrics.operation_type);
269 *self.custom_counters.entry(counter_key).or_insert(0) += 1;
270
271 let histogram_key = "execution_time_ms".to_string();
273 let histogram = self
274 .custom_histograms
275 .entry(histogram_key)
276 .or_insert_with(Histogram::new);
277 histogram.record(metrics.execution_time.as_millis() as f64);
278
279 Ok(())
280 }
281
282 fn get_summary(&self) -> MetricsSummary {
283 MetricsSummary {
284 total_operations: self.operation_metrics.len(),
285 aggregated: self.aggregated_metrics.clone(),
286 counters: self.custom_counters.clone(),
287 gauges: self.custom_gauges.clone(),
288 histograms: self
289 .custom_histograms
290 .iter()
291 .map(|(k, v)| (k.clone(), v.summary()))
292 .collect(),
293 }
294 }
295}
296
297#[allow(dead_code)]
299pub struct TraceCollector {
300 active_spans: HashMap<String, TraceSpan>,
301 completed_traces: VecDeque<DistributedTrace>,
302 trace_id_counter: u64,
303}
304
305impl TraceCollector {
306 fn new(_config: &ObservabilityConfig) -> Self {
307 Self {
308 active_spans: HashMap::new(),
309 completed_traces: VecDeque::new(),
310 trace_id_counter: 0,
311 }
312 }
313
314 fn create_operation_span(&mut self, metrics: &OperationMetrics) -> Result<()> {
315 self.trace_id_counter += 1;
316 let trace_id = format!("trace_{}", self.trace_id_counter);
317
318 let span = TraceSpan {
319 trace_id: trace_id.clone(),
320 span_id: format!("span_{}", self.trace_id_counter),
321 parent_span_id: None,
322 operation_name: metrics
323 .operation_name
324 .clone()
325 .unwrap_or_else(|| "anonymous".to_string()),
326 start_time: metrics.timestamp,
327 duration: Some(metrics.execution_time),
328 tags: vec![
329 (
330 "operation.type".to_string(),
331 format!("{:?}", metrics.operation_type),
332 ),
333 (
334 "query.complexity".to_string(),
335 metrics.complexity_score.to_string(),
336 ),
337 ("query.depth".to_string(), metrics.depth.to_string()),
338 ("cache.hit".to_string(), metrics.cache_hit.to_string()),
339 ],
340 logs: Vec::new(),
341 status: if metrics.error_count > 0 {
342 SpanStatus::Error
343 } else {
344 SpanStatus::Ok
345 },
346 };
347
348 let trace = DistributedTrace {
349 trace_id: trace_id.clone(),
350 spans: vec![span],
351 total_duration: metrics.execution_time,
352 service_count: 1,
353 error_count: metrics.error_count,
354 };
355
356 self.completed_traces.push_back(trace);
357
358 while self.completed_traces.len() > 1000 {
360 self.completed_traces.pop_front();
361 }
362
363 Ok(())
364 }
365
366 fn get_recent_traces(&self, limit: usize) -> Vec<DistributedTrace> {
367 self.completed_traces
368 .iter()
369 .rev()
370 .take(limit)
371 .cloned()
372 .collect()
373 }
374}
375
376pub struct AlertManager {
378 active_alerts: Vec<Alert>,
379 alert_history: VecDeque<Alert>,
380 thresholds: AlertThresholds,
381}
382
383impl AlertManager {
384 fn new(config: &ObservabilityConfig) -> Self {
385 Self {
386 active_alerts: Vec::new(),
387 alert_history: VecDeque::new(),
388 thresholds: config.alert_thresholds.clone(),
389 }
390 }
391
392 fn check_alerts(&mut self, metrics: &OperationMetrics) -> Result<()> {
393 let mut new_alerts = Vec::new();
394
395 if metrics.execution_time.as_millis() > self.thresholds.max_response_time_ms as u128 {
397 new_alerts.push(Alert {
398 id: format!(
399 "response_time_{}",
400 SystemTime::now()
401 .duration_since(UNIX_EPOCH)
402 .expect("SystemTime should be after UNIX_EPOCH")
403 .as_millis()
404 ),
405 alert_type: AlertType::HighResponseTime,
406 severity: AlertSeverity::Warning,
407 message: format!(
408 "Operation took {}ms (threshold: {}ms)",
409 metrics.execution_time.as_millis(),
410 self.thresholds.max_response_time_ms
411 ),
412 timestamp: SystemTime::now(),
413 resolved: false,
414 metadata: HashMap::new(),
415 });
416 }
417
418 if metrics.error_count > 0 {
420 new_alerts.push(Alert {
421 id: format!(
422 "error_{}",
423 SystemTime::now()
424 .duration_since(UNIX_EPOCH)
425 .expect("SystemTime should be after UNIX_EPOCH")
426 .as_millis()
427 ),
428 alert_type: AlertType::HighErrorRate,
429 severity: AlertSeverity::Critical,
430 message: format!("Operation had {} errors", metrics.error_count),
431 timestamp: SystemTime::now(),
432 resolved: false,
433 metadata: HashMap::new(),
434 });
435 }
436
437 for alert in new_alerts {
438 info!("New alert: {} - {}", alert.alert_type, alert.message);
439 self.alert_history.push_back(alert.clone());
440 self.active_alerts.push(alert);
441 }
442
443 Ok(())
444 }
445
446 fn get_active_alerts(&self) -> Vec<Alert> {
447 self.active_alerts.clone()
448 }
449}
450
451pub struct RealTimeMonitor {
453 current_metrics: RealTimeMetrics,
454 metrics_history: VecDeque<RealTimeMetrics>,
455 update_interval: Duration,
456 last_update: Instant,
457}
458
459impl RealTimeMonitor {
460 fn new(config: &ObservabilityConfig) -> Self {
461 Self {
462 current_metrics: RealTimeMetrics::default(),
463 metrics_history: VecDeque::new(),
464 update_interval: config.metrics_collection_interval,
465 last_update: Instant::now(),
466 }
467 }
468
469 async fn update_metrics(&mut self, operation_metrics: &OperationMetrics) -> Result<()> {
470 self.current_metrics.total_requests += 1;
471 self.current_metrics.total_errors += operation_metrics.error_count as u64;
472
473 let new_response_time = operation_metrics.execution_time;
475 self.current_metrics.avg_response_time = Duration::from_millis(
476 (self.current_metrics.avg_response_time.as_millis() as u64
477 + new_response_time.as_millis() as u64)
478 / 2,
479 );
480
481 self.current_metrics.error_rate =
483 self.current_metrics.total_errors as f64 / self.current_metrics.total_requests as f64;
484
485 if operation_metrics.cache_hit {
487 self.current_metrics.cache_hits += 1;
488 }
489 self.current_metrics.cache_hit_ratio =
490 self.current_metrics.cache_hits as f64 / self.current_metrics.total_requests as f64;
491
492 self.current_metrics.memory_usage_mb = system_monitor::get_current_memory_usage_mb().await;
494 self.current_metrics.cpu_usage_percent =
495 system_monitor::get_current_cpu_usage_percent().await;
496 self.current_metrics.timestamp = SystemTime::now();
497
498 if self.last_update.elapsed() >= self.update_interval {
500 self.metrics_history.push_back(self.current_metrics.clone());
501
502 while self.metrics_history.len() > 100 {
504 self.metrics_history.pop_front();
505 }
506
507 self.last_update = Instant::now();
508 }
509
510 Ok(())
511 }
512
513 fn get_current_state(&self) -> RealTimeMetrics {
514 self.current_metrics.clone()
515 }
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct ObservabilityDashboard {
522 pub service_info: ServiceInfo,
523 pub metrics: Option<MetricsSummary>,
524 pub traces: Option<Vec<DistributedTrace>>,
525 pub alerts: Option<Vec<Alert>>,
526 pub real_time_data: Option<RealTimeMetrics>,
527 pub health_status: HealthStatus,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
531pub struct ServiceInfo {
532 pub name: String,
533 pub version: String,
534 pub environment: String,
535 pub uptime: Duration,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
539pub struct MetricsSummary {
540 pub total_operations: usize,
541 pub aggregated: AggregatedMetrics,
542 pub counters: HashMap<String, u64>,
543 pub gauges: HashMap<String, f64>,
544 pub histograms: HashMap<String, HistogramSummary>,
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct AggregatedMetrics {
549 pub total_requests: u64,
550 pub total_errors: u64,
551 pub avg_execution_time: Duration,
552 pub min_execution_time: Duration,
553 pub max_execution_time: Duration,
554 pub p50_execution_time: Duration,
555 pub p95_execution_time: Duration,
556 pub p99_execution_time: Duration,
557}
558
559impl AggregatedMetrics {
560 fn new() -> Self {
561 Self {
562 total_requests: 0,
563 total_errors: 0,
564 avg_execution_time: Duration::from_millis(0),
565 min_execution_time: Duration::from_secs(u64::MAX),
566 max_execution_time: Duration::from_millis(0),
567 p50_execution_time: Duration::from_millis(0),
568 p95_execution_time: Duration::from_millis(0),
569 p99_execution_time: Duration::from_millis(0),
570 }
571 }
572
573 fn update(&mut self, metrics: &OperationMetrics) {
574 self.total_requests += 1;
575 self.total_errors += metrics.error_count as u64;
576
577 let exec_time = metrics.execution_time;
578
579 if exec_time < self.min_execution_time {
581 self.min_execution_time = exec_time;
582 }
583 if exec_time > self.max_execution_time {
584 self.max_execution_time = exec_time;
585 }
586
587 self.avg_execution_time = Duration::from_millis(
589 (self.avg_execution_time.as_millis() as u64 * (self.total_requests - 1)
590 + exec_time.as_millis() as u64)
591 / self.total_requests,
592 );
593 }
594}
595
596#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct DistributedTrace {
598 pub trace_id: String,
599 pub spans: Vec<TraceSpan>,
600 pub total_duration: Duration,
601 pub service_count: usize,
602 pub error_count: usize,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize)]
606pub struct TraceSpan {
607 pub trace_id: String,
608 pub span_id: String,
609 pub parent_span_id: Option<String>,
610 pub operation_name: String,
611 pub start_time: SystemTime,
612 pub duration: Option<Duration>,
613 pub tags: Vec<(String, String)>,
614 pub logs: Vec<SpanLog>,
615 pub status: SpanStatus,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
619pub struct SpanLog {
620 pub timestamp: SystemTime,
621 pub level: String,
622 pub message: String,
623 pub fields: HashMap<String, String>,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize)]
627pub enum SpanStatus {
628 Ok,
629 Error,
630 Timeout,
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize)]
634pub struct Alert {
635 pub id: String,
636 pub alert_type: AlertType,
637 pub severity: AlertSeverity,
638 pub message: String,
639 pub timestamp: SystemTime,
640 pub resolved: bool,
641 pub metadata: HashMap<String, String>,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
645pub enum AlertType {
646 HighResponseTime,
647 HighErrorRate,
648 HighMemoryUsage,
649 HighCpuUsage,
650 LowCacheHitRatio,
651 TooManyConcurrentRequests,
652}
653
654impl std::fmt::Display for AlertType {
655 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
656 match self {
657 AlertType::HighResponseTime => write!(f, "High Response Time"),
658 AlertType::HighErrorRate => write!(f, "High Error Rate"),
659 AlertType::HighMemoryUsage => write!(f, "High Memory Usage"),
660 AlertType::HighCpuUsage => write!(f, "High CPU Usage"),
661 AlertType::LowCacheHitRatio => write!(f, "Low Cache Hit Ratio"),
662 AlertType::TooManyConcurrentRequests => write!(f, "Too Many Concurrent Requests"),
663 }
664 }
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
668pub enum AlertSeverity {
669 Info,
670 Warning,
671 Critical,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize)]
675pub struct RealTimeMetrics {
676 pub total_requests: u64,
677 pub total_errors: u64,
678 pub error_rate: f64,
679 pub avg_response_time: Duration,
680 pub current_concurrent_requests: usize,
681 pub cache_hits: u64,
682 pub cache_hit_ratio: f64,
683 pub memory_usage_mb: f64,
684 pub cpu_usage_percent: f64,
685 pub timestamp: SystemTime,
686}
687
688impl Default for RealTimeMetrics {
689 fn default() -> Self {
690 Self {
691 total_requests: 0,
692 total_errors: 0,
693 error_rate: 0.0,
694 avg_response_time: Duration::from_millis(0),
695 current_concurrent_requests: 0,
696 cache_hits: 0,
697 cache_hit_ratio: 0.0,
698 memory_usage_mb: 0.0,
699 cpu_usage_percent: 0.0,
700 timestamp: SystemTime::now(),
701 }
702 }
703}
704
705#[derive(Debug, Clone, Serialize, Deserialize)]
706pub struct HealthStatus {
707 pub status: HealthStatusLevel,
708 pub score: f64,
709 pub issues: Vec<String>,
710 pub last_check: SystemTime,
711}
712
713#[derive(Debug, Clone, Serialize, Deserialize)]
714pub enum HealthStatusLevel {
715 Healthy,
716 Warning,
717 Critical,
718}
719
720#[derive(Debug, Clone)]
721pub struct Histogram {
722 buckets: Vec<(f64, u64)>,
723 total_count: u64,
724 total_sum: f64,
725}
726
727impl Histogram {
728 fn new() -> Self {
729 Self {
730 buckets: vec![
731 (1.0, 0),
732 (5.0, 0),
733 (10.0, 0),
734 (25.0, 0),
735 (50.0, 0),
736 (100.0, 0),
737 (250.0, 0),
738 (500.0, 0),
739 (1000.0, 0),
740 (2500.0, 0),
741 (5000.0, 0),
742 (10000.0, 0),
743 (f64::INFINITY, 0),
744 ],
745 total_count: 0,
746 total_sum: 0.0,
747 }
748 }
749
750 fn record(&mut self, value: f64) {
751 self.total_count += 1;
752 self.total_sum += value;
753
754 for (threshold, count) in &mut self.buckets {
755 if value <= *threshold {
756 *count += 1;
757 }
758 }
759 }
760
761 fn summary(&self) -> HistogramSummary {
762 HistogramSummary {
763 count: self.total_count,
764 sum: self.total_sum,
765 buckets: self.buckets.clone(),
766 }
767 }
768}
769
770#[derive(Debug, Clone, Serialize, Deserialize)]
771pub struct HistogramSummary {
772 pub count: u64,
773 pub sum: f64,
774 pub buckets: Vec<(f64, u64)>,
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use crate::ast::OperationType;
781 use crate::performance::ClientInfo;
782
783 #[tokio::test]
784 async fn test_observability_system_creation() {
785 let config = ObservabilityConfig::default();
786 let system = ObservabilitySystem::new(config);
787
788 let dashboard = system.get_dashboard().await.expect("should succeed");
789 assert_eq!(dashboard.service_info.name, "oxirs-gql");
790 }
791
792 #[tokio::test]
793 async fn test_metrics_collection() {
794 let config = ObservabilityConfig::default();
795 let system = ObservabilitySystem::new(config);
796
797 let metrics = OperationMetrics {
798 operation_name: Some("test_query".to_string()),
799 operation_type: OperationType::Query,
800 query_hash: 12345,
801 execution_time: Duration::from_millis(100),
802 parsing_time: Duration::from_millis(10),
803 validation_time: Duration::from_millis(5),
804 planning_time: Duration::from_millis(15),
805 field_count: 3,
806 depth: 2,
807 complexity_score: 10,
808 cache_hit: true,
809 error_count: 0,
810 timestamp: SystemTime::now(),
811 client_info: ClientInfo::default(),
812 };
813
814 system
815 .record_operation(&metrics)
816 .await
817 .expect("should succeed");
818
819 let dashboard = system.get_dashboard().await.expect("should succeed");
820 assert!(dashboard.metrics.is_some());
821 }
822
823 #[tokio::test]
824 async fn test_alert_generation() {
825 let mut config = ObservabilityConfig::default();
826 config.alert_thresholds.max_response_time_ms = 50; let system = ObservabilitySystem::new(config);
829
830 let slow_metrics = OperationMetrics {
831 operation_name: Some("slow_query".to_string()),
832 operation_type: OperationType::Query,
833 query_hash: 67890,
834 execution_time: Duration::from_millis(100), parsing_time: Duration::from_millis(10),
836 validation_time: Duration::from_millis(5),
837 planning_time: Duration::from_millis(15),
838 field_count: 5,
839 depth: 3,
840 complexity_score: 20,
841 cache_hit: false,
842 error_count: 0,
843 timestamp: SystemTime::now(),
844 client_info: ClientInfo::default(),
845 };
846
847 system
848 .record_operation(&slow_metrics)
849 .await
850 .expect("should succeed");
851
852 let dashboard = system.get_dashboard().await.expect("should succeed");
853 if let Some(alerts) = dashboard.alerts {
854 assert!(!alerts.is_empty());
855 }
856 }
857
858 #[tokio::test]
859 async fn test_health_status_calculation() {
860 let config = ObservabilityConfig::default();
861 let system = ObservabilitySystem::new(config);
862
863 let health = system.calculate_health_status().await;
864 assert!(matches!(health.status, HealthStatusLevel::Healthy));
865 assert!(health.score >= 90.0);
866 }
867}