oxify_vector/
metrics.rs

1//! Observability and Metrics
2//!
3//! This module provides metrics collection and tracking for vector search operations.
4//!
5//! ## Features
6//!
7//! - **Search Latency**: Track p50, p95, p99 latencies
8//! - **Queries Per Second (QPS)**: Track query throughput
9//! - **Index Health**: Monitor index size and build time
10//! - **Thread-safe**: Metrics can be collected from multiple threads
11//!
12//! ## Example
13//!
14//! ```rust
15//! use oxify_vector::metrics::Metrics;
16//! use std::time::Duration;
17//!
18//! # fn example() {
19//! let metrics = Metrics::new();
20//!
21//! // Record search latency
22//! metrics.record_search_latency(Duration::from_micros(150));
23//! metrics.record_search_latency(Duration::from_micros(200));
24//!
25//! // Get statistics
26//! let stats = metrics.get_search_stats();
27//! println!("p50 latency: {:?}", stats.p50_latency);
28//! println!("QPS: {:.2}", stats.qps);
29//! # }
30//! ```
31
32use std::sync::{Arc, Mutex};
33use std::time::{Duration, Instant};
34
35/// Search metrics statistics
36#[derive(Debug, Clone)]
37pub struct SearchStats {
38    /// Total number of queries
39    pub total_queries: u64,
40    /// Queries per second
41    pub qps: f64,
42    /// p50 (median) latency
43    pub p50_latency: Duration,
44    /// p95 latency
45    pub p95_latency: Duration,
46    /// p99 latency
47    pub p99_latency: Duration,
48    /// Average latency
49    pub avg_latency: Duration,
50    /// Minimum latency
51    pub min_latency: Duration,
52    /// Maximum latency
53    pub max_latency: Duration,
54}
55
56impl Default for SearchStats {
57    fn default() -> Self {
58        Self {
59            total_queries: 0,
60            qps: 0.0,
61            p50_latency: Duration::ZERO,
62            p95_latency: Duration::ZERO,
63            p99_latency: Duration::ZERO,
64            avg_latency: Duration::ZERO,
65            min_latency: Duration::MAX,
66            max_latency: Duration::ZERO,
67        }
68    }
69}
70
71/// Index metrics statistics
72#[derive(Debug, Clone)]
73pub struct IndexStats {
74    /// Number of vectors in index
75    pub num_vectors: usize,
76    /// Vector dimension
77    pub dimensions: usize,
78    /// Index build time
79    pub build_time: Duration,
80    /// Memory usage estimate (bytes)
81    pub memory_bytes: usize,
82}
83
84impl Default for IndexStats {
85    fn default() -> Self {
86        Self {
87            num_vectors: 0,
88            dimensions: 0,
89            build_time: Duration::ZERO,
90            memory_bytes: 0,
91        }
92    }
93}
94
95/// Thread-safe metrics collector
96#[derive(Clone)]
97pub struct Metrics {
98    search_metrics: Arc<Mutex<SearchMetrics>>,
99    index_stats: Arc<Mutex<IndexStats>>,
100}
101
102impl Metrics {
103    /// Create a new metrics collector
104    pub fn new() -> Self {
105        Self {
106            search_metrics: Arc::new(Mutex::new(SearchMetrics::new())),
107            index_stats: Arc::new(Mutex::new(IndexStats::default())),
108        }
109    }
110
111    /// Record a search latency
112    pub fn record_search_latency(&self, latency: Duration) {
113        let mut metrics = self.search_metrics.lock().unwrap();
114        metrics.record_latency(latency);
115    }
116
117    /// Get search statistics
118    pub fn get_search_stats(&self) -> SearchStats {
119        let metrics = self.search_metrics.lock().unwrap();
120        metrics.compute_stats()
121    }
122
123    /// Set index statistics
124    pub fn set_index_stats(&self, stats: IndexStats) {
125        let mut index_stats = self.index_stats.lock().unwrap();
126        *index_stats = stats;
127    }
128
129    /// Get index statistics
130    pub fn get_index_stats(&self) -> IndexStats {
131        let index_stats = self.index_stats.lock().unwrap();
132        index_stats.clone()
133    }
134
135    /// Reset all metrics
136    pub fn reset(&self) {
137        let mut search_metrics = self.search_metrics.lock().unwrap();
138        *search_metrics = SearchMetrics::new();
139
140        let mut index_stats = self.index_stats.lock().unwrap();
141        *index_stats = IndexStats::default();
142    }
143}
144
145impl Default for Metrics {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151/// Internal search metrics collector
152struct SearchMetrics {
153    latencies: Vec<Duration>,
154    start_time: Instant,
155}
156
157impl SearchMetrics {
158    fn new() -> Self {
159        Self {
160            latencies: Vec::new(),
161            start_time: Instant::now(),
162        }
163    }
164
165    fn record_latency(&mut self, latency: Duration) {
166        self.latencies.push(latency);
167    }
168
169    fn compute_stats(&self) -> SearchStats {
170        if self.latencies.is_empty() {
171            return SearchStats::default();
172        }
173
174        let mut sorted_latencies = self.latencies.clone();
175        sorted_latencies.sort();
176
177        let total_queries = sorted_latencies.len() as u64;
178
179        // Calculate percentiles (using linear interpolation at percentile boundary)
180        let p50_idx = ((total_queries as f64 * 0.50).ceil() as usize).saturating_sub(1);
181        let p95_idx = ((total_queries as f64 * 0.95).ceil() as usize).saturating_sub(1);
182        let p99_idx = ((total_queries as f64 * 0.99).ceil() as usize).saturating_sub(1);
183
184        let p50_latency = sorted_latencies
185            .get(p50_idx)
186            .copied()
187            .unwrap_or(Duration::ZERO);
188        let p95_latency = sorted_latencies
189            .get(p95_idx)
190            .copied()
191            .unwrap_or(Duration::ZERO);
192        let p99_latency = sorted_latencies
193            .get(p99_idx)
194            .copied()
195            .unwrap_or(Duration::ZERO);
196
197        // Calculate average
198        let total_micros: u128 = sorted_latencies.iter().map(|d| d.as_micros()).sum();
199        let avg_micros = total_micros / total_queries as u128;
200        let avg_latency = Duration::from_micros(avg_micros as u64);
201
202        // Calculate min and max
203        let min_latency = *sorted_latencies.first().unwrap();
204        let max_latency = *sorted_latencies.last().unwrap();
205
206        // Calculate QPS
207        let elapsed = self.start_time.elapsed().as_secs_f64();
208        let qps = if elapsed > 0.0 {
209            total_queries as f64 / elapsed
210        } else {
211            0.0
212        };
213
214        SearchStats {
215            total_queries,
216            qps,
217            p50_latency,
218            p95_latency,
219            p99_latency,
220            avg_latency,
221            min_latency,
222            max_latency,
223        }
224    }
225}
226
227/// Helper to measure search latency
228pub struct LatencyTimer {
229    start: Instant,
230    metrics: Option<Metrics>,
231}
232
233impl LatencyTimer {
234    /// Create a new latency timer
235    pub fn new(metrics: Option<Metrics>) -> Self {
236        Self {
237            start: Instant::now(),
238            metrics,
239        }
240    }
241
242    /// Finish timing and record the latency
243    pub fn finish(self) -> Duration {
244        let latency = self.start.elapsed();
245        if let Some(metrics) = self.metrics {
246            metrics.record_search_latency(latency);
247        }
248        latency
249    }
250
251    /// Get elapsed time without finishing
252    pub fn elapsed(&self) -> Duration {
253        self.start.elapsed()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use std::thread;
261    use std::time::Duration;
262
263    #[test]
264    fn test_metrics_creation() {
265        let metrics = Metrics::new();
266        let stats = metrics.get_search_stats();
267        assert_eq!(stats.total_queries, 0);
268        assert_eq!(stats.qps, 0.0);
269    }
270
271    #[test]
272    fn test_record_search_latency() {
273        let metrics = Metrics::new();
274
275        metrics.record_search_latency(Duration::from_micros(100));
276        metrics.record_search_latency(Duration::from_micros(200));
277        metrics.record_search_latency(Duration::from_micros(300));
278
279        let stats = metrics.get_search_stats();
280        assert_eq!(stats.total_queries, 3);
281        assert_eq!(stats.min_latency, Duration::from_micros(100));
282        assert_eq!(stats.max_latency, Duration::from_micros(300));
283        assert_eq!(stats.p50_latency, Duration::from_micros(200));
284    }
285
286    #[test]
287    fn test_percentiles() {
288        let metrics = Metrics::new();
289
290        // Record 100 samples: 1μs, 2μs, ..., 100μs
291        for i in 1..=100 {
292            metrics.record_search_latency(Duration::from_micros(i));
293        }
294
295        let stats = metrics.get_search_stats();
296        assert_eq!(stats.total_queries, 100);
297
298        // p50 should be around 50μs
299        assert!(stats.p50_latency >= Duration::from_micros(49));
300        assert!(stats.p50_latency <= Duration::from_micros(51));
301
302        // p95 should be around 95μs
303        assert!(stats.p95_latency >= Duration::from_micros(94));
304        assert!(stats.p95_latency <= Duration::from_micros(96));
305
306        // p99 should be around 99μs
307        assert!(stats.p99_latency >= Duration::from_micros(98));
308        assert!(stats.p99_latency <= Duration::from_micros(100));
309    }
310
311    #[test]
312    fn test_average_latency() {
313        let metrics = Metrics::new();
314
315        metrics.record_search_latency(Duration::from_micros(100));
316        metrics.record_search_latency(Duration::from_micros(200));
317        metrics.record_search_latency(Duration::from_micros(300));
318
319        let stats = metrics.get_search_stats();
320        assert_eq!(stats.avg_latency, Duration::from_micros(200));
321    }
322
323    #[test]
324    fn test_qps_calculation() {
325        let metrics = Metrics::new();
326
327        // Record some queries
328        for _ in 0..10 {
329            metrics.record_search_latency(Duration::from_micros(100));
330        }
331
332        // Wait a bit
333        thread::sleep(Duration::from_millis(100));
334
335        let stats = metrics.get_search_stats();
336        assert!(stats.qps > 0.0);
337        assert_eq!(stats.total_queries, 10);
338    }
339
340    #[test]
341    fn test_index_stats() {
342        let metrics = Metrics::new();
343
344        let index_stats = IndexStats {
345            num_vectors: 1000,
346            dimensions: 768,
347            build_time: Duration::from_secs(5),
348            memory_bytes: 1024 * 1024, // 1MB
349        };
350
351        metrics.set_index_stats(index_stats.clone());
352
353        let retrieved = metrics.get_index_stats();
354        assert_eq!(retrieved.num_vectors, 1000);
355        assert_eq!(retrieved.dimensions, 768);
356        assert_eq!(retrieved.build_time, Duration::from_secs(5));
357        assert_eq!(retrieved.memory_bytes, 1024 * 1024);
358    }
359
360    #[test]
361    fn test_metrics_reset() {
362        let metrics = Metrics::new();
363
364        // Add some data
365        metrics.record_search_latency(Duration::from_micros(100));
366        metrics.record_search_latency(Duration::from_micros(200));
367
368        let stats = metrics.get_search_stats();
369        assert_eq!(stats.total_queries, 2);
370
371        // Reset
372        metrics.reset();
373
374        let stats = metrics.get_search_stats();
375        assert_eq!(stats.total_queries, 0);
376    }
377
378    #[test]
379    fn test_latency_timer() {
380        let metrics = Metrics::new();
381
382        let timer = LatencyTimer::new(Some(metrics.clone()));
383        thread::sleep(Duration::from_millis(10));
384        let latency = timer.finish();
385
386        assert!(latency >= Duration::from_millis(10));
387
388        let stats = metrics.get_search_stats();
389        assert_eq!(stats.total_queries, 1);
390        assert!(stats.min_latency >= Duration::from_millis(10));
391    }
392
393    #[test]
394    fn test_latency_timer_without_metrics() {
395        let timer = LatencyTimer::new(None);
396        thread::sleep(Duration::from_millis(5));
397        let latency = timer.finish();
398
399        assert!(latency >= Duration::from_millis(5));
400    }
401
402    #[test]
403    fn test_latency_timer_elapsed() {
404        let timer = LatencyTimer::new(None);
405        thread::sleep(Duration::from_millis(5));
406        let elapsed = timer.elapsed();
407
408        assert!(elapsed >= Duration::from_millis(5));
409    }
410
411    #[test]
412    fn test_thread_safety() {
413        let metrics = Metrics::new();
414        let metrics_clone = metrics.clone();
415
416        let handle = thread::spawn(move || {
417            for _ in 0..100 {
418                metrics_clone.record_search_latency(Duration::from_micros(100));
419            }
420        });
421
422        for _ in 0..100 {
423            metrics.record_search_latency(Duration::from_micros(200));
424        }
425
426        handle.join().unwrap();
427
428        let stats = metrics.get_search_stats();
429        assert_eq!(stats.total_queries, 200);
430    }
431}