Skip to main content

request_shadow/
log.rs

1//! Bounded ring buffer for divergence records.
2//!
3//! Real production telemetry should ship divergences to your tracing /
4//! metrics pipeline. This in-process log is for tests and for operators
5//! poking at a live process via an admin endpoint.
6
7use std::collections::VecDeque;
8
9use parking_lot_dummy::Mutex;
10
11use crate::divergence::Divergence;
12
13/// One log entry: the input that produced the divergence + the divergence
14/// itself. We don't store the full responses — that's potentially a lot of
15/// memory.
16#[derive(Clone, Debug)]
17pub struct DivergenceEntry {
18    /// Sampling / correlation key used by the call.
19    pub key: Vec<u8>,
20    /// The structured diff.
21    pub divergence: Divergence,
22}
23
24/// Bounded ring buffer. Drops the oldest entry when full.
25#[derive(Debug)]
26pub struct DivergenceLog {
27    capacity: usize,
28    entries: Mutex<VecDeque<DivergenceEntry>>,
29}
30
31impl DivergenceLog {
32    /// Build a log with the given capacity. `capacity = 0` disables logging.
33    pub fn new(capacity: usize) -> Self {
34        Self {
35            capacity,
36            entries: Mutex::new(VecDeque::with_capacity(capacity)),
37        }
38    }
39
40    /// Push one divergence. No-op when capacity is 0.
41    pub fn push(&self, entry: DivergenceEntry) {
42        if self.capacity == 0 {
43            return;
44        }
45        let mut g = self.entries.lock();
46        if g.len() == self.capacity {
47            g.pop_front();
48        }
49        g.push_back(entry);
50    }
51
52    /// Snapshot the current entries oldest-first.
53    pub fn snapshot(&self) -> Vec<DivergenceEntry> {
54        let g = self.entries.lock();
55        g.iter().cloned().collect()
56    }
57
58    /// Number of stored entries (≤ capacity).
59    pub fn len(&self) -> usize {
60        self.entries.lock().len()
61    }
62
63    /// Whether the log has any entries.
64    pub fn is_empty(&self) -> bool {
65        self.entries.lock().is_empty()
66    }
67}
68
69impl Default for DivergenceLog {
70    fn default() -> Self {
71        Self::new(128)
72    }
73}
74
75// We don't want a parking_lot dep just for this — wrap std::sync::Mutex with
76// a poison-discarding API so the call sites stay clean.
77mod parking_lot_dummy {
78    use std::sync::Mutex as StdMutex;
79
80    #[derive(Debug)]
81    pub struct Mutex<T>(StdMutex<T>);
82
83    impl<T> Mutex<T> {
84        pub fn new(t: T) -> Self {
85            Self(StdMutex::new(t))
86        }
87        pub fn lock(&self) -> std::sync::MutexGuard<'_, T> {
88            self.0
89                .lock()
90                .unwrap_or_else(std::sync::PoisonError::into_inner)
91        }
92    }
93}