reasonkit/telemetry/
mod.rs

1//! # ReasonKit Telemetry Module
2//!
3//! Privacy-first telemetry for the ReasonKit Adaptive Learning Loop (RALL).
4//!
5//! ## Design Principles
6//!
7//! 1. **Local-First**: All data stored in SQLite, never sent externally by default
8//! 2. **Privacy-First**: PII stripping, differential privacy, opt-in only
9//! 3. **Lightweight**: Minimal overhead, async batch processing
10//! 4. **Auditable**: Full schema versioning, data lineage
11//!
12//! ## Architecture
13//!
14//! ```text
15//! ┌─────────────────────────────────────────────────────────────┐
16//! │                    RALL TELEMETRY SYSTEM                     │
17//! ├─────────────────────────────────────────────────────────────┤
18//! │ LAYER 4: AGGREGATION                                        │
19//! │   Local Clustering | Pattern Detection | Model Feedback     │
20//! ├─────────────────────────────────────────────────────────────┤
21//! │ LAYER 3: PRIVACY FIREWALL                                   │
22//! │   PII Stripping | Differential Privacy | Redaction          │
23//! ├─────────────────────────────────────────────────────────────┤
24//! │ LAYER 2: COLLECTION                                         │
25//! │   Event Queue | Batch Writer | Schema Validation            │
26//! ├─────────────────────────────────────────────────────────────┤
27//! │ LAYER 1: EVENTS                                             │
28//! │   Query Events | Feedback Events | Session Events           │
29//! └─────────────────────────────────────────────────────────────┘
30//! ```
31
32mod config;
33mod events;
34mod privacy;
35mod schema;
36mod storage;
37
38// OpenTelemetry module (stub - feature currently commented out in Cargo.toml)
39// TODO: Uncomment when ready to implement full OpenTelemetry integration
40// #[cfg(feature = "opentelemetry")]
41// mod opentelemetry;
42
43pub use config::*;
44pub use events::*;
45pub use privacy::*;
46pub use schema::*;
47pub use storage::*;
48
49// #[cfg(feature = "opentelemetry")]
50// pub use opentelemetry::*;
51
52use chrono::{DateTime, Utc};
53use serde::{Deserialize, Serialize};
54use std::sync::Arc;
55use tokio::sync::RwLock;
56use uuid::Uuid;
57
58/// Telemetry system version for schema migrations
59pub const TELEMETRY_SCHEMA_VERSION: u32 = 1;
60
61/// Default telemetry database filename
62pub const DEFAULT_TELEMETRY_DB: &str = ".rk_telemetry.db";
63
64/// Result type for telemetry operations
65pub type TelemetryResult<T> = Result<T, TelemetryError>;
66
67/// Telemetry error types
68#[derive(Debug, thiserror::Error)]
69pub enum TelemetryError {
70    /// Database error
71    #[error("Database error: {0}")]
72    Database(String),
73
74    /// Configuration error
75    #[error("Configuration error: {0}")]
76    Config(String),
77
78    /// Privacy violation detected
79    #[error("Privacy violation: {0}")]
80    PrivacyViolation(String),
81
82    /// Schema validation error
83    #[error("Schema validation error: {0}")]
84    SchemaValidation(String),
85
86    /// IO error
87    #[error("IO error: {0}")]
88    Io(#[from] std::io::Error),
89
90    /// JSON serialization error
91    #[error("JSON error: {0}")]
92    Json(#[from] serde_json::Error),
93
94    /// Telemetry disabled
95    #[error("Telemetry is disabled")]
96    Disabled,
97}
98
99/// Main telemetry collector
100pub struct TelemetryCollector {
101    /// Configuration
102    config: TelemetryConfig,
103    /// Storage backend
104    storage: Arc<RwLock<TelemetryStorage>>,
105    /// Privacy filter
106    privacy: PrivacyFilter,
107    /// Session ID
108    session_id: Uuid,
109    /// Whether telemetry is enabled
110    enabled: bool,
111}
112
113impl TelemetryCollector {
114    /// Create a new telemetry collector
115    pub async fn new(config: TelemetryConfig) -> TelemetryResult<Self> {
116        if !config.enabled {
117            return Ok(Self {
118                config: config.clone(),
119                storage: Arc::new(RwLock::new(TelemetryStorage::noop())),
120                privacy: PrivacyFilter::new(config.privacy.clone()),
121                session_id: Uuid::new_v4(),
122                enabled: false,
123            });
124        }
125
126        let mut storage = TelemetryStorage::new(&config.db_path).await?;
127        let privacy = PrivacyFilter::new(config.privacy.clone());
128        let session_id = Uuid::new_v4();
129
130        // Insert session record to satisfy foreign key constraints
131        storage.insert_session(session_id).await?;
132
133        Ok(Self {
134            config: config.clone(),
135            storage: Arc::new(RwLock::new(storage)),
136            privacy,
137            session_id,
138            enabled: true,
139        })
140    }
141
142    /// Record a query event
143    pub async fn record_query(&self, event: QueryEvent) -> TelemetryResult<()> {
144        if !self.enabled {
145            return Ok(());
146        }
147
148        // Apply privacy filter
149        let sanitized = self.privacy.sanitize_query_event(event)?;
150
151        // Store event
152        let mut storage = self.storage.write().await;
153        storage.insert_query_event(&sanitized).await
154    }
155
156    /// Record user feedback
157    pub async fn record_feedback(&self, event: FeedbackEvent) -> TelemetryResult<()> {
158        if !self.enabled {
159            return Ok(());
160        }
161
162        let sanitized = self.privacy.sanitize_feedback_event(event)?;
163
164        let mut storage = self.storage.write().await;
165        storage.insert_feedback_event(&sanitized).await
166    }
167
168    /// Record a reasoning trace
169    pub async fn record_trace(&self, event: TraceEvent) -> TelemetryResult<()> {
170        if !self.enabled {
171            return Ok(());
172        }
173
174        let sanitized = self.privacy.sanitize_trace_event(event)?;
175
176        let mut storage = self.storage.write().await;
177        storage.insert_trace_event(&sanitized).await
178    }
179
180    /// Get aggregated metrics for local ML training
181    pub async fn get_aggregated_metrics(&self) -> TelemetryResult<AggregatedMetrics> {
182        if !self.enabled {
183            return Err(TelemetryError::Disabled);
184        }
185
186        let storage = self.storage.read().await;
187        storage.get_aggregated_metrics().await
188    }
189
190    /// Export anonymized data for community model training (opt-in)
191    pub async fn export_for_community(&self) -> TelemetryResult<CommunityExport> {
192        if !self.enabled || !self.config.community_contribution {
193            return Err(TelemetryError::Disabled);
194        }
195
196        let storage = self.storage.read().await;
197        storage.export_anonymized().await
198    }
199
200    /// Get current session ID
201    pub fn session_id(&self) -> Uuid {
202        self.session_id
203    }
204
205    /// Check if telemetry is enabled
206    pub fn is_enabled(&self) -> bool {
207        self.enabled
208    }
209}
210
211/// Aggregated metrics for local ML optimization
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct AggregatedMetrics {
214    /// Total query count
215    pub total_queries: u64,
216    /// Average query latency (ms)
217    pub avg_latency_ms: f64,
218    /// Tool usage distribution
219    pub tool_usage: Vec<ToolUsageMetric>,
220    /// Query pattern clusters
221    pub query_clusters: Vec<QueryCluster>,
222    /// Feedback summary
223    pub feedback_summary: FeedbackSummary,
224    /// Time range
225    pub time_range: TimeRange,
226}
227
228/// Tool usage metric
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct ToolUsageMetric {
231    /// Tool name
232    pub tool: String,
233    /// Usage count
234    pub count: u64,
235    /// Success rate (0.0 - 1.0)
236    pub success_rate: f64,
237    /// Average execution time (ms)
238    pub avg_execution_ms: f64,
239}
240
241/// Query cluster for pattern detection
242#[derive(Debug, Clone, Serialize, Deserialize)]
243pub struct QueryCluster {
244    /// Cluster ID
245    pub id: u32,
246    /// Centroid embedding (if available)
247    pub centroid: Option<Vec<f32>>,
248    /// Number of queries in cluster
249    pub count: u64,
250    /// Representative query (anonymized)
251    pub representative: String,
252    /// Common tools used
253    pub common_tools: Vec<String>,
254}
255
256/// Feedback summary
257#[derive(Debug, Clone, Serialize, Deserialize)]
258pub struct FeedbackSummary {
259    /// Total feedback count
260    pub total_feedback: u64,
261    /// Positive feedback ratio
262    pub positive_ratio: f64,
263    /// Categories with most negative feedback
264    pub improvement_areas: Vec<String>,
265}
266
267/// Time range for metrics
268#[derive(Debug, Clone, Serialize, Deserialize)]
269pub struct TimeRange {
270    /// Start time
271    pub start: DateTime<Utc>,
272    /// End time
273    pub end: DateTime<Utc>,
274}
275
276/// Community export format (fully anonymized)
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct CommunityExport {
279    /// Schema version
280    pub schema_version: u32,
281    /// Export timestamp
282    pub exported_at: DateTime<Utc>,
283    /// Anonymized aggregates only
284    pub aggregates: AggregatedMetrics,
285    /// Differential privacy epsilon used
286    pub dp_epsilon: f64,
287    /// Hash of contributing user (for dedup)
288    pub contributor_hash: String,
289}
290
291#[cfg(test)]
292mod tests {
293    use super::*;
294
295    #[tokio::test]
296    async fn test_telemetry_disabled() {
297        let config = TelemetryConfig {
298            enabled: false,
299            ..Default::default()
300        };
301
302        let collector = TelemetryCollector::new(config).await.unwrap();
303        assert!(!collector.is_enabled());
304    }
305
306    #[tokio::test]
307    async fn test_session_id_generation() {
308        let config = TelemetryConfig::default();
309        let collector = TelemetryCollector::new(config).await.unwrap();
310
311        // Session ID should be a valid UUID
312        let _ = collector.session_id();
313    }
314}