use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use tokio::sync::broadcast;
const DEFAULT_MAX_EVENTS: usize = 10_000;
const BROADCAST_CAPACITY: usize = 100;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogEvent {
pub timestamp: DateTime<Utc>,
pub level: String,
pub target: String,
pub message: String,
pub fields: HashMap<String, String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<SpanInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanInfo {
pub name: String,
pub fields: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SortOrder {
#[default]
NewestFirst,
OldestFirst,
}
#[derive(Debug, Clone, Default)]
pub struct LogFilter {
pub global_level: Option<String>,
pub target_levels: HashMap<String, String>,
pub search: Option<String>,
pub target: Option<String>,
pub sort_order: SortOrder,
}
fn level_to_number(level: &str) -> u8 {
match level.to_uppercase().as_str() {
"ERROR" => 5,
"WARN" => 4,
"INFO" => 3,
"DEBUG" => 2,
"TRACE" => 1,
_ => 0, }
}
#[derive(Clone)]
pub struct LogStorage {
events: Arc<RwLock<VecDeque<LogEvent>>>,
max_events: usize,
tx: broadcast::Sender<LogEvent>,
}
impl LogStorage {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_MAX_EVENTS)
}
pub fn with_capacity(max_events: usize) -> Self {
let (tx, _) = broadcast::channel(BROADCAST_CAPACITY);
Self {
events: Arc::new(RwLock::new(VecDeque::with_capacity(max_events))),
max_events,
tx,
}
}
pub fn push(&self, event: LogEvent) {
let mut events = self.events.write();
if events.len() >= self.max_events {
events.pop_front();
}
let _ = self.tx.send(event.clone());
events.push_back(event);
}
pub fn subscribe(&self) -> broadcast::Receiver<LogEvent> {
self.tx.subscribe()
}
pub fn get_filtered(
&self,
filter: &LogFilter,
limit: Option<usize>,
offset: Option<usize>,
) -> (Vec<LogEvent>, usize) {
let events = self.events.read();
let offset = offset.unwrap_or(0);
let filtered: Vec<LogEvent> = events
.iter()
.filter(|event| self.matches_filter(event, filter))
.cloned()
.collect();
let total_filtered = filtered.len();
let paginated: Vec<LogEvent> = match filter.sort_order {
SortOrder::NewestFirst => {
filtered
.into_iter()
.rev()
.skip(offset)
.take(limit.unwrap_or(usize::MAX))
.collect()
}
SortOrder::OldestFirst => {
filtered
.into_iter()
.skip(offset)
.take(limit.unwrap_or(usize::MAX))
.collect()
}
};
(paginated, total_filtered)
}
pub fn get_targets(&self) -> Vec<String> {
let events = self.events.read();
let mut targets: Vec<String> = events
.iter()
.map(|e| e.target.clone())
.collect::<std::collections::HashSet<_>>()
.into_iter()
.collect();
targets.sort();
targets
}
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.events.read().is_empty()
}
#[allow(dead_code)]
pub fn clear(&self) {
self.events.write().clear();
}
fn matches_filter(&self, event: &LogEvent, filter: &LogFilter) -> bool {
let target_level = filter
.target_levels
.iter()
.filter(|(target, _)| {
event.target == **target || event.target.starts_with(&format!("{}::", target))
})
.max_by_key(|(target, _)| target.len())
.map(|(_, level)| level);
let required_level = target_level.or(filter.global_level.as_ref());
if let Some(level_str) = required_level {
let event_level_num = level_to_number(&event.level);
let required_level_num = level_to_number(level_str);
if event_level_num < required_level_num {
return false;
}
}
if let Some(ref target_filter) = filter.target {
if !event
.target
.to_lowercase()
.contains(&target_filter.to_lowercase())
{
return false;
}
}
if let Some(ref search) = filter.search {
if !event
.message
.to_lowercase()
.contains(&search.to_lowercase())
{
return false;
}
}
true
}
}
impl Default for LogStorage {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_event(level: &str, target: &str, message: &str) -> LogEvent {
LogEvent {
timestamp: Utc::now(),
level: level.to_string(),
target: target.to_string(),
message: message.to_string(),
fields: HashMap::new(),
span: None,
file: None,
line: None,
}
}
#[test]
fn test_circular_buffer() {
let storage = LogStorage::with_capacity(3);
storage.push(create_test_event("INFO", "test", "msg1"));
storage.push(create_test_event("INFO", "test", "msg2"));
storage.push(create_test_event("INFO", "test", "msg3"));
let filter = LogFilter::default();
let (_events, count) = storage.get_filtered(&filter, None, None);
assert_eq!(count, 3);
storage.push(create_test_event("INFO", "test", "msg4"));
let (events, count) = storage.get_filtered(&filter, None, None);
assert_eq!(count, 3);
assert_eq!(events[0].message, "msg4");
assert_eq!(events[2].message, "msg2");
}
#[test]
fn test_level_filter() {
let storage = LogStorage::new();
storage.push(create_test_event("INFO", "test", "info msg"));
storage.push(create_test_event("ERROR", "test", "error msg"));
storage.push(create_test_event("DEBUG", "test", "debug msg"));
let filter = LogFilter {
global_level: Some("ERROR".to_string()),
..Default::default()
};
let (filtered, count) = storage.get_filtered(&filter, None, None);
assert_eq!(count, 1);
assert_eq!(filtered[0].level, "ERROR");
}
#[test]
fn test_search_filter() {
let storage = LogStorage::new();
storage.push(create_test_event("INFO", "test", "hello world"));
storage.push(create_test_event("INFO", "test", "goodbye world"));
storage.push(create_test_event("INFO", "test", "testing"));
let filter = LogFilter {
search: Some("hello".to_string()),
..Default::default()
};
let (filtered, count) = storage.get_filtered(&filter, None, None);
assert_eq!(count, 1);
assert!(filtered[0].message.contains("hello"));
}
}