Skip to main content

ipfrs_storage/
event_log.rs

1//! Structured event logging for storage operations.
2//!
3//! Provides a bounded, queryable event log that tracks storage operations
4//! such as block additions, deletions, compaction events, quota violations,
5//! and errors. Events are tagged with type, severity, and logical tick,
6//! enabling efficient filtering and diagnostics.
7
8use std::collections::{HashMap, VecDeque};
9
10/// The kind of storage operation that generated an event.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum EventType {
13    /// A new block was added to storage.
14    BlockAdded,
15    /// A block was deleted from storage.
16    BlockDeleted,
17    /// A block was read / accessed.
18    BlockAccessed,
19    /// A compaction cycle started.
20    CompactionStarted,
21    /// A compaction cycle completed.
22    CompactionCompleted,
23    /// A quota limit was exceeded.
24    QuotaExceeded,
25    /// An error occurred during a storage operation.
26    Error,
27}
28
29/// Severity level of a storage event.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum EventSeverity {
32    /// Informational – normal operation.
33    Info,
34    /// Warning – something unusual but not fatal.
35    Warning,
36    /// Error – an operation failed.
37    Error,
38    /// Critical – requires immediate attention.
39    Critical,
40}
41
42/// A single storage event record.
43#[derive(Debug, Clone)]
44pub struct StorageEvent {
45    /// Unique, monotonically increasing event identifier.
46    pub id: u64,
47    /// The type of storage operation.
48    pub event_type: EventType,
49    /// Severity level.
50    pub severity: EventSeverity,
51    /// Human-readable description.
52    pub message: String,
53    /// Optional CID of the block involved.
54    pub block_cid: Option<String>,
55    /// Logical tick at which the event was recorded.
56    pub tick: u64,
57    /// Arbitrary key-value metadata.
58    pub metadata: HashMap<String, String>,
59}
60
61/// Aggregate statistics about the event log.
62#[derive(Debug, Clone)]
63pub struct EventLogStats {
64    /// Current number of events in the log.
65    pub total_events: usize,
66    /// Counts keyed by severity name.
67    pub events_by_severity: HashMap<String, u64>,
68    /// Tick of the oldest event, if any.
69    pub oldest_tick: Option<u64>,
70    /// Tick of the newest event, if any.
71    pub newest_tick: Option<u64>,
72}
73
74/// A bounded, queryable log of [`StorageEvent`]s.
75///
76/// When the number of stored events exceeds `max_events`, the oldest
77/// event is evicted on each new insertion.
78pub struct StorageEventLog {
79    events: VecDeque<StorageEvent>,
80    max_events: usize,
81    next_id: u64,
82    current_tick: u64,
83    counts_by_type: HashMap<EventType, u64>,
84}
85
86impl StorageEventLog {
87    /// Create a new event log that retains at most `max_events` entries.
88    pub fn new(max_events: usize) -> Self {
89        Self {
90            events: VecDeque::new(),
91            max_events,
92            next_id: 0,
93            current_tick: 0,
94            counts_by_type: HashMap::new(),
95        }
96    }
97
98    /// Append a new event and return its unique id.
99    ///
100    /// If the log has reached `max_events`, the oldest event is evicted first.
101    pub fn log(
102        &mut self,
103        event_type: EventType,
104        severity: EventSeverity,
105        message: &str,
106        block_cid: Option<&str>,
107        metadata: HashMap<String, String>,
108    ) -> u64 {
109        let id = self.next_id;
110        self.next_id = self.next_id.saturating_add(1);
111
112        let event = StorageEvent {
113            id,
114            event_type,
115            severity,
116            message: message.to_string(),
117            block_cid: block_cid.map(|s| s.to_string()),
118            tick: self.current_tick,
119            metadata,
120        };
121
122        // Evict oldest if at capacity.
123        if self.events.len() >= self.max_events {
124            self.events.pop_front();
125        }
126
127        *self.counts_by_type.entry(event_type).or_insert(0) += 1;
128        self.events.push_back(event);
129
130        id
131    }
132
133    /// Look up an event by its id.
134    pub fn get_event(&self, id: u64) -> Option<&StorageEvent> {
135        self.events.iter().find(|e| e.id == id)
136    }
137
138    /// Return all events matching the given [`EventType`].
139    pub fn events_by_type(&self, event_type: EventType) -> Vec<&StorageEvent> {
140        self.events
141            .iter()
142            .filter(|e| e.event_type == event_type)
143            .collect()
144    }
145
146    /// Return all events matching the given [`EventSeverity`].
147    pub fn events_by_severity(&self, severity: EventSeverity) -> Vec<&StorageEvent> {
148        self.events
149            .iter()
150            .filter(|e| e.severity == severity)
151            .collect()
152    }
153
154    /// Return all events whose tick is strictly greater than `tick`.
155    pub fn events_since(&self, tick: u64) -> Vec<&StorageEvent> {
156        self.events.iter().filter(|e| e.tick > tick).collect()
157    }
158
159    /// Return the cumulative count of events of the given type
160    /// (including evicted events).
161    pub fn count_by_type(&self, event_type: EventType) -> u64 {
162        self.counts_by_type.get(&event_type).copied().unwrap_or(0)
163    }
164
165    /// Return the last `n` events (or fewer if the log is smaller).
166    pub fn recent(&self, n: usize) -> Vec<&StorageEvent> {
167        let len = self.events.len();
168        let skip = len.saturating_sub(n);
169        self.events.iter().skip(skip).collect()
170    }
171
172    /// Advance the logical tick by one.
173    pub fn tick(&mut self) {
174        self.current_tick = self.current_tick.saturating_add(1);
175    }
176
177    /// Remove all events from the log.
178    ///
179    /// Cumulative type counts and the current tick are preserved.
180    pub fn clear(&mut self) {
181        self.events.clear();
182    }
183
184    /// Return the number of events currently in the log.
185    pub fn event_count(&self) -> usize {
186        self.events.len()
187    }
188
189    /// Compute aggregate statistics over the current event log.
190    pub fn stats(&self) -> EventLogStats {
191        let mut events_by_severity: HashMap<String, u64> = HashMap::new();
192        for event in &self.events {
193            let key = format!("{:?}", event.severity);
194            *events_by_severity.entry(key).or_insert(0) += 1;
195        }
196
197        let oldest_tick = self.events.front().map(|e| e.tick);
198        let newest_tick = self.events.back().map(|e| e.tick);
199
200        EventLogStats {
201            total_events: self.events.len(),
202            events_by_severity,
203            oldest_tick,
204            newest_tick,
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn empty_meta() -> HashMap<String, String> {
214        HashMap::new()
215    }
216
217    // 1. Log a single event and verify id.
218    #[test]
219    fn test_log_returns_id() {
220        let mut log = StorageEventLog::new(100);
221        let id = log.log(
222            EventType::BlockAdded,
223            EventSeverity::Info,
224            "added",
225            None,
226            empty_meta(),
227        );
228        assert_eq!(id, 0);
229    }
230
231    // 2. Sequential ids.
232    #[test]
233    fn test_sequential_ids() {
234        let mut log = StorageEventLog::new(100);
235        let a = log.log(
236            EventType::BlockAdded,
237            EventSeverity::Info,
238            "a",
239            None,
240            empty_meta(),
241        );
242        let b = log.log(
243            EventType::BlockDeleted,
244            EventSeverity::Info,
245            "b",
246            None,
247            empty_meta(),
248        );
249        assert_eq!(a, 0);
250        assert_eq!(b, 1);
251    }
252
253    // 3. get_event returns correct event.
254    #[test]
255    fn test_get_event() {
256        let mut log = StorageEventLog::new(100);
257        let id = log.log(
258            EventType::BlockAccessed,
259            EventSeverity::Warning,
260            "access",
261            Some("Qm123"),
262            empty_meta(),
263        );
264        let evt = log.get_event(id).expect("event should exist");
265        assert_eq!(evt.event_type, EventType::BlockAccessed);
266        assert_eq!(evt.severity, EventSeverity::Warning);
267        assert_eq!(evt.message, "access");
268        assert_eq!(evt.block_cid.as_deref(), Some("Qm123"));
269    }
270
271    // 4. get_event returns None for missing id.
272    #[test]
273    fn test_get_event_missing() {
274        let log = StorageEventLog::new(100);
275        assert!(log.get_event(42).is_none());
276    }
277
278    // 5. events_by_type filters correctly.
279    #[test]
280    fn test_events_by_type() {
281        let mut log = StorageEventLog::new(100);
282        log.log(
283            EventType::BlockAdded,
284            EventSeverity::Info,
285            "a",
286            None,
287            empty_meta(),
288        );
289        log.log(
290            EventType::BlockDeleted,
291            EventSeverity::Info,
292            "b",
293            None,
294            empty_meta(),
295        );
296        log.log(
297            EventType::BlockAdded,
298            EventSeverity::Info,
299            "c",
300            None,
301            empty_meta(),
302        );
303        let added = log.events_by_type(EventType::BlockAdded);
304        assert_eq!(added.len(), 2);
305    }
306
307    // 6. events_by_severity filters correctly.
308    #[test]
309    fn test_events_by_severity() {
310        let mut log = StorageEventLog::new(100);
311        log.log(
312            EventType::Error,
313            EventSeverity::Error,
314            "err1",
315            None,
316            empty_meta(),
317        );
318        log.log(
319            EventType::BlockAdded,
320            EventSeverity::Info,
321            "ok",
322            None,
323            empty_meta(),
324        );
325        log.log(
326            EventType::Error,
327            EventSeverity::Error,
328            "err2",
329            None,
330            empty_meta(),
331        );
332        let errors = log.events_by_severity(EventSeverity::Error);
333        assert_eq!(errors.len(), 2);
334    }
335
336    // 7. events_since returns events after tick.
337    #[test]
338    fn test_events_since() {
339        let mut log = StorageEventLog::new(100);
340        log.log(
341            EventType::BlockAdded,
342            EventSeverity::Info,
343            "t0",
344            None,
345            empty_meta(),
346        );
347        log.tick();
348        log.log(
349            EventType::BlockAdded,
350            EventSeverity::Info,
351            "t1",
352            None,
353            empty_meta(),
354        );
355        log.tick();
356        log.log(
357            EventType::BlockAdded,
358            EventSeverity::Info,
359            "t2",
360            None,
361            empty_meta(),
362        );
363        let since = log.events_since(0);
364        assert_eq!(since.len(), 2);
365    }
366
367    // 8. count_by_type tracks cumulative counts.
368    #[test]
369    fn test_count_by_type() {
370        let mut log = StorageEventLog::new(100);
371        log.log(
372            EventType::BlockAdded,
373            EventSeverity::Info,
374            "a",
375            None,
376            empty_meta(),
377        );
378        log.log(
379            EventType::BlockAdded,
380            EventSeverity::Info,
381            "b",
382            None,
383            empty_meta(),
384        );
385        log.log(
386            EventType::BlockDeleted,
387            EventSeverity::Info,
388            "c",
389            None,
390            empty_meta(),
391        );
392        assert_eq!(log.count_by_type(EventType::BlockAdded), 2);
393        assert_eq!(log.count_by_type(EventType::BlockDeleted), 1);
394        assert_eq!(log.count_by_type(EventType::Error), 0);
395    }
396
397    // 9. recent returns last n events.
398    #[test]
399    fn test_recent() {
400        let mut log = StorageEventLog::new(100);
401        for i in 0..5 {
402            log.log(
403                EventType::BlockAdded,
404                EventSeverity::Info,
405                &format!("e{i}"),
406                None,
407                empty_meta(),
408            );
409        }
410        let r = log.recent(3);
411        assert_eq!(r.len(), 3);
412        assert_eq!(r[0].id, 2);
413        assert_eq!(r[2].id, 4);
414    }
415
416    // 10. recent with n > event_count.
417    #[test]
418    fn test_recent_exceeds_count() {
419        let mut log = StorageEventLog::new(100);
420        log.log(
421            EventType::BlockAdded,
422            EventSeverity::Info,
423            "only",
424            None,
425            empty_meta(),
426        );
427        let r = log.recent(10);
428        assert_eq!(r.len(), 1);
429    }
430
431    // 11. Max events eviction.
432    #[test]
433    fn test_max_events_eviction() {
434        let mut log = StorageEventLog::new(3);
435        for i in 0..5 {
436            log.log(
437                EventType::BlockAdded,
438                EventSeverity::Info,
439                &format!("e{i}"),
440                None,
441                empty_meta(),
442            );
443        }
444        assert_eq!(log.event_count(), 3);
445        // Oldest remaining should be id=2.
446        assert!(log.get_event(0).is_none());
447        assert!(log.get_event(1).is_none());
448        assert!(log.get_event(2).is_some());
449    }
450
451    // 12. Eviction preserves cumulative counts.
452    #[test]
453    fn test_eviction_preserves_counts() {
454        let mut log = StorageEventLog::new(2);
455        for _ in 0..5 {
456            log.log(
457                EventType::BlockAdded,
458                EventSeverity::Info,
459                "x",
460                None,
461                empty_meta(),
462            );
463        }
464        assert_eq!(log.count_by_type(EventType::BlockAdded), 5);
465    }
466
467    // 13. clear removes all events.
468    #[test]
469    fn test_clear() {
470        let mut log = StorageEventLog::new(100);
471        log.log(
472            EventType::BlockAdded,
473            EventSeverity::Info,
474            "a",
475            None,
476            empty_meta(),
477        );
478        log.log(
479            EventType::BlockDeleted,
480            EventSeverity::Info,
481            "b",
482            None,
483            empty_meta(),
484        );
485        log.clear();
486        assert_eq!(log.event_count(), 0);
487    }
488
489    // 14. clear preserves cumulative counts.
490    #[test]
491    fn test_clear_preserves_counts() {
492        let mut log = StorageEventLog::new(100);
493        log.log(
494            EventType::BlockAdded,
495            EventSeverity::Info,
496            "a",
497            None,
498            empty_meta(),
499        );
500        log.clear();
501        assert_eq!(log.count_by_type(EventType::BlockAdded), 1);
502    }
503
504    // 15. stats total_events.
505    #[test]
506    fn test_stats_total() {
507        let mut log = StorageEventLog::new(100);
508        log.log(
509            EventType::BlockAdded,
510            EventSeverity::Info,
511            "a",
512            None,
513            empty_meta(),
514        );
515        log.log(
516            EventType::Error,
517            EventSeverity::Error,
518            "e",
519            None,
520            empty_meta(),
521        );
522        let s = log.stats();
523        assert_eq!(s.total_events, 2);
524    }
525
526    // 16. stats events_by_severity.
527    #[test]
528    fn test_stats_severity_counts() {
529        let mut log = StorageEventLog::new(100);
530        log.log(
531            EventType::BlockAdded,
532            EventSeverity::Info,
533            "a",
534            None,
535            empty_meta(),
536        );
537        log.log(
538            EventType::Error,
539            EventSeverity::Error,
540            "e",
541            None,
542            empty_meta(),
543        );
544        log.log(
545            EventType::QuotaExceeded,
546            EventSeverity::Warning,
547            "q",
548            None,
549            empty_meta(),
550        );
551        let s = log.stats();
552        assert_eq!(s.events_by_severity.get("Info").copied(), Some(1));
553        assert_eq!(s.events_by_severity.get("Error").copied(), Some(1));
554        assert_eq!(s.events_by_severity.get("Warning").copied(), Some(1));
555    }
556
557    // 17. stats tick range.
558    #[test]
559    fn test_stats_tick_range() {
560        let mut log = StorageEventLog::new(100);
561        log.log(
562            EventType::BlockAdded,
563            EventSeverity::Info,
564            "t0",
565            None,
566            empty_meta(),
567        );
568        log.tick();
569        log.tick();
570        log.log(
571            EventType::BlockAdded,
572            EventSeverity::Info,
573            "t2",
574            None,
575            empty_meta(),
576        );
577        let s = log.stats();
578        assert_eq!(s.oldest_tick, Some(0));
579        assert_eq!(s.newest_tick, Some(2));
580    }
581
582    // 18. stats on empty log.
583    #[test]
584    fn test_stats_empty() {
585        let log = StorageEventLog::new(100);
586        let s = log.stats();
587        assert_eq!(s.total_events, 0);
588        assert!(s.oldest_tick.is_none());
589        assert!(s.newest_tick.is_none());
590    }
591
592    // 19. Multiple event types.
593    #[test]
594    fn test_multiple_event_types() {
595        let mut log = StorageEventLog::new(100);
596        log.log(
597            EventType::CompactionStarted,
598            EventSeverity::Info,
599            "start",
600            None,
601            empty_meta(),
602        );
603        log.log(
604            EventType::CompactionCompleted,
605            EventSeverity::Info,
606            "done",
607            None,
608            empty_meta(),
609        );
610        assert_eq!(log.events_by_type(EventType::CompactionStarted).len(), 1);
611        assert_eq!(log.events_by_type(EventType::CompactionCompleted).len(), 1);
612    }
613
614    // 20. Tick tracking.
615    #[test]
616    fn test_tick_tracking() {
617        let mut log = StorageEventLog::new(100);
618        log.log(
619            EventType::BlockAdded,
620            EventSeverity::Info,
621            "t0",
622            None,
623            empty_meta(),
624        );
625        log.tick();
626        log.tick();
627        log.tick();
628        log.log(
629            EventType::BlockAdded,
630            EventSeverity::Info,
631            "t3",
632            None,
633            empty_meta(),
634        );
635        let evt = log.get_event(1).expect("should exist");
636        assert_eq!(evt.tick, 3);
637    }
638
639    // 21. Metadata preserved.
640    #[test]
641    fn test_metadata_preserved() {
642        let mut log = StorageEventLog::new(100);
643        let mut meta = HashMap::new();
644        meta.insert("size".to_string(), "1024".to_string());
645        meta.insert("codec".to_string(), "dag-pb".to_string());
646        let id = log.log(
647            EventType::BlockAdded,
648            EventSeverity::Info,
649            "m",
650            Some("QmABC"),
651            meta,
652        );
653        let evt = log.get_event(id).expect("should exist");
654        assert_eq!(evt.metadata.get("size").map(|s| s.as_str()), Some("1024"));
655        assert_eq!(
656            evt.metadata.get("codec").map(|s| s.as_str()),
657            Some("dag-pb")
658        );
659    }
660
661    // 22. event_count after multiple operations.
662    #[test]
663    fn test_event_count() {
664        let mut log = StorageEventLog::new(100);
665        assert_eq!(log.event_count(), 0);
666        log.log(
667            EventType::BlockAdded,
668            EventSeverity::Info,
669            "a",
670            None,
671            empty_meta(),
672        );
673        assert_eq!(log.event_count(), 1);
674        log.log(
675            EventType::BlockDeleted,
676            EventSeverity::Info,
677            "b",
678            None,
679            empty_meta(),
680        );
681        assert_eq!(log.event_count(), 2);
682    }
683
684    // 23. Critical severity.
685    #[test]
686    fn test_critical_severity() {
687        let mut log = StorageEventLog::new(100);
688        log.log(
689            EventType::Error,
690            EventSeverity::Critical,
691            "disk full",
692            None,
693            empty_meta(),
694        );
695        let critical = log.events_by_severity(EventSeverity::Critical);
696        assert_eq!(critical.len(), 1);
697        assert_eq!(critical[0].message, "disk full");
698    }
699
700    // 24. events_since with no matches.
701    #[test]
702    fn test_events_since_no_matches() {
703        let mut log = StorageEventLog::new(100);
704        log.log(
705            EventType::BlockAdded,
706            EventSeverity::Info,
707            "old",
708            None,
709            empty_meta(),
710        );
711        let since = log.events_since(0);
712        assert!(since.is_empty());
713    }
714
715    // 25. QuotaExceeded event type.
716    #[test]
717    fn test_quota_exceeded_type() {
718        let mut log = StorageEventLog::new(100);
719        log.log(
720            EventType::QuotaExceeded,
721            EventSeverity::Warning,
722            "over limit",
723            None,
724            empty_meta(),
725        );
726        assert_eq!(log.count_by_type(EventType::QuotaExceeded), 1);
727        let by_type = log.events_by_type(EventType::QuotaExceeded);
728        assert_eq!(by_type.len(), 1);
729    }
730
731    // 26. block_cid None.
732    #[test]
733    fn test_block_cid_none() {
734        let mut log = StorageEventLog::new(100);
735        let id = log.log(
736            EventType::CompactionStarted,
737            EventSeverity::Info,
738            "compact",
739            None,
740            empty_meta(),
741        );
742        let evt = log.get_event(id).expect("should exist");
743        assert!(evt.block_cid.is_none());
744    }
745
746    // 27. Max events = 1.
747    #[test]
748    fn test_max_events_one() {
749        let mut log = StorageEventLog::new(1);
750        log.log(
751            EventType::BlockAdded,
752            EventSeverity::Info,
753            "first",
754            None,
755            empty_meta(),
756        );
757        log.log(
758            EventType::BlockDeleted,
759            EventSeverity::Info,
760            "second",
761            None,
762            empty_meta(),
763        );
764        assert_eq!(log.event_count(), 1);
765        assert!(log.get_event(0).is_none());
766        assert!(log.get_event(1).is_some());
767    }
768
769    // 28. Tick does not affect existing events.
770    #[test]
771    fn test_tick_immutable_on_existing() {
772        let mut log = StorageEventLog::new(100);
773        log.log(
774            EventType::BlockAdded,
775            EventSeverity::Info,
776            "before",
777            None,
778            empty_meta(),
779        );
780        log.tick();
781        let evt = log.get_event(0).expect("should exist");
782        assert_eq!(evt.tick, 0);
783    }
784
785    // 29. recent(0) returns empty.
786    #[test]
787    fn test_recent_zero() {
788        let mut log = StorageEventLog::new(100);
789        log.log(
790            EventType::BlockAdded,
791            EventSeverity::Info,
792            "a",
793            None,
794            empty_meta(),
795        );
796        assert!(log.recent(0).is_empty());
797    }
798
799    // 30. Combined filter: type then severity check.
800    #[test]
801    fn test_combined_filter() {
802        let mut log = StorageEventLog::new(100);
803        log.log(
804            EventType::Error,
805            EventSeverity::Error,
806            "e1",
807            None,
808            empty_meta(),
809        );
810        log.log(
811            EventType::Error,
812            EventSeverity::Critical,
813            "e2",
814            None,
815            empty_meta(),
816        );
817        log.log(
818            EventType::BlockAdded,
819            EventSeverity::Error,
820            "e3",
821            None,
822            empty_meta(),
823        );
824        let errors_of_type: Vec<_> = log
825            .events_by_type(EventType::Error)
826            .into_iter()
827            .filter(|e| e.severity == EventSeverity::Critical)
828            .collect();
829        assert_eq!(errors_of_type.len(), 1);
830        assert_eq!(errors_of_type[0].message, "e2");
831    }
832}