1use std::collections::{HashMap, VecDeque};
9use std::sync::RwLock;
10
11use chrono::{DateTime, Utc};
12use dashmap::DashMap;
13use serde::{Deserialize, Serialize};
14use tokio::sync::mpsc;
15
16use crate::console::LogLevel;
17use crate::process::Pid;
18
19pub const DEFAULT_LOG_CAPACITY: usize = 100_000;
21
22const SUBSCRIBER_CHANNEL_CAPACITY: usize = 1024;
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct LogEntry {
28 pub timestamp: DateTime<Utc>,
30 pub level: LogLevel,
32 pub source_pid: Option<Pid>,
34 pub source_service: Option<String>,
36 pub message: String,
38 pub fields: HashMap<String, serde_json::Value>,
40 pub trace_id: Option<String>,
42}
43
44impl LogEntry {
45 pub fn new(level: LogLevel, message: impl Into<String>) -> Self {
47 Self {
48 timestamp: Utc::now(),
49 level,
50 source_pid: None,
51 source_service: None,
52 message: message.into(),
53 fields: HashMap::new(),
54 trace_id: None,
55 }
56 }
57
58 pub fn with_pid(mut self, pid: Pid) -> Self {
60 self.source_pid = Some(pid);
61 self
62 }
63
64 pub fn with_service(mut self, service: impl Into<String>) -> Self {
66 self.source_service = Some(service.into());
67 self
68 }
69
70 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
72 self.trace_id = Some(trace_id.into());
73 self
74 }
75
76 pub fn with_field(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
78 self.fields.insert(key.into(), value);
79 self
80 }
81}
82
83#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct LogQuery {
86 pub pid: Option<Pid>,
88 pub service: Option<String>,
90 pub level_min: Option<LogLevel>,
92 pub since: Option<DateTime<Utc>>,
94 pub until: Option<DateTime<Utc>>,
96 pub limit: usize,
98 pub trace_id: Option<String>,
100}
101
102impl LogQuery {
103 pub fn new(limit: usize) -> Self {
105 Self {
106 limit,
107 ..Default::default()
108 }
109 }
110}
111
112fn level_severity(level: &LogLevel) -> u8 {
114 match level {
115 LogLevel::Debug => 0,
116 LogLevel::Info => 1,
117 LogLevel::Warn => 2,
118 LogLevel::Error => 3,
119 }
120}
121
122pub struct LogService {
128 entries: RwLock<VecDeque<LogEntry>>,
129 max_entries: usize,
130 subscribers: DashMap<String, mpsc::Sender<LogEntry>>,
131}
132
133impl LogService {
134 pub fn new(max_entries: usize) -> Self {
136 Self {
137 entries: RwLock::new(VecDeque::with_capacity(max_entries.min(8192))),
138 max_entries,
139 subscribers: DashMap::new(),
140 }
141 }
142
143 pub fn with_default_capacity() -> Self {
145 Self::new(DEFAULT_LOG_CAPACITY)
146 }
147
148 pub fn ingest(&self, entry: LogEntry) {
154 {
156 let mut entries = self.entries.write().expect("log write lock poisoned");
157 while entries.len() >= self.max_entries {
158 entries.pop_front();
159 }
160 entries.push_back(entry.clone());
161 }
162
163 let mut dead_subs = Vec::new();
165 for sub in self.subscribers.iter() {
166 if sub.value().try_send(entry.clone()).is_err() {
167 dead_subs.push(sub.key().clone());
168 }
169 }
170 for key in dead_subs {
171 self.subscribers.remove(&key);
172 }
173 }
174
175 pub fn log(&self, level: LogLevel, message: impl Into<String>, pid: Option<Pid>) {
177 let mut entry = LogEntry::new(level, message);
178 entry.source_pid = pid;
179 self.ingest(entry);
180 }
181
182 pub fn query(&self, query: &LogQuery) -> Vec<LogEntry> {
184 let entries = self.entries.read().expect("log read lock poisoned");
185 entries
186 .iter()
187 .filter(|e| query.pid.is_none_or(|pid| e.source_pid == Some(pid)))
188 .filter(|e| {
189 query
190 .service
191 .as_ref()
192 .is_none_or(|s| e.source_service.as_ref() == Some(s))
193 })
194 .filter(|e| {
195 query
196 .level_min
197 .as_ref()
198 .is_none_or(|min| level_severity(&e.level) >= level_severity(min))
199 })
200 .filter(|e| query.since.is_none_or(|since| e.timestamp >= since))
201 .filter(|e| query.until.is_none_or(|until| e.timestamp <= until))
202 .filter(|e| {
203 query
204 .trace_id
205 .as_ref()
206 .is_none_or(|tid| e.trace_id.as_ref() == Some(tid))
207 })
208 .take(query.limit)
209 .cloned()
210 .collect()
211 }
212
213 pub fn subscribe(&self) -> (String, mpsc::Receiver<LogEntry>) {
218 let id = uuid::Uuid::new_v4().to_string();
219 let (tx, rx) = mpsc::channel(SUBSCRIBER_CHANNEL_CAPACITY);
220 self.subscribers.insert(id.clone(), tx);
221 (id, rx)
222 }
223
224 pub fn unsubscribe(&self, id: &str) {
226 self.subscribers.remove(id);
227 }
228
229 pub fn len(&self) -> usize {
231 self.entries.read().expect("log read lock poisoned").len()
232 }
233
234 pub fn is_empty(&self) -> bool {
236 self.len() == 0
237 }
238
239 pub fn capacity(&self) -> usize {
241 self.max_entries
242 }
243
244 pub fn subscriber_count(&self) -> usize {
246 self.subscribers.len()
247 }
248
249 pub fn clear(&self) {
251 self.entries
252 .write()
253 .expect("log write lock poisoned")
254 .clear();
255 }
256}
257
258impl Default for LogService {
259 fn default() -> Self {
260 Self::with_default_capacity()
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 fn info_entry(msg: &str) -> LogEntry {
269 LogEntry::new(LogLevel::Info, msg)
270 }
271
272 #[test]
273 fn ingest_and_len() {
274 let service = LogService::new(100);
275 assert!(service.is_empty());
276
277 service.ingest(info_entry("hello"));
278 assert_eq!(service.len(), 1);
279 }
280
281 #[test]
282 fn ring_buffer_eviction() {
283 let service = LogService::new(3);
284
285 for i in 0..5 {
286 service.ingest(info_entry(&format!("msg-{i}")));
287 }
288
289 assert_eq!(service.len(), 3);
290 let results = service.query(&LogQuery::new(10));
291 assert_eq!(results[0].message, "msg-2");
292 assert_eq!(results[2].message, "msg-4");
293 }
294
295 #[test]
296 fn query_by_pid() {
297 let service = LogService::new(100);
298 service.ingest(info_entry("from-1").with_pid(1));
299 service.ingest(info_entry("from-2").with_pid(2));
300 service.ingest(info_entry("from-1-again").with_pid(1));
301
302 let results = service.query(&LogQuery {
303 pid: Some(1),
304 limit: 10,
305 ..Default::default()
306 });
307 assert_eq!(results.len(), 2);
308 assert!(results.iter().all(|e| e.source_pid == Some(1)));
309 }
310
311 #[test]
312 fn query_by_service() {
313 let service = LogService::new(100);
314 service.ingest(info_entry("a").with_service("auth"));
315 service.ingest(info_entry("b").with_service("db"));
316 service.ingest(info_entry("c").with_service("auth"));
317
318 let results = service.query(&LogQuery {
319 service: Some("auth".into()),
320 limit: 10,
321 ..Default::default()
322 });
323 assert_eq!(results.len(), 2);
324 }
325
326 #[test]
327 fn query_by_level() {
328 let service = LogService::new(100);
329 service.ingest(LogEntry::new(LogLevel::Debug, "debug-msg"));
330 service.ingest(LogEntry::new(LogLevel::Info, "info-msg"));
331 service.ingest(LogEntry::new(LogLevel::Warn, "warn-msg"));
332 service.ingest(LogEntry::new(LogLevel::Error, "error-msg"));
333
334 let results = service.query(&LogQuery {
335 level_min: Some(LogLevel::Warn),
336 limit: 10,
337 ..Default::default()
338 });
339 assert_eq!(results.len(), 2);
340 assert!(
341 results
342 .iter()
343 .all(|e| matches!(e.level, LogLevel::Warn | LogLevel::Error))
344 );
345 }
346
347 #[test]
348 fn query_by_time_range() {
349 let service = LogService::new(100);
350 let before = Utc::now();
351 service.ingest(info_entry("in-range"));
352 let after = Utc::now();
353
354 let results = service.query(&LogQuery {
355 since: Some(before),
356 until: Some(after),
357 limit: 10,
358 ..Default::default()
359 });
360 assert_eq!(results.len(), 1);
361 }
362
363 #[test]
364 fn query_by_trace_id() {
365 let service = LogService::new(100);
366 service.ingest(info_entry("traced").with_trace_id("trace-abc"));
367 service.ingest(info_entry("untraced"));
368 service.ingest(info_entry("other-trace").with_trace_id("trace-def"));
369
370 let results = service.query(&LogQuery {
371 trace_id: Some("trace-abc".into()),
372 limit: 10,
373 ..Default::default()
374 });
375 assert_eq!(results.len(), 1);
376 assert_eq!(results[0].message, "traced");
377 }
378
379 #[test]
380 fn query_limit() {
381 let service = LogService::new(100);
382 for i in 0..10 {
383 service.ingest(info_entry(&format!("msg-{i}")));
384 }
385
386 let results = service.query(&LogQuery::new(3));
387 assert_eq!(results.len(), 3);
388 }
389
390 #[tokio::test]
391 async fn subscriber_receives_entries() {
392 let service = LogService::new(100);
393 let (_id, mut rx) = service.subscribe();
394
395 service.ingest(info_entry("sub-msg-1"));
396 service.ingest(info_entry("sub-msg-2"));
397
398 let m1 = rx.recv().await.unwrap();
399 let m2 = rx.recv().await.unwrap();
400 assert_eq!(m1.message, "sub-msg-1");
401 assert_eq!(m2.message, "sub-msg-2");
402 }
403
404 #[test]
405 fn unsubscribe() {
406 let service = LogService::new(100);
407 let (id, _rx) = service.subscribe();
408 assert_eq!(service.subscriber_count(), 1);
409
410 service.unsubscribe(&id);
411 assert_eq!(service.subscriber_count(), 0);
412 }
413
414 #[test]
415 fn dead_subscriber_removed() {
416 let service = LogService::new(100);
417 let (_id, rx) = service.subscribe();
418 assert_eq!(service.subscriber_count(), 1);
419
420 drop(rx);
422
423 service.ingest(info_entry("after-drop"));
425 assert_eq!(service.subscriber_count(), 0);
426 }
427
428 #[test]
429 fn clear() {
430 let service = LogService::new(100);
431 service.ingest(info_entry("a"));
432 service.ingest(info_entry("b"));
433 assert_eq!(service.len(), 2);
434
435 service.clear();
436 assert!(service.is_empty());
437 }
438
439 #[test]
440 fn log_convenience() {
441 let service = LogService::new(100);
442 service.log(LogLevel::Warn, "warning!", Some(42));
443
444 let results = service.query(&LogQuery {
445 pid: Some(42),
446 limit: 10,
447 ..Default::default()
448 });
449 assert_eq!(results.len(), 1);
450 assert_eq!(results[0].message, "warning!");
451 assert!(matches!(results[0].level, LogLevel::Warn));
452 }
453
454 #[test]
455 fn log_entry_builder() {
456 let entry = LogEntry::new(LogLevel::Info, "test")
457 .with_pid(5)
458 .with_service("auth")
459 .with_trace_id("t-123")
460 .with_field("key", serde_json::json!("value"));
461
462 assert_eq!(entry.source_pid, Some(5));
463 assert_eq!(entry.source_service.as_deref(), Some("auth"));
464 assert_eq!(entry.trace_id.as_deref(), Some("t-123"));
465 assert_eq!(
466 entry.fields.get("key").unwrap(),
467 &serde_json::json!("value")
468 );
469 }
470
471 #[test]
472 fn log_entry_serde_roundtrip() {
473 let entry = LogEntry::new(LogLevel::Error, "boom")
474 .with_pid(7)
475 .with_service("db")
476 .with_trace_id("trace-999")
477 .with_field("code", serde_json::json!(500));
478
479 let json = serde_json::to_string(&entry).unwrap();
480 let restored: LogEntry = serde_json::from_str(&json).unwrap();
481 assert_eq!(restored.message, "boom");
482 assert_eq!(restored.source_pid, Some(7));
483 assert_eq!(restored.fields["code"], serde_json::json!(500));
484 }
485
486 #[test]
487 fn default_service() {
488 let service = LogService::default();
489 assert_eq!(service.capacity(), DEFAULT_LOG_CAPACITY);
490 }
491
492 #[test]
493 fn log_query_default() {
494 let query = LogQuery::default();
495 assert!(query.pid.is_none());
496 assert_eq!(query.limit, 0);
497 }
498}