1use crate::telemetry::{EventType, TelemetryEvent, TelemetryStorage};
7use anyhow::{Context, Result};
8use chrono::Timelike;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug, Clone, Copy)]
15pub enum OutputFormat {
16 Text,
18 Json,
20 Markdown,
22}
23
24fn get_storage_path() -> PathBuf {
26 dirs::data_local_dir()
27 .unwrap_or_else(|| PathBuf::from("."))
28 .join("voirs")
29 .join("telemetry")
30}
31
32pub async fn analyze_telemetry(
34 output: Option<PathBuf>,
35 format: Option<OutputFormat>,
36 time_range: Option<TimeRange>,
37 verbose: bool,
38) -> Result<()> {
39 let storage_path = get_storage_path();
41 let storage =
42 TelemetryStorage::new(&storage_path).context("Failed to initialize telemetry storage")?;
43
44 let mut all_events = storage.get_all_events().await?;
46
47 if let Some(range) = &time_range {
49 all_events.retain(|e| range.contains(e.timestamp));
50 }
51
52 if verbose {
53 eprintln!("Analyzing {} telemetry events...", all_events.len());
54 }
55
56 let analysis = generate_analysis(&all_events);
58
59 let output_format = format.unwrap_or(OutputFormat::Text);
61 let formatted = format_analysis(&analysis, output_format)?;
62
63 if let Some(path) = output {
65 std::fs::write(&path, formatted)
66 .with_context(|| format!("Failed to write output to {}", path.display()))?;
67 println!("Analysis written to {}", path.display());
68 } else {
69 println!("{}", formatted);
70 }
71
72 Ok(())
73}
74
75pub async fn generate_insights(
77 min_confidence: f32,
78 max_insights: usize,
79 verbose: bool,
80) -> Result<()> {
81 let storage_path = get_storage_path();
82 let storage =
83 TelemetryStorage::new(&storage_path).context("Failed to initialize telemetry storage")?;
84
85 let all_events = storage.get_all_events().await?;
86
87 if verbose {
88 eprintln!("Generating insights from {} events...", all_events.len());
89 }
90
91 let insights = find_insights(&all_events, min_confidence);
92
93 println!("\nš Telemetry Insights\n");
95 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
96
97 for (i, insight) in insights.iter().take(max_insights).enumerate() {
98 println!(
99 "{}. {} (Confidence: {:.1}%)",
100 i + 1,
101 insight.message,
102 insight.confidence * 100.0
103 );
104 if !insight.recommendation.is_empty() {
105 println!(" š” {}", insight.recommendation);
106 }
107 println!();
108 }
109
110 if insights.is_empty() {
111 println!("No significant insights found. Continue using VoiRS to collect more data.\n");
112 }
113
114 Ok(())
115}
116
117pub async fn compare_periods(period1: TimeRange, period2: TimeRange, verbose: bool) -> Result<()> {
119 let storage_path = get_storage_path();
120 let storage =
121 TelemetryStorage::new(&storage_path).context("Failed to initialize telemetry storage")?;
122
123 let all_events = storage.get_all_events().await?;
124
125 let events1: Vec<_> = all_events
126 .iter()
127 .filter(|e| period1.contains(e.timestamp))
128 .collect();
129 let events2: Vec<_> = all_events
130 .iter()
131 .filter(|e| period2.contains(e.timestamp))
132 .collect();
133
134 if verbose {
135 eprintln!(
136 "Comparing {} events (period 1) vs {} events (period 2)",
137 events1.len(),
138 events2.len()
139 );
140 }
141
142 let events1_owned: Vec<TelemetryEvent> = events1.iter().map(|e| (*e).clone()).collect();
143 let events2_owned: Vec<TelemetryEvent> = events2.iter().map(|e| (*e).clone()).collect();
144
145 let analysis1 = generate_analysis(&events1_owned);
146 let analysis2 = generate_analysis(&events2_owned);
147
148 println!("\nš Telemetry Comparison\n");
150 println!("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
151
152 println!("Period 1: {} events", events1.len());
153 println!("Period 2: {} events", events2.len());
154 println!(
155 "Change: {:+.1}%\n",
156 calculate_change(events1.len(), events2.len())
157 );
158
159 compare_metric(
161 "Total Commands",
162 analysis1.command_count,
163 analysis2.command_count,
164 );
165 compare_metric(
166 "Synthesis Requests",
167 analysis1.synthesis_count,
168 analysis2.synthesis_count,
169 );
170 compare_metric("Error Rate", analysis1.error_rate, analysis2.error_rate);
171 compare_metric(
172 "Avg Performance (ms)",
173 analysis1.avg_performance_ms,
174 analysis2.avg_performance_ms,
175 );
176
177 Ok(())
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct TimeRange {
183 pub start: chrono::DateTime<chrono::Utc>,
184 pub end: chrono::DateTime<chrono::Utc>,
185}
186
187impl TimeRange {
188 pub fn new(start: chrono::DateTime<chrono::Utc>, end: chrono::DateTime<chrono::Utc>) -> Self {
190 Self { start, end }
191 }
192
193 pub fn contains(&self, timestamp: chrono::DateTime<chrono::Utc>) -> bool {
195 timestamp >= self.start && timestamp <= self.end
196 }
197
198 pub fn last_days(days: u32) -> Self {
200 let now = chrono::Utc::now();
201 let start = now - chrono::Duration::days(days as i64);
202 Self { start, end: now }
203 }
204
205 pub fn last_hours(hours: u32) -> Self {
207 let now = chrono::Utc::now();
208 let start = now - chrono::Duration::hours(hours as i64);
209 Self { start, end: now }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct TelemetryAnalysis {
216 pub total_events: usize,
217 pub command_count: usize,
218 pub synthesis_count: usize,
219 pub error_count: usize,
220 pub error_rate: f32,
221 pub avg_performance_ms: f32,
222 pub command_distribution: HashMap<String, usize>,
223 pub error_distribution: HashMap<String, usize>,
224 pub peak_hours: Vec<usize>,
225}
226
227fn generate_analysis(events: &[TelemetryEvent]) -> TelemetryAnalysis {
229 let total_events = events.len();
230 let command_count = events
231 .iter()
232 .filter(|e| matches!(e.event_type, EventType::CommandExecuted))
233 .count();
234 let synthesis_count = events
235 .iter()
236 .filter(|e| matches!(e.event_type, EventType::SynthesisRequest))
237 .count();
238 let error_count = events
239 .iter()
240 .filter(|e| matches!(e.event_type, EventType::Error))
241 .count();
242
243 let error_rate = if total_events > 0 {
244 (error_count as f32 / total_events as f32) * 100.0
245 } else {
246 0.0
247 };
248
249 let performance_events: Vec<_> = events
251 .iter()
252 .filter_map(|e| {
253 if matches!(e.event_type, EventType::Performance) {
254 e.metadata
255 .get("duration_ms")
256 .and_then(|v| v.parse::<f32>().ok())
257 } else {
258 None
259 }
260 })
261 .collect();
262
263 let avg_performance_ms = if !performance_events.is_empty() {
264 performance_events.iter().sum::<f32>() / performance_events.len() as f32
265 } else {
266 0.0
267 };
268
269 let mut command_distribution = HashMap::new();
271 for event in events.iter() {
272 if matches!(event.event_type, EventType::CommandExecuted) {
273 if let Some(command) = event.metadata.get("command") {
274 *command_distribution.entry(command.clone()).or_insert(0) += 1;
275 }
276 }
277 }
278
279 let mut error_distribution = HashMap::new();
281 for event in events.iter() {
282 if matches!(event.event_type, EventType::Error) {
283 if let Some(severity) = event.metadata.get("severity") {
284 *error_distribution.entry(severity.clone()).or_insert(0) += 1;
285 }
286 }
287 }
288
289 let mut hour_counts = vec![0usize; 24];
291 for event in events.iter() {
292 let hour = event.timestamp.hour() as usize;
293 hour_counts[hour] += 1;
294 }
295
296 let max_count = *hour_counts.iter().max().unwrap_or(&0);
297 let peak_hours: Vec<usize> = hour_counts
298 .iter()
299 .enumerate()
300 .filter(|(_, &count)| count > max_count / 2) .map(|(hour, _)| hour)
302 .collect();
303
304 TelemetryAnalysis {
305 total_events,
306 command_count,
307 synthesis_count,
308 error_count,
309 error_rate,
310 avg_performance_ms,
311 command_distribution,
312 error_distribution,
313 peak_hours,
314 }
315}
316
317fn format_analysis(analysis: &TelemetryAnalysis, format: OutputFormat) -> Result<String> {
319 match format {
320 OutputFormat::Json => Ok(serde_json::to_string_pretty(analysis)?),
321 OutputFormat::Text => Ok(format_text_analysis(analysis)),
322 _ => Ok(format_text_analysis(analysis)),
323 }
324}
325
326fn format_text_analysis(analysis: &TelemetryAnalysis) -> String {
328 let mut output = String::new();
329
330 output.push_str("\nš Telemetry Analysis Report\n");
331 output.push_str("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n\n");
332
333 output.push_str(&format!("Total Events: {}\n", analysis.total_events));
334 output.push_str(&format!(
335 "Commands Executed: {}\n",
336 analysis.command_count
337 ));
338 output.push_str(&format!(
339 "Synthesis Requests: {}\n",
340 analysis.synthesis_count
341 ));
342 output.push_str(&format!(
343 "Errors: {} ({:.1}%)\n",
344 analysis.error_count, analysis.error_rate
345 ));
346 output.push_str(&format!(
347 "Avg Performance: {:.1}ms\n\n",
348 analysis.avg_performance_ms
349 ));
350
351 if !analysis.command_distribution.is_empty() {
353 output.push_str("Top Commands:\n");
354 let mut commands: Vec<_> = analysis.command_distribution.iter().collect();
355 commands.sort_by(|a, b| b.1.cmp(a.1));
356 for (i, (cmd, count)) in commands.iter().take(5).enumerate() {
357 output.push_str(&format!(" {}. {} ({})\n", i + 1, cmd, count));
358 }
359 output.push('\n');
360 }
361
362 if !analysis.peak_hours.is_empty() {
364 output.push_str("Peak Activity Hours: ");
365 output.push_str(
366 &analysis
367 .peak_hours
368 .iter()
369 .map(|h| format!("{}:00", h))
370 .collect::<Vec<_>>()
371 .join(", "),
372 );
373 output.push_str("\n\n");
374 }
375
376 output
377}
378
379#[derive(Debug, Clone)]
381pub struct Insight {
382 pub message: String,
383 pub recommendation: String,
384 pub confidence: f32,
385}
386
387fn find_insights(events: &[TelemetryEvent], min_confidence: f32) -> Vec<Insight> {
389 let mut insights = Vec::new();
390 let analysis = generate_analysis(events);
391
392 if analysis.error_rate > 10.0 {
394 insights.push(Insight {
395 message: format!("High error rate detected ({:.1}%)", analysis.error_rate),
396 recommendation: "Review error logs and consider filing a bug report".to_string(),
397 confidence: (analysis.error_rate / 100.0).min(1.0),
398 });
399 }
400
401 if analysis.avg_performance_ms > 5000.0 {
403 insights.push(Insight {
404 message: format!(
405 "Slow average performance ({:.1}ms)",
406 analysis.avg_performance_ms
407 ),
408 recommendation: "Consider optimizing models or using GPU acceleration".to_string(),
409 confidence: (analysis.avg_performance_ms / 10000.0).min(1.0),
410 });
411 }
412
413 if analysis.synthesis_count > 100 && analysis.command_count < 10 {
415 insights.push(Insight {
416 message: "You're mainly using synthesis - explore other VoiRS features".to_string(),
417 recommendation: "Try batch processing, voice cloning, or interactive mode".to_string(),
418 confidence: 0.8,
419 });
420 }
421
422 insights.retain(|i| i.confidence >= min_confidence);
424
425 insights.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap());
427
428 insights
429}
430
431fn calculate_change(old_value: usize, new_value: usize) -> f32 {
433 if old_value == 0 {
434 if new_value > 0 {
435 100.0
436 } else {
437 0.0
438 }
439 } else {
440 ((new_value as f32 - old_value as f32) / old_value as f32) * 100.0
441 }
442}
443
444fn compare_metric(name: &str, value1: impl std::fmt::Display, value2: impl std::fmt::Display) {
446 println!(
447 "{:20} {:>12} -> {:>12}",
448 name,
449 format!("{}", value1),
450 format!("{}", value2)
451 );
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_time_range_contains() {
460 let start = chrono::Utc::now() - chrono::Duration::days(1);
461 let end = chrono::Utc::now();
462 let middle = chrono::Utc::now() - chrono::Duration::hours(12);
463
464 let range = TimeRange::new(start, end);
465 assert!(range.contains(middle));
466 assert!(!range.contains(start - chrono::Duration::hours(1)));
467 assert!(!range.contains(end + chrono::Duration::hours(1)));
468 }
469
470 #[test]
471 fn test_time_range_last_days() {
472 let now = chrono::Utc::now();
473 let range = TimeRange::last_days(7);
474
475 assert!(range.contains(now - chrono::Duration::hours(1)));
477 assert!(range.contains(now - chrono::Duration::days(1)));
478 assert!(range.contains(now - chrono::Duration::days(6)));
479
480 assert!(!range.contains(now - chrono::Duration::days(8)));
482 }
483
484 #[test]
485 fn test_generate_analysis_empty() {
486 let events = vec![];
487 let analysis = generate_analysis(&events);
488 assert_eq!(analysis.total_events, 0);
489 assert_eq!(analysis.error_rate, 0.0);
490 }
491
492 #[test]
493 fn test_calculate_change() {
494 assert_eq!(calculate_change(100, 150), 50.0);
495 assert_eq!(calculate_change(100, 50), -50.0);
496 assert_eq!(calculate_change(0, 100), 100.0);
497 }
498
499 #[test]
500 fn test_find_insights_high_error_rate() {
501 let mut events = Vec::new();
503 let now = chrono::Utc::now();
504
505 for i in 0..20 {
506 let mut metadata = crate::telemetry::EventMetadata::new();
507 metadata.set("message", "test error");
508 metadata.set("severity", "high");
509
510 events.push(TelemetryEvent {
511 id: format!("event_{}", i),
512 event_type: EventType::Error,
513 timestamp: now,
514 metadata,
515 user_id: Some("test_user".to_string()),
516 session_id: "test_session".to_string(),
517 });
518 }
519
520 for i in 20..100 {
521 let mut metadata = crate::telemetry::EventMetadata::new();
522 metadata.set("command", "test");
523
524 events.push(TelemetryEvent {
525 id: format!("event_{}", i),
526 event_type: EventType::CommandExecuted,
527 timestamp: now,
528 metadata,
529 user_id: Some("test_user".to_string()),
530 session_id: "test_session".to_string(),
531 });
532 }
533
534 let insights = find_insights(&events, 0.1);
535 assert!(!insights.is_empty());
536 }
537
538 #[test]
539 fn test_format_text_analysis() {
540 let analysis = TelemetryAnalysis {
541 total_events: 100,
542 command_count: 80,
543 synthesis_count: 50,
544 error_count: 5,
545 error_rate: 5.0,
546 avg_performance_ms: 1500.0,
547 command_distribution: HashMap::new(),
548 error_distribution: HashMap::new(),
549 peak_hours: vec![9, 14, 20],
550 };
551
552 let text = format_text_analysis(&analysis);
553 assert!(text.contains("Total Events:"));
554 assert!(text.contains("100"));
555 assert!(text.contains("5.0%"));
556 }
557}