1use chrono::{DateTime, Utc};
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6use std::collections::{HashMap, VecDeque};
7use std::sync::Arc;
8use tokio::sync::broadcast;
9
10const DEFAULT_MAX_EVENTS: usize = 10_000;
12const BROADCAST_CAPACITY: usize = 100;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct LogEvent {
18 pub timestamp: DateTime<Utc>,
19 pub level: String,
20 pub target: String,
21 pub message: String,
22 pub fields: HashMap<String, String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
24 pub span: Option<SpanInfo>,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub file: Option<String>,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub line: Option<u32>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SpanInfo {
34 pub name: String,
35 pub fields: HashMap<String, String>,
36}
37
38#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub enum SortOrder {
41 #[default]
43 NewestFirst,
44 OldestFirst,
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct LogFilter {
51 pub global_level: Option<String>,
52 pub target_levels: HashMap<String, String>,
53 pub search: Option<String>,
54 pub target: Option<String>,
55 pub sort_order: SortOrder,
56}
57
58fn level_to_number(level: &str) -> u8 {
61 match level.to_uppercase().as_str() {
62 "ERROR" => 5,
63 "WARN" => 4,
64 "INFO" => 3,
65 "DEBUG" => 2,
66 "TRACE" => 1,
67 _ => 0, }
69}
70
71#[derive(Clone)]
73pub struct LogStorage {
74 events: Arc<RwLock<VecDeque<LogEvent>>>,
75 max_events: usize,
76 tx: broadcast::Sender<LogEvent>,
77}
78
79impl LogStorage {
80 pub fn new() -> Self {
82 Self::with_capacity(DEFAULT_MAX_EVENTS)
83 }
84
85 pub fn with_capacity(max_events: usize) -> Self {
87 let (tx, _) = broadcast::channel(BROADCAST_CAPACITY);
88 Self {
89 events: Arc::new(RwLock::new(VecDeque::with_capacity(max_events))),
90 max_events,
91 tx,
92 }
93 }
94
95 pub fn push(&self, event: LogEvent) {
97 let mut events = self.events.write();
98
99 if events.len() >= self.max_events {
100 events.pop_front();
101 }
102
103 let _ = self.tx.send(event.clone());
105
106 events.push_back(event);
107 }
108
109 pub fn subscribe(&self) -> broadcast::Receiver<LogEvent> {
111 self.tx.subscribe()
112 }
113
114 pub fn get_filtered(
116 &self,
117 filter: &LogFilter,
118 limit: Option<usize>,
119 offset: Option<usize>,
120 ) -> (Vec<LogEvent>, usize) {
121 let events = self.events.read();
122 let offset = offset.unwrap_or(0);
123
124 let filtered: Vec<LogEvent> = events
125 .iter()
126 .filter(|event| self.matches_filter(event, filter))
127 .cloned()
128 .collect();
129
130 let total_filtered = filtered.len();
131
132 let paginated: Vec<LogEvent> = match filter.sort_order {
134 SortOrder::NewestFirst => {
135 filtered
137 .into_iter()
138 .rev()
139 .skip(offset)
140 .take(limit.unwrap_or(usize::MAX))
141 .collect()
142 }
143 SortOrder::OldestFirst => {
144 filtered
146 .into_iter()
147 .skip(offset)
148 .take(limit.unwrap_or(usize::MAX))
149 .collect()
150 }
151 };
152
153 (paginated, total_filtered)
154 }
155
156 pub fn get_targets(&self) -> Vec<String> {
158 let events = self.events.read();
159 let mut targets: Vec<String> = events
160 .iter()
161 .map(|e| e.target.clone())
162 .collect::<std::collections::HashSet<_>>()
163 .into_iter()
164 .collect();
165
166 targets.sort();
167 targets
168 }
169
170 #[allow(dead_code)]
172 pub fn is_empty(&self) -> bool {
173 self.events.read().is_empty()
174 }
175
176 #[allow(dead_code)]
178 pub fn clear(&self) {
179 self.events.write().clear();
180 }
181
182 fn matches_filter(&self, event: &LogEvent, filter: &LogFilter) -> bool {
184 let target_level = filter
188 .target_levels
189 .iter()
190 .filter(|(target, _)| {
191 event.target == **target || event.target.starts_with(&format!("{}::", target))
192 })
193 .max_by_key(|(target, _)| target.len())
195 .map(|(_, level)| level);
196
197 let required_level = target_level.or(filter.global_level.as_ref());
199
200 if let Some(level_str) = required_level {
202 let event_level_num = level_to_number(&event.level);
203 let required_level_num = level_to_number(level_str);
204
205 if event_level_num < required_level_num {
207 return false;
208 }
209 }
210
211 if let Some(ref target_filter) = filter.target {
213 if !event
214 .target
215 .to_lowercase()
216 .contains(&target_filter.to_lowercase())
217 {
218 return false;
219 }
220 }
221
222 if let Some(ref search) = filter.search {
224 if !event
225 .message
226 .to_lowercase()
227 .contains(&search.to_lowercase())
228 {
229 return false;
230 }
231 }
232
233 true
234 }
235}
236
237impl Default for LogStorage {
238 fn default() -> Self {
239 Self::new()
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 fn create_test_event(level: &str, target: &str, message: &str) -> LogEvent {
248 LogEvent {
249 timestamp: Utc::now(),
250 level: level.to_string(),
251 target: target.to_string(),
252 message: message.to_string(),
253 fields: HashMap::new(),
254 span: None,
255 file: None,
256 line: None,
257 }
258 }
259
260 #[test]
261 fn test_circular_buffer() {
262 let storage = LogStorage::with_capacity(3);
263
264 storage.push(create_test_event("INFO", "test", "msg1"));
265 storage.push(create_test_event("INFO", "test", "msg2"));
266 storage.push(create_test_event("INFO", "test", "msg3"));
267
268 let filter = LogFilter::default();
269 let (_events, count) = storage.get_filtered(&filter, None, None);
270 assert_eq!(count, 3);
271
272 storage.push(create_test_event("INFO", "test", "msg4"));
274
275 let (events, count) = storage.get_filtered(&filter, None, None);
276 assert_eq!(count, 3);
277 assert_eq!(events[0].message, "msg4");
279 assert_eq!(events[2].message, "msg2");
280 }
281
282 #[test]
283 fn test_level_filter() {
284 let storage = LogStorage::new();
285
286 storage.push(create_test_event("INFO", "test", "info msg"));
287 storage.push(create_test_event("ERROR", "test", "error msg"));
288 storage.push(create_test_event("DEBUG", "test", "debug msg"));
289
290 let filter = LogFilter {
291 global_level: Some("ERROR".to_string()),
292 ..Default::default()
293 };
294
295 let (filtered, count) = storage.get_filtered(&filter, None, None);
296 assert_eq!(count, 1);
297 assert_eq!(filtered[0].level, "ERROR");
298 }
299
300 #[test]
301 fn test_search_filter() {
302 let storage = LogStorage::new();
303
304 storage.push(create_test_event("INFO", "test", "hello world"));
305 storage.push(create_test_event("INFO", "test", "goodbye world"));
306 storage.push(create_test_event("INFO", "test", "testing"));
307
308 let filter = LogFilter {
309 search: Some("hello".to_string()),
310 ..Default::default()
311 };
312
313 let (filtered, count) = storage.get_filtered(&filter, None, None);
314 assert_eq!(count, 1);
315 assert!(filtered[0].message.contains("hello"));
316 }
317}