tracing_web_console/
storage.rs

1//! Log storage with circular buffer implementation
2
3use 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
10/// Maximum number of log events to store in memory
11const DEFAULT_MAX_EVENTS: usize = 10_000;
12/// Capacity of the broadcast channel for real-time log streaming
13const BROADCAST_CAPACITY: usize = 100;
14
15/// A single log event captured by the subscriber
16#[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/// Information about the span context
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SpanInfo {
34    pub name: String,
35    pub fields: HashMap<String, String>,
36}
37
38/// Sort order for log queries
39#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
40pub enum SortOrder {
41    /// Newest logs first (default)
42    #[default]
43    NewestFirst,
44    /// Oldest logs first
45    OldestFirst,
46}
47
48/// Filters for querying log events
49#[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
58/// Convert log level string to numeric value for comparison
59/// Higher number = higher severity (ERROR > WARN > INFO > DEBUG > TRACE)
60fn 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, // Unknown levels are lowest priority
68    }
69}
70
71/// Thread-safe circular buffer for storing log events
72#[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    /// Create a new log storage with default capacity
81    pub fn new() -> Self {
82        Self::with_capacity(DEFAULT_MAX_EVENTS)
83    }
84
85    /// Create a new log storage with specified capacity
86    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    /// Add a new log event, removing oldest if at capacity
96    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        // Send to broadcast channel, ignore if no receivers
104        let _ = self.tx.send(event.clone());
105
106        events.push_back(event);
107    }
108
109    /// Subscribe to real-time log events
110    pub fn subscribe(&self) -> broadcast::Receiver<LogEvent> {
111        self.tx.subscribe()
112    }
113
114    /// Get all log events matching the filter
115    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        // Apply sort order and pagination
133        let paginated: Vec<LogEvent> = match filter.sort_order {
134            SortOrder::NewestFirst => {
135                // Reverse to get newest first, then paginate
136                filtered
137                    .into_iter()
138                    .rev()
139                    .skip(offset)
140                    .take(limit.unwrap_or(usize::MAX))
141                    .collect()
142            }
143            SortOrder::OldestFirst => {
144                // Keep natural order (oldest first), then paginate
145                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    /// Get all unique targets from stored events
157    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    /// Check if storage is empty
171    #[allow(dead_code)]
172    pub fn is_empty(&self) -> bool {
173        self.events.read().is_empty()
174    }
175
176    /// Clear all stored events
177    #[allow(dead_code)]
178    pub fn clear(&self) {
179        self.events.write().clear();
180    }
181
182    /// Check if an event matches the filter criteria
183    fn matches_filter(&self, event: &LogEvent, filter: &LogFilter) -> bool {
184        // Determine the required log level for this event's target
185        // Target filters take precedence over global level
186        // Use prefix matching: "my_crate" matches "my_crate::module::thing"
187        let target_level = filter
188            .target_levels
189            .iter()
190            .filter(|(target, _)| {
191                event.target == **target || event.target.starts_with(&format!("{}::", target))
192            })
193            // If multiple matches, use the most specific (longest) target
194            .max_by_key(|(target, _)| target.len())
195            .map(|(_, level)| level);
196
197        // Target-specific level takes precedence, then fall back to global level
198        let required_level = target_level.or(filter.global_level.as_ref());
199
200        // If a level filter is specified, check if event level meets it
201        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            // Event level must be >= required level (higher severity)
206            if event_level_num < required_level_num {
207                return false;
208            }
209        }
210
211        // Filter by target (case-insensitive contains)
212        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        // Filter by search term in message (case-insensitive contains)
223        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        // Adding 4th should remove oldest
273        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        // NewestFirst by default, so msg4 should be first
278        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}