Skip to main content

ipfrs_storage/
metrics.rs

1//! Storage metrics and observability
2//!
3//! This module provides comprehensive metrics tracking for storage operations,
4//! enabling production monitoring and performance analysis.
5
6use crate::traits::BlockStore;
7use async_trait::async_trait;
8use ipfrs_core::{Block, Cid, Result};
9use serde::{Deserialize, Serialize};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14/// Storage operation metrics
15#[derive(Debug, Clone, Default, Serialize, Deserialize)]
16pub struct StorageMetrics {
17    /// Total number of put operations
18    pub put_count: u64,
19    /// Total number of get operations
20    pub get_count: u64,
21    /// Total number of has operations
22    pub has_count: u64,
23    /// Total number of delete operations
24    pub delete_count: u64,
25    /// Total number of successful gets (cache hits + disk hits)
26    pub get_hits: u64,
27    /// Total number of failed gets (not found)
28    pub get_misses: u64,
29    /// Total bytes written
30    pub bytes_written: u64,
31    /// Total bytes read
32    pub bytes_read: u64,
33    /// Average put latency in microseconds
34    pub avg_put_latency_us: u64,
35    /// Average get latency in microseconds
36    pub avg_get_latency_us: u64,
37    /// Average has latency in microseconds
38    pub avg_has_latency_us: u64,
39    /// Peak put latency in microseconds
40    pub peak_put_latency_us: u64,
41    /// Peak get latency in microseconds
42    pub peak_get_latency_us: u64,
43    /// Number of errors encountered
44    pub error_count: u64,
45    /// Total number of batch operations (put_many, get_many, etc.)
46    pub batch_op_count: u64,
47    /// Total number of items in batch operations
48    pub batch_items_count: u64,
49    /// Average batch size (items per batch)
50    pub avg_batch_size: u64,
51}
52
53impl StorageMetrics {
54    /// Calculate cache hit rate (0.0 to 1.0)
55    pub fn cache_hit_rate(&self) -> f64 {
56        let total = self.get_hits + self.get_misses;
57        if total == 0 {
58            0.0
59        } else {
60            self.get_hits as f64 / total as f64
61        }
62    }
63
64    /// Calculate average operation latency
65    pub fn avg_operation_latency_us(&self) -> u64 {
66        let total_ops = self.put_count + self.get_count + self.has_count;
67        let total_latency = (self.put_count * self.avg_put_latency_us)
68            + (self.get_count * self.avg_get_latency_us)
69            + (self.has_count * self.avg_has_latency_us);
70        total_latency.checked_div(total_ops).unwrap_or(0)
71    }
72
73    /// Calculate throughput in operations per second
74    pub fn ops_per_second(&self, duration: Duration) -> f64 {
75        let total_ops = self.put_count + self.get_count + self.has_count + self.delete_count;
76        let seconds = duration.as_secs_f64();
77        if seconds > 0.0 {
78            total_ops as f64 / seconds
79        } else {
80            0.0
81        }
82    }
83
84    /// Calculate batch efficiency (percentage of operations that were batched)
85    pub fn batch_efficiency(&self) -> f64 {
86        let total_ops = self.put_count + self.get_count + self.has_count + self.delete_count;
87        if total_ops == 0 {
88            0.0
89        } else {
90            self.batch_items_count as f64 / total_ops as f64
91        }
92    }
93
94    /// Calculate write throughput in bytes per second
95    pub fn write_throughput_bps(&self, duration: Duration) -> f64 {
96        let seconds = duration.as_secs_f64();
97        if seconds > 0.0 {
98            self.bytes_written as f64 / seconds
99        } else {
100            0.0
101        }
102    }
103
104    /// Calculate read throughput in bytes per second
105    pub fn read_throughput_bps(&self, duration: Duration) -> f64 {
106        let seconds = duration.as_secs_f64();
107        if seconds > 0.0 {
108            self.bytes_read as f64 / seconds
109        } else {
110            0.0
111        }
112    }
113}
114
115/// Internal metrics collector
116struct MetricsCollector {
117    put_count: AtomicU64,
118    get_count: AtomicU64,
119    has_count: AtomicU64,
120    delete_count: AtomicU64,
121    get_hits: AtomicU64,
122    get_misses: AtomicU64,
123    bytes_written: AtomicU64,
124    bytes_read: AtomicU64,
125    put_latency_sum: AtomicU64,
126    get_latency_sum: AtomicU64,
127    has_latency_sum: AtomicU64,
128    peak_put_latency: AtomicU64,
129    peak_get_latency: AtomicU64,
130    error_count: AtomicU64,
131    batch_op_count: AtomicU64,
132    batch_items_count: AtomicU64,
133    start_time: Instant,
134}
135
136impl Default for MetricsCollector {
137    fn default() -> Self {
138        Self {
139            put_count: AtomicU64::new(0),
140            get_count: AtomicU64::new(0),
141            has_count: AtomicU64::new(0),
142            delete_count: AtomicU64::new(0),
143            get_hits: AtomicU64::new(0),
144            get_misses: AtomicU64::new(0),
145            bytes_written: AtomicU64::new(0),
146            bytes_read: AtomicU64::new(0),
147            put_latency_sum: AtomicU64::new(0),
148            get_latency_sum: AtomicU64::new(0),
149            has_latency_sum: AtomicU64::new(0),
150            peak_put_latency: AtomicU64::new(0),
151            peak_get_latency: AtomicU64::new(0),
152            error_count: AtomicU64::new(0),
153            batch_op_count: AtomicU64::new(0),
154            batch_items_count: AtomicU64::new(0),
155            start_time: Instant::now(),
156        }
157    }
158}
159
160impl MetricsCollector {
161    fn snapshot(&self) -> StorageMetrics {
162        let put_count = self.put_count.load(Ordering::Relaxed);
163        let get_count = self.get_count.load(Ordering::Relaxed);
164        let has_count = self.has_count.load(Ordering::Relaxed);
165        let batch_op_count = self.batch_op_count.load(Ordering::Relaxed);
166        let batch_items_count = self.batch_items_count.load(Ordering::Relaxed);
167
168        StorageMetrics {
169            put_count,
170            get_count,
171            has_count,
172            delete_count: self.delete_count.load(Ordering::Relaxed),
173            get_hits: self.get_hits.load(Ordering::Relaxed),
174            get_misses: self.get_misses.load(Ordering::Relaxed),
175            bytes_written: self.bytes_written.load(Ordering::Relaxed),
176            bytes_read: self.bytes_read.load(Ordering::Relaxed),
177            avg_put_latency_us: self
178                .put_latency_sum
179                .load(Ordering::Relaxed)
180                .checked_div(put_count)
181                .unwrap_or(0),
182            avg_get_latency_us: self
183                .get_latency_sum
184                .load(Ordering::Relaxed)
185                .checked_div(get_count)
186                .unwrap_or(0),
187            avg_has_latency_us: self
188                .has_latency_sum
189                .load(Ordering::Relaxed)
190                .checked_div(has_count)
191                .unwrap_or(0),
192            peak_put_latency_us: self.peak_put_latency.load(Ordering::Relaxed),
193            peak_get_latency_us: self.peak_get_latency.load(Ordering::Relaxed),
194            error_count: self.error_count.load(Ordering::Relaxed),
195            batch_op_count,
196            batch_items_count,
197            avg_batch_size: batch_items_count.checked_div(batch_op_count).unwrap_or(0),
198        }
199    }
200
201    fn record_put(&self, bytes: u64, latency_us: u64) {
202        self.put_count.fetch_add(1, Ordering::Relaxed);
203        self.bytes_written.fetch_add(bytes, Ordering::Relaxed);
204        self.put_latency_sum
205            .fetch_add(latency_us, Ordering::Relaxed);
206
207        let mut current_peak = self.peak_put_latency.load(Ordering::Relaxed);
208        while latency_us > current_peak {
209            match self.peak_put_latency.compare_exchange_weak(
210                current_peak,
211                latency_us,
212                Ordering::Relaxed,
213                Ordering::Relaxed,
214            ) {
215                Ok(_) => break,
216                Err(x) => current_peak = x,
217            }
218        }
219    }
220
221    fn record_get(&self, bytes: Option<u64>, latency_us: u64) {
222        self.get_count.fetch_add(1, Ordering::Relaxed);
223
224        if let Some(bytes) = bytes {
225            self.get_hits.fetch_add(1, Ordering::Relaxed);
226            self.bytes_read.fetch_add(bytes, Ordering::Relaxed);
227        } else {
228            self.get_misses.fetch_add(1, Ordering::Relaxed);
229        }
230
231        self.get_latency_sum
232            .fetch_add(latency_us, Ordering::Relaxed);
233
234        let mut current_peak = self.peak_get_latency.load(Ordering::Relaxed);
235        while latency_us > current_peak {
236            match self.peak_get_latency.compare_exchange_weak(
237                current_peak,
238                latency_us,
239                Ordering::Relaxed,
240                Ordering::Relaxed,
241            ) {
242                Ok(_) => break,
243                Err(x) => current_peak = x,
244            }
245        }
246    }
247
248    fn record_has(&self, latency_us: u64) {
249        self.has_count.fetch_add(1, Ordering::Relaxed);
250        self.has_latency_sum
251            .fetch_add(latency_us, Ordering::Relaxed);
252    }
253
254    fn record_delete(&self) {
255        self.delete_count.fetch_add(1, Ordering::Relaxed);
256    }
257
258    fn record_error(&self) {
259        self.error_count.fetch_add(1, Ordering::Relaxed);
260    }
261
262    fn record_batch(&self, batch_size: usize) {
263        self.batch_op_count.fetch_add(1, Ordering::Relaxed);
264        self.batch_items_count
265            .fetch_add(batch_size as u64, Ordering::Relaxed);
266    }
267
268    fn uptime(&self) -> Duration {
269        self.start_time.elapsed()
270    }
271
272    fn reset(&self) {
273        self.put_count.store(0, Ordering::Relaxed);
274        self.get_count.store(0, Ordering::Relaxed);
275        self.has_count.store(0, Ordering::Relaxed);
276        self.delete_count.store(0, Ordering::Relaxed);
277        self.get_hits.store(0, Ordering::Relaxed);
278        self.get_misses.store(0, Ordering::Relaxed);
279        self.bytes_written.store(0, Ordering::Relaxed);
280        self.bytes_read.store(0, Ordering::Relaxed);
281        self.put_latency_sum.store(0, Ordering::Relaxed);
282        self.get_latency_sum.store(0, Ordering::Relaxed);
283        self.has_latency_sum.store(0, Ordering::Relaxed);
284        self.peak_put_latency.store(0, Ordering::Relaxed);
285        self.peak_get_latency.store(0, Ordering::Relaxed);
286        self.error_count.store(0, Ordering::Relaxed);
287        self.batch_op_count.store(0, Ordering::Relaxed);
288        self.batch_items_count.store(0, Ordering::Relaxed);
289    }
290}
291
292/// Block store with metrics tracking
293pub struct MetricsBlockStore<S: BlockStore> {
294    inner: S,
295    metrics: Arc<MetricsCollector>,
296}
297
298impl<S: BlockStore> MetricsBlockStore<S> {
299    /// Create a new metrics-enabled block store
300    pub fn new(inner: S) -> Self {
301        Self {
302            inner,
303            metrics: Arc::new(MetricsCollector::default()),
304        }
305    }
306
307    /// Get current metrics snapshot
308    pub fn metrics(&self) -> StorageMetrics {
309        self.metrics.snapshot()
310    }
311
312    /// Get uptime duration
313    pub fn uptime(&self) -> Duration {
314        self.metrics.uptime()
315    }
316
317    /// Reset all metrics counters
318    ///
319    /// This resets all counters to zero while keeping the store running.
320    /// The start time is not reset, so uptime() will continue from the original start.
321    pub fn reset_metrics(&self) {
322        self.metrics.reset();
323    }
324
325    /// Get the inner store
326    pub fn inner(&self) -> &S {
327        &self.inner
328    }
329
330    /// Consume this store and return the inner store
331    pub fn into_inner(self) -> S {
332        self.inner
333    }
334}
335
336#[async_trait]
337impl<S: BlockStore> BlockStore for MetricsBlockStore<S> {
338    async fn put(&self, block: &Block) -> Result<()> {
339        let start = Instant::now();
340        let result = self.inner.put(block).await;
341        let latency_us = start.elapsed().as_micros() as u64;
342
343        match &result {
344            Ok(_) => {
345                self.metrics
346                    .record_put(block.data().len() as u64, latency_us);
347            }
348            Err(_) => {
349                self.metrics.record_error();
350            }
351        }
352
353        result
354    }
355
356    async fn put_many(&self, blocks: &[Block]) -> Result<()> {
357        let start = Instant::now();
358        let result = self.inner.put_many(blocks).await;
359        let latency_us = start.elapsed().as_micros() as u64;
360
361        match &result {
362            Ok(_) => {
363                // Record batch operation
364                self.metrics.record_batch(blocks.len());
365                // Record as individual puts for metrics
366                let avg_latency = latency_us / blocks.len().max(1) as u64;
367                for block in blocks {
368                    self.metrics
369                        .record_put(block.data().len() as u64, avg_latency);
370                }
371            }
372            Err(_) => {
373                self.metrics.record_error();
374            }
375        }
376
377        result
378    }
379
380    async fn get(&self, cid: &Cid) -> Result<Option<Block>> {
381        let start = Instant::now();
382        let result = self.inner.get(cid).await;
383        let latency_us = start.elapsed().as_micros() as u64;
384
385        match &result {
386            Ok(Some(block)) => {
387                self.metrics
388                    .record_get(Some(block.data().len() as u64), latency_us);
389            }
390            Ok(None) => {
391                self.metrics.record_get(None, latency_us);
392            }
393            Err(_) => {
394                self.metrics.record_error();
395            }
396        }
397
398        result
399    }
400
401    async fn get_many(&self, cids: &[Cid]) -> Result<Vec<Option<Block>>> {
402        let start = Instant::now();
403        let result = self.inner.get_many(cids).await;
404        let latency_us = start.elapsed().as_micros() as u64;
405
406        match &result {
407            Ok(blocks) => {
408                // Record batch operation
409                self.metrics.record_batch(blocks.len());
410                let avg_latency = latency_us / blocks.len().max(1) as u64;
411                for block in blocks {
412                    match block {
413                        Some(b) => {
414                            self.metrics
415                                .record_get(Some(b.data().len() as u64), avg_latency);
416                        }
417                        None => {
418                            self.metrics.record_get(None, avg_latency);
419                        }
420                    }
421                }
422            }
423            Err(_) => {
424                self.metrics.record_error();
425            }
426        }
427
428        result
429    }
430
431    async fn has(&self, cid: &Cid) -> Result<bool> {
432        let start = Instant::now();
433        let result = self.inner.has(cid).await;
434        let latency_us = start.elapsed().as_micros() as u64;
435
436        match &result {
437            Ok(_) => {
438                self.metrics.record_has(latency_us);
439            }
440            Err(_) => {
441                self.metrics.record_error();
442            }
443        }
444
445        result
446    }
447
448    async fn has_many(&self, cids: &[Cid]) -> Result<Vec<bool>> {
449        let start = Instant::now();
450        let result = self.inner.has_many(cids).await;
451        let latency_us = start.elapsed().as_micros() as u64;
452
453        match &result {
454            Ok(results) => {
455                // Record batch operation
456                self.metrics.record_batch(results.len());
457                let avg_latency = latency_us / results.len().max(1) as u64;
458                for _ in results {
459                    self.metrics.record_has(avg_latency);
460                }
461            }
462            Err(_) => {
463                self.metrics.record_error();
464            }
465        }
466
467        result
468    }
469
470    async fn delete(&self, cid: &Cid) -> Result<()> {
471        let result = self.inner.delete(cid).await;
472
473        match &result {
474            Ok(_) => {
475                self.metrics.record_delete();
476            }
477            Err(_) => {
478                self.metrics.record_error();
479            }
480        }
481
482        result
483    }
484
485    async fn delete_many(&self, cids: &[Cid]) -> Result<()> {
486        let result = self.inner.delete_many(cids).await;
487
488        match &result {
489            Ok(_) => {
490                // Record batch operation
491                self.metrics.record_batch(cids.len());
492                for _ in cids {
493                    self.metrics.record_delete();
494                }
495            }
496            Err(_) => {
497                self.metrics.record_error();
498            }
499        }
500
501        result
502    }
503
504    fn list_cids(&self) -> Result<Vec<Cid>> {
505        self.inner.list_cids()
506    }
507
508    fn len(&self) -> usize {
509        self.inner.len()
510    }
511
512    fn is_empty(&self) -> bool {
513        self.inner.is_empty()
514    }
515
516    async fn flush(&self) -> Result<()> {
517        self.inner.flush().await
518    }
519
520    async fn close(&self) -> Result<()> {
521        self.inner.close().await
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528    use crate::MemoryBlockStore;
529    use bytes::Bytes;
530
531    #[tokio::test]
532    async fn test_metrics_tracking() {
533        let store = MemoryBlockStore::new();
534        let metrics_store = MetricsBlockStore::new(store);
535
536        // Put a block
537        let block = Block::new(Bytes::from("test data")).unwrap();
538        metrics_store.put(&block).await.unwrap();
539
540        let metrics = metrics_store.metrics();
541        assert_eq!(metrics.put_count, 1);
542        assert_eq!(metrics.bytes_written, 9); // "test data" is 9 bytes
543
544        // Get the block
545        let retrieved = metrics_store.get(block.cid()).await.unwrap();
546        assert!(retrieved.is_some());
547
548        let metrics = metrics_store.metrics();
549        assert_eq!(metrics.get_count, 1);
550        assert_eq!(metrics.get_hits, 1);
551        assert_eq!(metrics.get_misses, 0);
552        assert_eq!(metrics.bytes_read, 9);
553
554        // Check cache hit rate
555        assert_eq!(metrics.cache_hit_rate(), 1.0);
556    }
557
558    #[tokio::test]
559    async fn test_metrics_cache_miss() {
560        let store = MemoryBlockStore::new();
561        let metrics_store = MetricsBlockStore::new(store);
562
563        // Try to get non-existent block
564        let fake_block = Block::new(Bytes::from("fake")).unwrap();
565        let result = metrics_store.get(fake_block.cid()).await.unwrap();
566        assert!(result.is_none());
567
568        let metrics = metrics_store.metrics();
569        assert_eq!(metrics.get_count, 1);
570        assert_eq!(metrics.get_hits, 0);
571        assert_eq!(metrics.get_misses, 1);
572        assert_eq!(metrics.cache_hit_rate(), 0.0);
573    }
574
575    #[tokio::test]
576    async fn test_metrics_latency_tracking() {
577        let store = MemoryBlockStore::new();
578        let metrics_store = MetricsBlockStore::new(store);
579
580        // Put some blocks with small delays to ensure measurable latency
581        for i in 0..5 {
582            let block = Block::new(Bytes::from(format!("block {}", i))).unwrap();
583            // Add small delay to ensure latency is measurable in microseconds
584            tokio::time::sleep(std::time::Duration::from_micros(10)).await;
585            metrics_store.put(&block).await.unwrap();
586        }
587
588        let metrics = metrics_store.metrics();
589        assert_eq!(metrics.put_count, 5);
590        assert!(metrics.avg_put_latency_us > 0);
591        assert!(metrics.peak_put_latency_us > 0);
592    }
593
594    #[test]
595    fn test_storage_metrics_calculations() {
596        let metrics = StorageMetrics {
597            put_count: 100,
598            get_count: 200,
599            has_count: 50,
600            delete_count: 10,
601            get_hits: 180,
602            get_misses: 20,
603            bytes_written: 10000,
604            bytes_read: 18000,
605            avg_put_latency_us: 100,
606            avg_get_latency_us: 50,
607            avg_has_latency_us: 10,
608            peak_put_latency_us: 500,
609            peak_get_latency_us: 200,
610            error_count: 5,
611            batch_op_count: 10,
612            batch_items_count: 50,
613            avg_batch_size: 5,
614        };
615
616        // Test cache hit rate
617        assert_eq!(metrics.cache_hit_rate(), 0.9); // 180/200 = 0.9
618
619        // Test average operation latency
620        let avg_latency = metrics.avg_operation_latency_us();
621        let expected = (100 * 100 + 200 * 50 + 50 * 10) / 350;
622        assert_eq!(avg_latency, expected);
623
624        // Test ops per second
625        let duration = Duration::from_secs(10);
626        let ops_per_sec = metrics.ops_per_second(duration);
627        assert_eq!(ops_per_sec, 36.0); // (100 + 200 + 50 + 10) / 10 = 36
628    }
629}