pub mod collector;
pub mod config;
pub mod events;
pub mod export;
pub mod privacy;
pub mod storage;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
pub use collector::TelemetryCollector;
pub use config::{TelemetryConfig, TelemetryLevel};
pub use events::{EventMetadata, EventType, TelemetryEvent};
pub use export::{ExportFormat, TelemetryExporter};
pub use privacy::{AnonymizationLevel, PrivacyControl};
pub use storage::TelemetryStorage;
pub struct TelemetrySystem {
config: Arc<RwLock<TelemetryConfig>>,
collector: Arc<TelemetryCollector>,
storage: Arc<RwLock<TelemetryStorage>>,
exporter: Arc<TelemetryExporter>,
}
impl TelemetrySystem {
pub async fn new(config: TelemetryConfig) -> Result<Self, TelemetryError> {
let config = Arc::new(RwLock::new(config));
let storage = Arc::new(RwLock::new(TelemetryStorage::new(
&config.read().await.storage_path,
)?));
let collector = Arc::new(TelemetryCollector::new(Arc::clone(&config)).await);
let exporter = Arc::new(TelemetryExporter::new(Arc::clone(&config)));
Ok(Self {
config,
collector,
storage,
exporter,
})
}
pub async fn record_event(&self, event: TelemetryEvent) -> Result<(), TelemetryError> {
if !self.config.read().await.enabled {
return Ok(());
}
let event = self.collector.apply_privacy(event).await?;
self.storage.write().await.store_event(&event).await?;
Ok(())
}
pub async fn get_statistics(&self) -> Result<TelemetryStatistics, TelemetryError> {
self.storage.read().await.get_statistics().await
}
pub async fn export(
&self,
format: ExportFormat,
output: &std::path::Path,
) -> Result<(), TelemetryError> {
let events = self.storage.read().await.get_all_events().await?;
self.exporter.export(&events, format, output).await
}
pub async fn clear_data(&self) -> Result<(), TelemetryError> {
self.storage.write().await.clear().await
}
pub async fn update_config(&self, new_config: TelemetryConfig) -> Result<(), TelemetryError> {
*self.config.write().await = new_config;
Ok(())
}
pub async fn is_enabled(&self) -> bool {
self.config.read().await.enabled
}
}
#[derive(Debug, thiserror::Error)]
pub enum TelemetryError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Privacy violation: {0}")]
PrivacyViolation(String),
#[error("Configuration error: {0}")]
Configuration(String),
#[error("Storage error: {0}")]
Storage(String),
#[error("Export error: {0}")]
Export(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TelemetryStatistics {
pub total_events: u64,
pub events_by_type: std::collections::HashMap<String, u64>,
pub synthesis_requests: u64,
pub avg_synthesis_duration: f64,
pub total_errors: u64,
pub most_used_commands: Vec<(String, u64)>,
pub most_used_voices: Vec<(String, u64)>,
pub start_time: Option<chrono::DateTime<chrono::Utc>>,
pub end_time: Option<chrono::DateTime<chrono::Utc>>,
pub storage_size_bytes: u64,
}
impl Default for TelemetryStatistics {
fn default() -> Self {
Self {
total_events: 0,
events_by_type: std::collections::HashMap::new(),
synthesis_requests: 0,
avg_synthesis_duration: 0.0,
total_errors: 0,
most_used_commands: Vec::new(),
most_used_voices: Vec::new(),
start_time: None,
end_time: None,
storage_size_bytes: 0,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[tokio::test]
async fn test_telemetry_system_creation() {
let temp_dir = std::env::temp_dir().join("voirs_telemetry_test");
let config = TelemetryConfig {
enabled: true,
level: TelemetryLevel::Standard,
storage_path: temp_dir.clone(),
anonymization: AnonymizationLevel::Medium,
remote_endpoint: None,
batch_size: 100,
flush_interval_secs: 60,
};
let system = TelemetrySystem::new(config).await;
assert!(system.is_ok());
let _ = std::fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_telemetry_disabled() {
let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_disabled");
let config = TelemetryConfig {
enabled: false,
level: TelemetryLevel::Minimal,
storage_path: temp_dir.clone(),
anonymization: AnonymizationLevel::High,
remote_endpoint: None,
batch_size: 100,
flush_interval_secs: 60,
};
let system = TelemetrySystem::new(config).await.unwrap();
assert!(!system.is_enabled().await);
let _ = std::fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_record_event() {
let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_record");
let config = TelemetryConfig {
enabled: true,
level: TelemetryLevel::Standard,
storage_path: temp_dir.clone(),
anonymization: AnonymizationLevel::Low,
remote_endpoint: None,
batch_size: 100,
flush_interval_secs: 60,
};
let system = TelemetrySystem::new(config).await.unwrap();
let event = TelemetryEvent {
id: uuid::Uuid::new_v4().to_string(),
event_type: EventType::CommandExecuted,
timestamp: chrono::Utc::now(),
metadata: EventMetadata::default(),
user_id: Some("test_user".to_string()),
session_id: uuid::Uuid::new_v4().to_string(),
};
let result = system.record_event(event).await;
assert!(result.is_ok());
let _ = std::fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_get_statistics() {
let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_stats");
let config = TelemetryConfig {
enabled: true,
level: TelemetryLevel::Standard,
storage_path: temp_dir.clone(),
anonymization: AnonymizationLevel::Low,
remote_endpoint: None,
batch_size: 100,
flush_interval_secs: 60,
};
let system = TelemetrySystem::new(config).await.unwrap();
let stats = system.get_statistics().await;
assert!(stats.is_ok());
let _ = std::fs::remove_dir_all(temp_dir);
}
#[tokio::test]
async fn test_clear_data() {
let temp_dir = std::env::temp_dir().join("voirs_telemetry_test_clear");
let config = TelemetryConfig {
enabled: true,
level: TelemetryLevel::Standard,
storage_path: temp_dir.clone(),
anonymization: AnonymizationLevel::Low,
remote_endpoint: None,
batch_size: 100,
flush_interval_secs: 60,
};
let system = TelemetrySystem::new(config).await.unwrap();
let result = system.clear_data().await;
assert!(result.is_ok());
let _ = std::fs::remove_dir_all(temp_dir);
}
}