Skip to main content

oxirs_chat/
dashboard.rs

1//! Analytics Dashboard Backend API
2//!
3//! Provides comprehensive analytics and metrics endpoints for monitoring
4//! chat system performance, user activity, and query patterns.
5
6use anyhow::Result;
7use chrono::{DateTime, Duration, Utc};
8#[cfg(feature = "excel-export")]
9use rust_xlsxwriter::{Format, Workbook};
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::sync::Arc;
13use tokio::sync::RwLock;
14use tracing::info;
15
16/// Dashboard analytics manager
17pub struct DashboardAnalytics {
18    /// Query performance metrics
19    query_metrics: Arc<RwLock<QueryMetrics>>,
20    /// User activity tracker
21    user_activity: Arc<RwLock<UserActivityTracker>>,
22    /// System health metrics
23    system_health: Arc<RwLock<SystemHealthMetrics>>,
24}
25
26/// Dashboard configuration
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct DashboardConfig {
29    /// Metrics retention period (days)
30    pub retention_days: u32,
31    /// Enable real-time updates
32    pub enable_realtime: bool,
33    /// Aggregation interval (seconds)
34    pub aggregation_interval_secs: u64,
35    /// Maximum data points per chart
36    pub max_data_points: usize,
37}
38
39impl Default for DashboardConfig {
40    fn default() -> Self {
41        Self {
42            retention_days: 30,
43            enable_realtime: true,
44            aggregation_interval_secs: 300, // 5 minutes
45            max_data_points: 100,
46        }
47    }
48}
49
50/// Query performance metrics
51#[derive(Debug, Clone, Default)]
52pub struct QueryMetrics {
53    /// Total queries executed
54    pub total_queries: u64,
55    /// Successful queries
56    pub successful_queries: u64,
57    /// Failed queries
58    pub failed_queries: u64,
59    /// Average response time (milliseconds)
60    pub avg_response_time_ms: f64,
61    /// P95 response time
62    pub p95_response_time_ms: f64,
63    /// P99 response time
64    pub p99_response_time_ms: f64,
65    /// Query history
66    pub query_history: Vec<QueryRecord>,
67}
68
69/// Individual query record
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct QueryRecord {
72    pub query_id: String,
73    pub query_type: QueryType,
74    pub execution_time_ms: u64,
75    pub result_count: usize,
76    pub success: bool,
77    pub timestamp: DateTime<Utc>,
78    pub error: Option<String>,
79}
80
81/// Query type classification
82#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
83#[serde(rename_all = "snake_case")]
84pub enum QueryType {
85    NaturalLanguage,
86    Sparql,
87    VectorSearch,
88    Hybrid,
89}
90
91/// User activity tracker
92#[derive(Debug, Clone, Default)]
93pub struct UserActivityTracker {
94    /// Active users (last 24 hours)
95    pub active_users_24h: u64,
96    /// Total sessions
97    pub total_sessions: u64,
98    /// Average session duration (seconds)
99    pub avg_session_duration_secs: f64,
100    /// User activity timeline
101    pub activity_timeline: Vec<ActivityDataPoint>,
102    /// Top users by activity
103    pub top_users: Vec<UserActivity>,
104}
105
106/// Activity data point for timeline charts
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ActivityDataPoint {
109    pub timestamp: DateTime<Utc>,
110    pub active_users: u64,
111    pub queries_per_minute: f64,
112    pub avg_response_time_ms: f64,
113}
114
115/// User activity summary
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct UserActivity {
118    pub user_id: String,
119    pub query_count: u64,
120    pub session_count: u64,
121    pub total_time_secs: u64,
122    pub last_active: DateTime<Utc>,
123}
124
125/// System health metrics
126#[derive(Debug, Clone, Default)]
127pub struct SystemHealthMetrics {
128    /// CPU usage percentage
129    pub cpu_usage_percent: f64,
130    /// Memory usage (MB)
131    pub memory_usage_mb: f64,
132    /// Active connections
133    pub active_connections: u64,
134    /// Cache hit rate
135    pub cache_hit_rate: f64,
136    /// Error rate (per 1000 requests)
137    pub error_rate: f64,
138    /// Health timeline
139    pub health_timeline: Vec<HealthDataPoint>,
140}
141
142/// Health data point for system monitoring
143#[derive(Debug, Clone, Serialize, Deserialize)]
144pub struct HealthDataPoint {
145    pub timestamp: DateTime<Utc>,
146    pub cpu_percent: f64,
147    pub memory_mb: f64,
148    pub active_connections: u64,
149    pub requests_per_second: f64,
150}
151
152impl DashboardAnalytics {
153    /// Create a new dashboard analytics manager
154    pub fn new(config: DashboardConfig) -> Self {
155        info!(
156            "Initializing dashboard analytics with retention: {} days",
157            config.retention_days
158        );
159
160        Self {
161            query_metrics: Arc::new(RwLock::new(QueryMetrics::default())),
162            user_activity: Arc::new(RwLock::new(UserActivityTracker::default())),
163            system_health: Arc::new(RwLock::new(SystemHealthMetrics::default())),
164        }
165    }
166
167    /// Get comprehensive dashboard overview
168    pub async fn get_overview(&self) -> DashboardOverview {
169        let query_metrics = self.query_metrics.read().await;
170        let user_activity = self.user_activity.read().await;
171        let system_health = self.system_health.read().await;
172
173        DashboardOverview {
174            total_queries: query_metrics.total_queries,
175            successful_queries: query_metrics.successful_queries,
176            failed_queries: query_metrics.failed_queries,
177            avg_response_time_ms: query_metrics.avg_response_time_ms,
178            active_users_24h: user_activity.active_users_24h,
179            total_sessions: user_activity.total_sessions,
180            cpu_usage_percent: system_health.cpu_usage_percent,
181            memory_usage_mb: system_health.memory_usage_mb,
182            cache_hit_rate: system_health.cache_hit_rate,
183            error_rate: system_health.error_rate,
184            timestamp: Utc::now(),
185        }
186    }
187
188    /// Get query performance analytics
189    pub async fn get_query_analytics(&self, time_range: TimeRange) -> QueryAnalytics {
190        let metrics = self.query_metrics.read().await;
191
192        // Filter queries by time range
193        let filtered_queries: Vec<_> = metrics
194            .query_history
195            .iter()
196            .filter(|q| time_range.contains(q.timestamp))
197            .cloned()
198            .collect();
199
200        // Calculate statistics
201        let total = filtered_queries.len() as u64;
202        let successful = filtered_queries.iter().filter(|q| q.success).count() as u64;
203        let failed = total - successful;
204
205        let execution_times: Vec<f64> = filtered_queries
206            .iter()
207            .map(|q| q.execution_time_ms as f64)
208            .collect();
209
210        let avg_time = if !execution_times.is_empty() {
211            execution_times.iter().sum::<f64>() / execution_times.len() as f64
212        } else {
213            0.0
214        };
215
216        // Query type distribution
217        let mut type_distribution = HashMap::new();
218        for query in &filtered_queries {
219            *type_distribution.entry(query.query_type).or_insert(0) += 1;
220        }
221
222        QueryAnalytics {
223            total_queries: total,
224            successful_queries: successful,
225            failed_queries: failed,
226            avg_response_time_ms: avg_time,
227            p95_response_time_ms: Self::calculate_percentile(&execution_times, 0.95),
228            p99_response_time_ms: Self::calculate_percentile(&execution_times, 0.99),
229            query_type_distribution: type_distribution,
230            time_range,
231        }
232    }
233
234    /// Get user activity analytics
235    pub async fn get_user_analytics(&self, time_range: TimeRange) -> UserAnalytics {
236        let activity = self.user_activity.read().await;
237
238        // Filter activity by time range
239        let filtered_timeline: Vec<_> = activity
240            .activity_timeline
241            .iter()
242            .filter(|a| time_range.contains(a.timestamp))
243            .cloned()
244            .collect();
245
246        UserAnalytics {
247            active_users: activity.active_users_24h,
248            total_sessions: activity.total_sessions,
249            avg_session_duration_secs: activity.avg_session_duration_secs,
250            activity_timeline: filtered_timeline,
251            top_users: activity.top_users.clone(),
252            time_range,
253        }
254    }
255
256    /// Get system health analytics
257    pub async fn get_health_analytics(&self, time_range: TimeRange) -> HealthAnalytics {
258        let health = self.system_health.read().await;
259
260        // Filter health data by time range
261        let filtered_timeline: Vec<_> = health
262            .health_timeline
263            .iter()
264            .filter(|h| time_range.contains(h.timestamp))
265            .cloned()
266            .collect();
267
268        HealthAnalytics {
269            current_cpu_percent: health.cpu_usage_percent,
270            current_memory_mb: health.memory_usage_mb,
271            active_connections: health.active_connections,
272            cache_hit_rate: health.cache_hit_rate,
273            error_rate: health.error_rate,
274            health_timeline: filtered_timeline,
275            time_range,
276        }
277    }
278
279    /// Record a query execution
280    pub async fn record_query(&self, record: QueryRecord) {
281        let mut metrics = self.query_metrics.write().await;
282
283        metrics.total_queries += 1;
284        if record.success {
285            metrics.successful_queries += 1;
286        } else {
287            metrics.failed_queries += 1;
288        }
289
290        // Update average response time
291        let total_time = metrics.avg_response_time_ms * (metrics.total_queries - 1) as f64
292            + record.execution_time_ms as f64;
293        metrics.avg_response_time_ms = total_time / metrics.total_queries as f64;
294
295        metrics.query_history.push(record);
296
297        // Keep only recent queries (limit to 10,000)
298        if metrics.query_history.len() > 10_000 {
299            metrics.query_history.drain(0..1_000);
300        }
301    }
302
303    /// Update user activity
304    pub async fn update_user_activity(&self, user_id: String, query_count: u64) {
305        let mut activity = self.user_activity.write().await;
306
307        // Update or create user activity record
308        if let Some(user) = activity.top_users.iter_mut().find(|u| u.user_id == user_id) {
309            user.query_count += query_count;
310            user.last_active = Utc::now();
311        } else {
312            activity.top_users.push(UserActivity {
313                user_id,
314                query_count,
315                session_count: 1,
316                total_time_secs: 0,
317                last_active: Utc::now(),
318            });
319        }
320
321        // Sort by query count and keep top 100
322        activity
323            .top_users
324            .sort_by(|a, b| b.query_count.cmp(&a.query_count));
325        activity.top_users.truncate(100);
326    }
327
328    /// Update system health metrics
329    pub async fn update_health(&self, cpu_percent: f64, memory_mb: f64, connections: u64) {
330        let mut health = self.system_health.write().await;
331
332        health.cpu_usage_percent = cpu_percent;
333        health.memory_usage_mb = memory_mb;
334        health.active_connections = connections;
335
336        // Calculate requests per second from query metrics
337        let requests_per_second = self.calculate_requests_per_second().await;
338
339        // Add to timeline
340        health.health_timeline.push(HealthDataPoint {
341            timestamp: Utc::now(),
342            cpu_percent,
343            memory_mb,
344            active_connections: connections,
345            requests_per_second,
346        });
347
348        // Keep only recent data (last 24 hours at 5-minute intervals = 288 points)
349        if health.health_timeline.len() > 288 {
350            health.health_timeline.drain(0..100);
351        }
352    }
353
354    /// Calculate current requests per second based on recent query activity
355    async fn calculate_requests_per_second(&self) -> f64 {
356        let metrics = self.query_metrics.read().await;
357
358        // Calculate RPS from queries in the last 60 seconds
359        let now = Utc::now();
360        let one_minute_ago = now - Duration::seconds(60);
361
362        let recent_queries = metrics
363            .query_history
364            .iter()
365            .filter(|q| q.timestamp >= one_minute_ago)
366            .count();
367
368        // Return queries per second
369        recent_queries as f64 / 60.0
370    }
371
372    /// Calculate percentile from sorted values
373    fn calculate_percentile(values: &[f64], percentile: f64) -> f64 {
374        if values.is_empty() {
375            return 0.0;
376        }
377
378        let mut sorted = values.to_vec();
379        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
380
381        let index = (percentile * sorted.len() as f64) as usize;
382        sorted.get(index).copied().unwrap_or(0.0)
383    }
384}
385
386/// Dashboard overview summary
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct DashboardOverview {
389    pub total_queries: u64,
390    pub successful_queries: u64,
391    pub failed_queries: u64,
392    pub avg_response_time_ms: f64,
393    pub active_users_24h: u64,
394    pub total_sessions: u64,
395    pub cpu_usage_percent: f64,
396    pub memory_usage_mb: f64,
397    pub cache_hit_rate: f64,
398    pub error_rate: f64,
399    pub timestamp: DateTime<Utc>,
400}
401
402/// Query analytics response
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct QueryAnalytics {
405    pub total_queries: u64,
406    pub successful_queries: u64,
407    pub failed_queries: u64,
408    pub avg_response_time_ms: f64,
409    pub p95_response_time_ms: f64,
410    pub p99_response_time_ms: f64,
411    pub query_type_distribution: HashMap<QueryType, u64>,
412    pub time_range: TimeRange,
413}
414
415/// User analytics response
416#[derive(Debug, Clone, Serialize, Deserialize)]
417pub struct UserAnalytics {
418    pub active_users: u64,
419    pub total_sessions: u64,
420    pub avg_session_duration_secs: f64,
421    pub activity_timeline: Vec<ActivityDataPoint>,
422    pub top_users: Vec<UserActivity>,
423    pub time_range: TimeRange,
424}
425
426/// Health analytics response
427#[derive(Debug, Clone, Serialize, Deserialize)]
428pub struct HealthAnalytics {
429    pub current_cpu_percent: f64,
430    pub current_memory_mb: f64,
431    pub active_connections: u64,
432    pub cache_hit_rate: f64,
433    pub error_rate: f64,
434    pub health_timeline: Vec<HealthDataPoint>,
435    pub time_range: TimeRange,
436}
437
438/// Time range for analytics queries
439#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
440pub struct TimeRange {
441    pub start: DateTime<Utc>,
442    pub end: DateTime<Utc>,
443}
444
445impl TimeRange {
446    /// Create a time range for the last N hours
447    pub fn last_hours(hours: i64) -> Self {
448        let end = Utc::now();
449        let start = end - Duration::hours(hours);
450        Self { start, end }
451    }
452
453    /// Create a time range for the last N days
454    pub fn last_days(days: i64) -> Self {
455        let end = Utc::now();
456        let start = end - Duration::days(days);
457        Self { start, end }
458    }
459
460    /// Check if a timestamp is within this range
461    pub fn contains(&self, timestamp: DateTime<Utc>) -> bool {
462        timestamp >= self.start && timestamp <= self.end
463    }
464}
465
466/// Export format for analytics data
467#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
468#[serde(rename_all = "snake_case")]
469pub enum ExportFormat {
470    Json,
471    Csv,
472    Excel,
473}
474
475impl DashboardAnalytics {
476    /// Export analytics data in specified format
477    pub async fn export_data(
478        &self,
479        format: ExportFormat,
480        time_range: TimeRange,
481    ) -> Result<Vec<u8>> {
482        match format {
483            ExportFormat::Json => self.export_json(time_range).await,
484            ExportFormat::Csv => self.export_csv(time_range).await,
485            ExportFormat::Excel => {
486                #[cfg(feature = "excel-export")]
487                {
488                    self.export_excel(time_range).await
489                }
490                #[cfg(not(feature = "excel-export"))]
491                {
492                    anyhow::bail!("Excel export requires the 'excel-export' feature to be enabled")
493                }
494            }
495        }
496    }
497
498    async fn export_json(&self, time_range: TimeRange) -> Result<Vec<u8>> {
499        let overview = self.get_overview().await;
500        let query_analytics = self.get_query_analytics(time_range).await;
501        let user_analytics = self.get_user_analytics(time_range).await;
502        let health_analytics = self.get_health_analytics(time_range).await;
503
504        let export_data = serde_json::json!({
505            "overview": overview,
506            "query_analytics": query_analytics,
507            "user_analytics": user_analytics,
508            "health_analytics": health_analytics,
509        });
510
511        Ok(serde_json::to_vec_pretty(&export_data)?)
512    }
513
514    async fn export_csv(&self, time_range: TimeRange) -> Result<Vec<u8>> {
515        let query_analytics = self.get_query_analytics(time_range).await;
516        let user_analytics = self.get_user_analytics(time_range).await;
517        let health_analytics = self.get_health_analytics(time_range).await;
518
519        let mut csv_output = String::new();
520
521        // Section 1: Query Analytics Summary
522        csv_output.push_str("=== QUERY ANALYTICS ===\n");
523        csv_output.push_str("Metric,Value\n");
524        csv_output.push_str(&format!(
525            "Total Queries,{}\n",
526            query_analytics.total_queries
527        ));
528        csv_output.push_str(&format!(
529            "Successful Queries,{}\n",
530            query_analytics.successful_queries
531        ));
532        csv_output.push_str(&format!(
533            "Failed Queries,{}\n",
534            query_analytics.failed_queries
535        ));
536        csv_output.push_str(&format!(
537            "Average Response Time (ms),{:.2}\n",
538            query_analytics.avg_response_time_ms
539        ));
540        csv_output.push_str(&format!(
541            "P95 Response Time (ms),{:.2}\n",
542            query_analytics.p95_response_time_ms
543        ));
544        csv_output.push_str(&format!(
545            "P99 Response Time (ms),{:.2}\n",
546            query_analytics.p99_response_time_ms
547        ));
548        csv_output.push('\n');
549
550        // Section 2: Query Type Distribution
551        csv_output.push_str("=== QUERY TYPE DISTRIBUTION ===\n");
552        csv_output.push_str("Query Type,Count\n");
553        for (query_type, count) in &query_analytics.query_type_distribution {
554            csv_output.push_str(&format!("{:?},{}\n", query_type, count));
555        }
556        csv_output.push('\n');
557
558        // Section 3: User Analytics
559        csv_output.push_str("=== USER ANALYTICS ===\n");
560        csv_output.push_str("Metric,Value\n");
561        csv_output.push_str(&format!("Active Users,{}\n", user_analytics.active_users));
562        csv_output.push_str(&format!(
563            "Total Sessions,{}\n",
564            user_analytics.total_sessions
565        ));
566        csv_output.push_str(&format!(
567            "Avg Session Duration (secs),{:.2}\n",
568            user_analytics.avg_session_duration_secs
569        ));
570        csv_output.push('\n');
571
572        // Section 4: Top Users
573        csv_output.push_str("=== TOP USERS ===\n");
574        csv_output.push_str("User ID,Query Count,Session Count,Total Time (secs),Last Active\n");
575        for user in &user_analytics.top_users {
576            csv_output.push_str(&format!(
577                "{},{},{},{},{}\n",
578                user.user_id,
579                user.query_count,
580                user.session_count,
581                user.total_time_secs,
582                user.last_active.to_rfc3339()
583            ));
584        }
585        csv_output.push('\n');
586
587        // Section 5: Health Analytics
588        csv_output.push_str("=== HEALTH ANALYTICS ===\n");
589        csv_output.push_str("Metric,Value\n");
590        csv_output.push_str(&format!(
591            "Current CPU (%),{:.2}\n",
592            health_analytics.current_cpu_percent
593        ));
594        csv_output.push_str(&format!(
595            "Current Memory (MB),{:.2}\n",
596            health_analytics.current_memory_mb
597        ));
598        csv_output.push_str(&format!(
599            "Active Connections,{}\n",
600            health_analytics.active_connections
601        ));
602        csv_output.push_str(&format!(
603            "Cache Hit Rate,{:.2}\n",
604            health_analytics.cache_hit_rate
605        ));
606        csv_output.push_str(&format!("Error Rate,{:.2}\n", health_analytics.error_rate));
607        csv_output.push('\n');
608
609        // Section 6: Health Timeline
610        csv_output.push_str("=== HEALTH TIMELINE ===\n");
611        csv_output.push_str("Timestamp,CPU (%),Memory (MB),Active Connections,Requests/Second\n");
612        for datapoint in &health_analytics.health_timeline {
613            csv_output.push_str(&format!(
614                "{},{:.2},{:.2},{},{:.2}\n",
615                datapoint.timestamp.to_rfc3339(),
616                datapoint.cpu_percent,
617                datapoint.memory_mb,
618                datapoint.active_connections,
619                datapoint.requests_per_second
620            ));
621        }
622        csv_output.push('\n');
623
624        // Section 7: Activity Timeline
625        csv_output.push_str("=== ACTIVITY TIMELINE ===\n");
626        csv_output.push_str("Timestamp,Active Users,Queries/Min,Avg Response Time (ms)\n");
627        for datapoint in &user_analytics.activity_timeline {
628            csv_output.push_str(&format!(
629                "{},{},{:.2},{:.2}\n",
630                datapoint.timestamp.to_rfc3339(),
631                datapoint.active_users,
632                datapoint.queries_per_minute,
633                datapoint.avg_response_time_ms
634            ));
635        }
636
637        Ok(csv_output.into_bytes())
638    }
639
640    #[cfg(feature = "excel-export")]
641    async fn export_excel(&self, time_range: TimeRange) -> Result<Vec<u8>> {
642        let query_analytics = self.get_query_analytics(time_range).await;
643        let user_analytics = self.get_user_analytics(time_range).await;
644        let health_analytics = self.get_health_analytics(time_range).await;
645
646        // Create a new workbook
647        let mut workbook = Workbook::new();
648
649        // Create header format
650        let header_format = Format::new().set_bold();
651
652        // Sheet 1: Query Analytics Summary
653        let worksheet = workbook.add_worksheet();
654        worksheet.set_name("Query Analytics")?;
655
656        worksheet.write_string_with_format(0, 0, "Metric", &header_format)?;
657        worksheet.write_string_with_format(0, 1, "Value", &header_format)?;
658
659        let mut row = 1;
660        worksheet.write_string(row, 0, "Total Queries")?;
661        worksheet.write_number(row, 1, query_analytics.total_queries as f64)?;
662        row += 1;
663
664        worksheet.write_string(row, 0, "Successful Queries")?;
665        worksheet.write_number(row, 1, query_analytics.successful_queries as f64)?;
666        row += 1;
667
668        worksheet.write_string(row, 0, "Failed Queries")?;
669        worksheet.write_number(row, 1, query_analytics.failed_queries as f64)?;
670        row += 1;
671
672        worksheet.write_string(row, 0, "Avg Response Time (ms)")?;
673        worksheet.write_number(row, 1, query_analytics.avg_response_time_ms)?;
674        row += 1;
675
676        worksheet.write_string(row, 0, "P95 Response Time (ms)")?;
677        worksheet.write_number(row, 1, query_analytics.p95_response_time_ms)?;
678        row += 1;
679
680        worksheet.write_string(row, 0, "P99 Response Time (ms)")?;
681        worksheet.write_number(row, 1, query_analytics.p99_response_time_ms)?;
682
683        // Sheet 2: Query Type Distribution
684        let worksheet = workbook.add_worksheet();
685        worksheet.set_name("Query Types")?;
686
687        worksheet.write_string_with_format(0, 0, "Query Type", &header_format)?;
688        worksheet.write_string_with_format(0, 1, "Count", &header_format)?;
689
690        let mut row = 1;
691        for (query_type, count) in &query_analytics.query_type_distribution {
692            worksheet.write_string(row, 0, format!("{:?}", query_type))?;
693            worksheet.write_number(row, 1, *count as f64)?;
694            row += 1;
695        }
696
697        // Sheet 3: User Analytics
698        let worksheet = workbook.add_worksheet();
699        worksheet.set_name("User Analytics")?;
700
701        worksheet.write_string_with_format(0, 0, "Metric", &header_format)?;
702        worksheet.write_string_with_format(0, 1, "Value", &header_format)?;
703
704        let mut row = 1;
705        worksheet.write_string(row, 0, "Active Users")?;
706        worksheet.write_number(row, 1, user_analytics.active_users as f64)?;
707        row += 1;
708
709        worksheet.write_string(row, 0, "Total Sessions")?;
710        worksheet.write_number(row, 1, user_analytics.total_sessions as f64)?;
711        row += 1;
712
713        worksheet.write_string(row, 0, "Avg Session Duration (secs)")?;
714        worksheet.write_number(row, 1, user_analytics.avg_session_duration_secs)?;
715
716        // Sheet 4: Top Users
717        let worksheet = workbook.add_worksheet();
718        worksheet.set_name("Top Users")?;
719
720        worksheet.write_string_with_format(0, 0, "User ID", &header_format)?;
721        worksheet.write_string_with_format(0, 1, "Query Count", &header_format)?;
722        worksheet.write_string_with_format(0, 2, "Session Count", &header_format)?;
723        worksheet.write_string_with_format(0, 3, "Total Time (secs)", &header_format)?;
724        worksheet.write_string_with_format(0, 4, "Last Active", &header_format)?;
725
726        let mut row = 1;
727        for user in &user_analytics.top_users {
728            worksheet.write_string(row, 0, &user.user_id)?;
729            worksheet.write_number(row, 1, user.query_count as f64)?;
730            worksheet.write_number(row, 2, user.session_count as f64)?;
731            worksheet.write_number(row, 3, user.total_time_secs as f64)?;
732            worksheet.write_string(row, 4, user.last_active.to_rfc3339())?;
733            row += 1;
734        }
735
736        // Sheet 5: Health Analytics
737        let worksheet = workbook.add_worksheet();
738        worksheet.set_name("Health Analytics")?;
739
740        worksheet.write_string_with_format(0, 0, "Metric", &header_format)?;
741        worksheet.write_string_with_format(0, 1, "Value", &header_format)?;
742
743        let mut row = 1;
744        worksheet.write_string(row, 0, "Current CPU (%)")?;
745        worksheet.write_number(row, 1, health_analytics.current_cpu_percent)?;
746        row += 1;
747
748        worksheet.write_string(row, 0, "Current Memory (MB)")?;
749        worksheet.write_number(row, 1, health_analytics.current_memory_mb)?;
750        row += 1;
751
752        worksheet.write_string(row, 0, "Active Connections")?;
753        worksheet.write_number(row, 1, health_analytics.active_connections as f64)?;
754        row += 1;
755
756        worksheet.write_string(row, 0, "Cache Hit Rate")?;
757        worksheet.write_number(row, 1, health_analytics.cache_hit_rate)?;
758        row += 1;
759
760        worksheet.write_string(row, 0, "Error Rate")?;
761        worksheet.write_number(row, 1, health_analytics.error_rate)?;
762
763        // Sheet 6: Health Timeline
764        let worksheet = workbook.add_worksheet();
765        worksheet.set_name("Health Timeline")?;
766
767        worksheet.write_string_with_format(0, 0, "Timestamp", &header_format)?;
768        worksheet.write_string_with_format(0, 1, "CPU (%)", &header_format)?;
769        worksheet.write_string_with_format(0, 2, "Memory (MB)", &header_format)?;
770        worksheet.write_string_with_format(0, 3, "Active Connections", &header_format)?;
771        worksheet.write_string_with_format(0, 4, "Requests/Second", &header_format)?;
772
773        let mut row = 1;
774        for datapoint in &health_analytics.health_timeline {
775            worksheet.write_string(row, 0, datapoint.timestamp.to_rfc3339())?;
776            worksheet.write_number(row, 1, datapoint.cpu_percent)?;
777            worksheet.write_number(row, 2, datapoint.memory_mb)?;
778            worksheet.write_number(row, 3, datapoint.active_connections as f64)?;
779            worksheet.write_number(row, 4, datapoint.requests_per_second)?;
780            row += 1;
781        }
782
783        // Sheet 7: Activity Timeline
784        let worksheet = workbook.add_worksheet();
785        worksheet.set_name("Activity Timeline")?;
786
787        worksheet.write_string_with_format(0, 0, "Timestamp", &header_format)?;
788        worksheet.write_string_with_format(0, 1, "Active Users", &header_format)?;
789        worksheet.write_string_with_format(0, 2, "Queries/Min", &header_format)?;
790        worksheet.write_string_with_format(0, 3, "Avg Response Time (ms)", &header_format)?;
791
792        let mut row = 1;
793        for datapoint in &user_analytics.activity_timeline {
794            worksheet.write_string(row, 0, datapoint.timestamp.to_rfc3339())?;
795            worksheet.write_number(row, 1, datapoint.active_users as f64)?;
796            worksheet.write_number(row, 2, datapoint.queries_per_minute)?;
797            worksheet.write_number(row, 3, datapoint.avg_response_time_ms)?;
798            row += 1;
799        }
800
801        // Save to bytes
802        let buffer = workbook.save_to_buffer()?;
803        Ok(buffer)
804    }
805}
806
807#[cfg(test)]
808mod tests {
809    use super::*;
810
811    #[tokio::test]
812    async fn test_dashboard_creation() {
813        let config = DashboardConfig::default();
814        let dashboard = DashboardAnalytics::new(config);
815        let overview = dashboard.get_overview().await;
816
817        assert_eq!(overview.total_queries, 0);
818        assert_eq!(overview.active_users_24h, 0);
819    }
820
821    #[tokio::test]
822    async fn test_record_query() {
823        let config = DashboardConfig::default();
824        let dashboard = DashboardAnalytics::new(config);
825
826        let record = QueryRecord {
827            query_id: "test-query-1".to_string(),
828            query_type: QueryType::NaturalLanguage,
829            execution_time_ms: 150,
830            result_count: 5,
831            success: true,
832            timestamp: Utc::now(),
833            error: None,
834        };
835
836        dashboard.record_query(record).await;
837
838        let overview = dashboard.get_overview().await;
839        assert_eq!(overview.total_queries, 1);
840        assert_eq!(overview.successful_queries, 1);
841    }
842
843    #[tokio::test]
844    async fn test_time_range() {
845        let now = Utc::now();
846        let range = TimeRange {
847            start: now - Duration::hours(24),
848            end: now + Duration::hours(1), // Add buffer for test timing
849        };
850
851        assert!(range.contains(now));
852        assert!(!range.contains(now - Duration::days(2)));
853    }
854
855    #[test]
856    fn test_percentile_calculation() {
857        let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0];
858        let p95 = DashboardAnalytics::calculate_percentile(&values, 0.95);
859        assert!(p95 >= 9.0);
860    }
861
862    #[tokio::test]
863    async fn test_csv_export_with_data() {
864        let config = DashboardConfig::default();
865        let dashboard = DashboardAnalytics::new(config);
866
867        dashboard
868            .record_query(QueryRecord {
869                query_id: "csv_test".to_string(),
870                query_type: QueryType::VectorSearch,
871                execution_time_ms: 75,
872                result_count: 20,
873                success: true,
874                timestamp: Utc::now(),
875                error: None,
876            })
877            .await;
878
879        let time_range = TimeRange::last_hours(24);
880        let csv_data = dashboard
881            .export_data(ExportFormat::Csv, time_range)
882            .await
883            .expect("should succeed");
884
885        let csv_str = String::from_utf8(csv_data).expect("should succeed");
886        assert!(csv_str.contains("=== QUERY ANALYTICS ==="));
887        assert!(csv_str.contains("Total Queries,1"));
888    }
889
890    #[tokio::test]
891    #[cfg(feature = "excel-export")]
892    async fn test_excel_export_with_data() {
893        let config = DashboardConfig::default();
894        let dashboard = DashboardAnalytics::new(config);
895
896        for i in 0..3 {
897            dashboard
898                .record_query(QueryRecord {
899                    query_id: format!("excel_{}", i),
900                    query_type: QueryType::Sparql,
901                    execution_time_ms: 100,
902                    result_count: 10,
903                    success: true,
904                    timestamp: Utc::now(),
905                    error: None,
906                })
907                .await;
908        }
909
910        let time_range = TimeRange::last_days(1);
911        let excel_data = dashboard
912            .export_data(ExportFormat::Excel, time_range)
913            .await
914            .expect("should succeed");
915
916        assert!(!excel_data.is_empty());
917        assert_eq!(&excel_data[0..2], b"PK"); // Excel/ZIP signature
918    }
919
920    #[tokio::test]
921    async fn test_rps_calculation() {
922        let config = DashboardConfig::default();
923        let dashboard = DashboardAnalytics::new(config);
924
925        for _ in 0..5 {
926            dashboard
927                .record_query(QueryRecord {
928                    query_id: format!("rps_{}", fastrand::u32(..)),
929                    query_type: QueryType::Hybrid,
930                    execution_time_ms: 50,
931                    result_count: 5,
932                    success: true,
933                    timestamp: Utc::now(),
934                    error: None,
935                })
936                .await;
937        }
938
939        dashboard.update_health(45.0, 500.0, 8).await;
940
941        let health = dashboard
942            .get_health_analytics(TimeRange::last_hours(1))
943            .await;
944        assert!(!health.health_timeline.is_empty());
945    }
946}