use crate::entry::Entry;
use rustlavel_core::{Event, Json};
use std::collections::{BTreeMap, VecDeque};
use std::sync::{Arc, Mutex, MutexGuard};
use std::time::{Duration, SystemTime};
pub const DEFAULT_CAPACITY: usize = 500;
#[derive(Clone)]
pub struct Store {
inner: Arc<Mutex<Inner>>,
}
struct Inner {
entries: VecDeque<Entry>,
capacity: usize,
next_id: u64,
pending: Vec<u64>,
}
#[derive(Debug, Clone, Default)]
pub struct Filter {
pub kind: Option<String>,
pub after: Option<u64>,
pub limit: Option<usize>,
}
impl Store {
pub fn new() -> Self {
Store::with_capacity(DEFAULT_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
Store {
inner: Arc::new(Mutex::new(Inner {
entries: VecDeque::new(),
capacity: capacity.max(1),
next_id: 1,
pending: Vec::new(),
})),
}
}
fn lock(&self) -> MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn record(&self, event: &Event) -> Entry {
let mut inner = self.lock();
let id = inner.next_id;
inner.next_id += 1;
let entry = Entry::from_event(id, event);
inner.push(entry);
if event.kind == "http.request" {
let window = event.duration.unwrap_or(Duration::ZERO);
inner.claim(id, event.at.checked_sub(window).unwrap_or(event.at));
} else {
inner.pending.push(id);
let overflow = inner.pending.len().saturating_sub(inner.capacity);
inner.pending.drain(..overflow);
}
inner.entries.back().cloned().expect("the entry just pushed is present")
}
pub fn restore(&self, entries: Vec<Entry>) {
let mut inner = self.lock();
for entry in entries {
inner.next_id = inner.next_id.max(entry.id + 1);
inner.push(entry);
}
}
pub fn entries(&self, filter: &Filter) -> Vec<Entry> {
let inner = self.lock();
let limit = filter.limit.unwrap_or(usize::MAX);
inner
.entries
.iter()
.rev()
.filter(|entry| filter.kind.as_ref().is_none_or(|kind| &entry.kind == kind))
.filter(|entry| filter.after.is_none_or(|after| entry.id > after))
.take(limit)
.cloned()
.collect()
}
pub fn get(&self, id: u64) -> Option<Entry> {
self.lock().entries.iter().find(|entry| entry.id == id).cloned()
}
pub fn related(&self, id: u64) -> Vec<Entry> {
let inner = self.lock();
let Some(group) = inner.entries.iter().find(|entry| entry.id == id).and_then(|e| e.group)
else {
return Vec::new();
};
inner
.entries
.iter()
.filter(|entry| entry.group == Some(group) && entry.id != id)
.cloned()
.collect()
}
pub fn kinds(&self) -> Vec<(String, usize)> {
let inner = self.lock();
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for entry in &inner.entries {
*counts.entry(entry.kind.clone()).or_default() += 1;
}
counts.into_iter().collect()
}
pub fn len(&self) -> usize {
self.lock().entries.len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn capacity(&self) -> usize {
self.lock().capacity
}
pub fn clear(&self) {
let mut inner = self.lock();
inner.entries.clear();
inner.pending.clear();
}
pub fn to_json(&self, filter: &Filter) -> Json {
let entries: Vec<Json> = self.entries(filter).iter().map(Entry::to_json).collect();
let kinds: Vec<Json> = self
.kinds()
.into_iter()
.map(|(kind, count)| Json::object([("kind", Json::from(kind)), ("count", Json::from(count))]))
.collect();
Json::object([
("entries", Json::Array(entries)),
("kinds", Json::Array(kinds)),
("total", Json::from(self.len())),
("capacity", Json::from(self.capacity())),
])
}
}
impl Default for Store {
fn default() -> Self {
Store::new()
}
}
impl Inner {
fn push(&mut self, entry: Entry) {
self.entries.push_back(entry);
while self.entries.len() > self.capacity {
if let Some(evicted) = self.entries.pop_front() {
self.pending.retain(|id| *id != evicted.id);
}
}
}
fn claim(&mut self, request_id: u64, window_start: SystemTime) {
let key = self
.entries
.iter()
.find(|entry| entry.id == request_id)
.and_then(|entry| entry.request_key.clone());
let pending = std::mem::take(&mut self.pending);
let mut unclaimed = Vec::new();
for entry in self.entries.iter_mut() {
if entry.id == request_id {
entry.group = Some(request_id);
continue;
}
if entry.group.is_some() || !pending.contains(&entry.id) {
continue;
}
let mine = match key.as_deref() {
Some(key) => entry.request_key.as_deref() == Some(key),
None => entry.at >= window_start,
};
if mine {
entry.group = Some(request_id);
} else {
unclaimed.push(entry.id);
}
}
self.pending = unclaimed;
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn event(kind: &'static str) -> Event {
Event::new(kind)
}
#[test]
fn recording_assigns_monotonic_ids() {
let store = Store::new();
assert_eq!(store.record(&event("log")).id, 1);
assert_eq!(store.record(&event("log")).id, 2);
assert_eq!(store.len(), 2);
}
#[test]
fn the_oldest_entry_is_evicted_past_capacity() {
let store = Store::with_capacity(3);
for _ in 0..5 {
store.record(&event("log"));
}
let ids: Vec<u64> = store.entries(&Filter::default()).iter().map(|e| e.id).collect();
assert_eq!(ids, vec![5, 4, 3]);
assert_eq!(store.len(), 3);
assert!(store.get(1).is_none());
}
#[test]
fn entries_come_back_newest_first() {
let store = Store::new();
store.record(&event("log"));
store.record(&event("db.query"));
let ids: Vec<u64> = store.entries(&Filter::default()).iter().map(|e| e.id).collect();
assert_eq!(ids, vec![2, 1]);
}
#[test]
fn filtering_by_kind_keeps_only_that_kind() {
let store = Store::new();
store.record(&event("log"));
store.record(&event("db.query"));
store.record(&event("log"));
let filter = Filter { kind: Some("log".into()), ..Filter::default() };
let found = store.entries(&filter);
assert_eq!(found.len(), 2);
assert!(found.iter().all(|entry| entry.kind == "log"));
}
#[test]
fn filtering_after_an_id_returns_only_newer_entries() {
let store = Store::new();
store.record(&event("log"));
store.record(&event("log"));
store.record(&event("log"));
let filter = Filter { after: Some(2), ..Filter::default() };
let ids: Vec<u64> = store.entries(&filter).iter().map(|e| e.id).collect();
assert_eq!(ids, vec![3]);
}
#[test]
fn a_limit_caps_the_result() {
let store = Store::new();
for _ in 0..10 {
store.record(&event("log"));
}
let filter = Filter { limit: Some(4), ..Filter::default() };
assert_eq!(store.entries(&filter).len(), 4);
}
#[test]
fn a_request_claims_the_queries_recorded_during_it() {
let store = Store::new();
store.record(&event("db.query"));
store.record(&event("log"));
let request = store.record(&event("http.request").took(Duration::from_secs(5)));
let related: Vec<String> = store.related(request.id).iter().map(|e| e.kind.clone()).collect();
assert_eq!(related, vec!["db.query".to_string(), "log".to_string()]);
assert_eq!(store.get(1).unwrap().group, Some(request.id));
}
#[test]
fn an_entry_older_than_the_window_is_left_for_a_later_request() {
let store = Store::new();
let mut old = Event::new("log");
old.at = SystemTime::now() - Duration::from_secs(60);
store.record(&old);
let request = store.record(&event("http.request").took(Duration::from_millis(1)));
assert!(store.related(request.id).is_empty());
assert_eq!(store.get(1).unwrap().group, None);
}
#[test]
fn an_explicit_request_id_groups_exactly() {
let store = Store::new();
store.record(&event("db.query").with("request_id", "a"));
store.record(&event("db.query").with("request_id", "b"));
let request =
store.record(&event("http.request").with("request_id", "a").took(Duration::from_secs(5)));
let related = store.related(request.id);
assert_eq!(related.len(), 1);
assert_eq!(related[0].fields["request_id"].as_str(), Some("a"));
}
#[test]
fn kinds_are_counted_for_the_filter_bar() {
let store = Store::new();
store.record(&event("log"));
store.record(&event("log"));
store.record(&event("db.query"));
assert_eq!(store.kinds(), vec![("db.query".into(), 1), ("log".into(), 2)]);
}
#[test]
fn clearing_empties_the_buffer_without_reusing_ids() {
let store = Store::new();
store.record(&event("log"));
store.clear();
assert!(store.is_empty());
assert_eq!(store.record(&event("log")).id, 2);
}
#[test]
fn restoring_keeps_ids_ahead_of_what_was_loaded() {
let store = Store::new();
store.restore(vec![Entry::from_event(41, &event("log"))]);
assert_eq!(store.record(&event("log")).id, 42);
}
#[test]
fn a_zero_capacity_still_keeps_one_entry() {
let store = Store::with_capacity(0);
store.record(&event("log"));
assert_eq!(store.capacity(), 1);
assert_eq!(store.len(), 1);
}
}