Skip to main content

osproxy_observe/
breakglass.rs

1//! Break-glass ring buffer: a bounded, in-order tape of recent explanations
2//! captured **only when a directive asks** (`ring_buffer: true`, `docs/05` ยง5).
3//!
4//! Distinct from [`ExplainStore`](crate::ExplainStore), which is the always-on,
5//! lookup-by-request-id store behind `/debug/explain/{id}`. The break-glass
6//! buffer is a *sequence* an operator turns on deliberately, when a class of
7//! request is failing and the ids aren't known up front, flip a `ring_buffer`
8//! directive and read back the last N matching requests as a forensic tape.
9//!
10//! Single-instance by design (the captured tape lives on the instance that
11//! handled the requests); bounded so it costs nothing until used and cannot grow
12//! without limit once on. Shape-only, inherited from the explain document it
13//! stores, it cannot reveal a tenant value because none was ever captured.
14
15use std::collections::VecDeque;
16use std::sync::{Mutex, PoisonError};
17
18use serde_json::Value;
19
20/// A bounded in-memory ring of recent explanation documents, captured on demand.
21#[derive(Debug)]
22pub struct BreakGlassBuffer {
23    capacity: usize,
24    entries: Mutex<VecDeque<Value>>,
25}
26
27impl BreakGlassBuffer {
28    /// Creates a buffer holding at most `capacity` recent captures.
29    #[must_use]
30    pub fn new(capacity: usize) -> Self {
31        Self {
32            capacity: capacity.max(1),
33            entries: Mutex::new(VecDeque::new()),
34        }
35    }
36
37    /// Captures one explanation document, evicting the oldest if full.
38    pub fn capture(&self, doc: Value) {
39        let mut entries = self.lock();
40        if entries.len() >= self.capacity {
41            entries.pop_front();
42        }
43        entries.push_back(doc);
44    }
45
46    /// A snapshot of the captured tape, oldest first, the break-glass read.
47    #[must_use]
48    pub fn snapshot(&self) -> Vec<Value> {
49        self.lock().iter().cloned().collect()
50    }
51
52    /// How many captures the tape currently holds.
53    #[must_use]
54    pub fn len(&self) -> usize {
55        self.lock().len()
56    }
57
58    /// Whether the tape is empty (nothing captured yet).
59    #[must_use]
60    pub fn is_empty(&self) -> bool {
61        self.lock().is_empty()
62    }
63
64    /// Locks the tape, recovering a poisoned lock, it is append-only forensic
65    /// data with no invariant a panicking holder could tear (NFR-R1).
66    fn lock(&self) -> std::sync::MutexGuard<'_, VecDeque<Value>> {
67        self.entries.lock().unwrap_or_else(PoisonError::into_inner)
68    }
69}
70
71#[cfg(test)]
72#[path = "breakglass_tests.rs"]
73mod tests;