1use std::collections::VecDeque;
19use std::fmt::Write as _;
20use std::sync::{Arc, OnceLock};
21use std::time::{SystemTime, UNIX_EPOCH};
22
23use parking_lot::RwLock;
24use serde::Serialize;
25use tracing::field::{Field, Visit};
26use tracing::{Event, Level, Subscriber};
27use tracing_subscriber::layer::Context;
28use tracing_subscriber::Layer;
29
30pub const DEFAULT_CAPACITY: usize = 2_000;
32
33pub const CAPACITY_ENV_VAR: &str = "FEAGI_LOG_RING_BUFFER_CAPACITY";
35
36#[derive(Debug, Clone, Serialize)]
38pub struct LogRecord {
39 pub timestamp_ms: i64,
41 pub level: String,
43 pub target: String,
45 pub file: String,
47 pub line: u32,
49 pub message: String,
51 #[serde(skip_serializing_if = "Option::is_none")]
53 pub fields: Option<serde_json::Value>,
54}
55
56pub struct LogRingBuffer {
58 inner: RwLock<VecDeque<LogRecord>>,
59 capacity: usize,
60}
61
62impl LogRingBuffer {
63 pub fn new(capacity: usize) -> Self {
66 let initial_capacity = capacity.min(64);
67 Self {
68 inner: RwLock::new(VecDeque::with_capacity(initial_capacity)),
69 capacity,
70 }
71 }
72
73 pub fn capacity(&self) -> usize {
75 self.capacity
76 }
77
78 pub fn push(&self, record: LogRecord) {
80 if self.capacity == 0 {
81 return;
82 }
83 let mut inner = self.inner.write();
84 if inner.len() == self.capacity {
85 inner.pop_front();
86 }
87 inner.push_back(record);
88 }
89
90 pub fn snapshot(
98 &self,
99 since_ts_ms: Option<i64>,
100 min_level: Option<&str>,
101 target_prefix: Option<&str>,
102 limit: Option<usize>,
103 ) -> Vec<LogRecord> {
104 let inner = self.inner.read();
105 let min_rank = min_level.and_then(level_rank);
106 let mut filtered: Vec<LogRecord> = inner
107 .iter()
108 .filter(|r| match since_ts_ms {
109 Some(ts) => r.timestamp_ms >= ts,
110 None => true,
111 })
112 .filter(|r| match (min_rank, level_rank(&r.level)) {
113 (Some(min), Some(level)) => level >= min,
114 _ => true,
115 })
116 .filter(|r| match target_prefix {
117 Some(prefix) => r.target.starts_with(prefix),
118 None => true,
119 })
120 .cloned()
121 .collect();
122
123 if let Some(n) = limit {
124 if filtered.len() > n {
125 let skip = filtered.len() - n;
126 filtered.drain(0..skip);
127 }
128 }
129 filtered
130 }
131
132 pub fn clear(&self) {
134 self.inner.write().clear();
135 }
136}
137
138fn level_rank(level: &str) -> Option<u8> {
141 match level.to_ascii_uppercase().as_str() {
142 "TRACE" => Some(0),
143 "DEBUG" => Some(1),
144 "INFO" => Some(2),
145 "WARN" | "WARNING" => Some(3),
146 "ERROR" => Some(4),
147 _ => None,
148 }
149}
150
151fn level_to_str(level: &Level) -> &'static str {
152 match *level {
153 Level::TRACE => "TRACE",
154 Level::DEBUG => "DEBUG",
155 Level::INFO => "INFO",
156 Level::WARN => "WARN",
157 Level::ERROR => "ERROR",
158 }
159}
160
161fn now_unix_ms() -> i64 {
162 SystemTime::now()
163 .duration_since(UNIX_EPOCH)
164 .map(|d| d.as_millis() as i64)
165 .unwrap_or(0)
166}
167
168pub struct RingBufferLayer {
170 buffer: Arc<LogRingBuffer>,
171}
172
173impl RingBufferLayer {
174 pub fn new(buffer: Arc<LogRingBuffer>) -> Self {
175 Self { buffer }
176 }
177}
178
179impl<S: Subscriber> Layer<S> for RingBufferLayer {
180 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
181 if self.buffer.capacity() == 0 {
182 return;
183 }
184 let metadata = event.metadata();
185 let mut visitor = MessageVisitor::default();
186 event.record(&mut visitor);
187
188 let fields = if visitor.extra_fields.is_empty() {
189 None
190 } else {
191 Some(serde_json::Value::Object(visitor.extra_fields))
192 };
193
194 let record = LogRecord {
195 timestamp_ms: now_unix_ms(),
196 level: level_to_str(metadata.level()).to_string(),
197 target: metadata.target().to_string(),
198 file: metadata.file().unwrap_or_default().to_string(),
199 line: metadata.line().unwrap_or(0),
200 message: visitor.message,
201 fields,
202 };
203 self.buffer.push(record);
204 }
205}
206
207#[derive(Default)]
209struct MessageVisitor {
210 message: String,
211 extra_fields: serde_json::Map<String, serde_json::Value>,
212}
213
214impl Visit for MessageVisitor {
215 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
216 if field.name() == "message" {
217 let _ = write!(&mut self.message, "{:?}", value);
219 } else {
220 self.extra_fields.insert(
221 field.name().to_string(),
222 serde_json::Value::String(format!("{:?}", value)),
223 );
224 }
225 }
226
227 fn record_str(&mut self, field: &Field, value: &str) {
228 if field.name() == "message" {
229 self.message.push_str(value);
230 } else {
231 self.extra_fields.insert(
232 field.name().to_string(),
233 serde_json::Value::String(value.to_string()),
234 );
235 }
236 }
237
238 fn record_i64(&mut self, field: &Field, value: i64) {
239 self.extra_fields
240 .insert(field.name().to_string(), serde_json::json!(value));
241 }
242
243 fn record_u64(&mut self, field: &Field, value: u64) {
244 self.extra_fields
245 .insert(field.name().to_string(), serde_json::json!(value));
246 }
247
248 fn record_f64(&mut self, field: &Field, value: f64) {
249 self.extra_fields
250 .insert(field.name().to_string(), serde_json::json!(value));
251 }
252
253 fn record_bool(&mut self, field: &Field, value: bool) {
254 self.extra_fields
255 .insert(field.name().to_string(), serde_json::json!(value));
256 }
257}
258
259static GLOBAL_RING: OnceLock<Arc<LogRingBuffer>> = OnceLock::new();
260
261pub fn install_global_ring(capacity: usize) -> Arc<LogRingBuffer> {
264 GLOBAL_RING
265 .get_or_init(|| Arc::new(LogRingBuffer::new(capacity)))
266 .clone()
267}
268
269pub fn capacity_from_env() -> usize {
272 std::env::var(CAPACITY_ENV_VAR)
273 .ok()
274 .and_then(|v| v.parse::<usize>().ok())
275 .unwrap_or(DEFAULT_CAPACITY)
276}
277
278pub fn global_ring() -> Option<Arc<LogRingBuffer>> {
280 GLOBAL_RING.get().cloned()
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn ring_buffer_respects_capacity() {
289 let ring = LogRingBuffer::new(3);
290 for i in 0..5 {
291 ring.push(LogRecord {
292 timestamp_ms: i as i64,
293 level: "INFO".into(),
294 target: "test".into(),
295 file: String::new(),
296 line: 0,
297 message: format!("msg-{}", i),
298 fields: None,
299 });
300 }
301 let snap = ring.snapshot(None, None, None, None);
302 assert_eq!(snap.len(), 3);
303 assert_eq!(snap[0].message, "msg-2");
304 assert_eq!(snap[2].message, "msg-4");
305 }
306
307 #[test]
308 fn snapshot_filters_by_level_and_target() {
309 let ring = LogRingBuffer::new(10);
310 ring.push(LogRecord {
311 timestamp_ms: 100,
312 level: "DEBUG".into(),
313 target: "feagi-api".into(),
314 file: String::new(),
315 line: 0,
316 message: "debug".into(),
317 fields: None,
318 });
319 ring.push(LogRecord {
320 timestamp_ms: 200,
321 level: "ERROR".into(),
322 target: "feagi-burst-engine".into(),
323 file: String::new(),
324 line: 0,
325 message: "error".into(),
326 fields: None,
327 });
328 ring.push(LogRecord {
329 timestamp_ms: 300,
330 level: "WARN".into(),
331 target: "feagi-api".into(),
332 file: String::new(),
333 line: 0,
334 message: "warn".into(),
335 fields: None,
336 });
337
338 let warnings = ring.snapshot(None, Some("warn"), None, None);
339 assert_eq!(warnings.len(), 2);
340
341 let api_only = ring.snapshot(None, None, Some("feagi-api"), None);
342 assert_eq!(api_only.len(), 2);
343 assert!(api_only.iter().all(|r| r.target.starts_with("feagi-api")));
344
345 let recent = ring.snapshot(Some(250), None, None, None);
346 assert_eq!(recent.len(), 1);
347 assert_eq!(recent[0].timestamp_ms, 300);
348
349 let limited = ring.snapshot(None, None, None, Some(2));
350 assert_eq!(limited.len(), 2);
351 assert_eq!(limited.last().unwrap().timestamp_ms, 300);
352 }
353
354 #[test]
355 fn capacity_zero_disables_buffer() {
356 let ring = LogRingBuffer::new(0);
357 ring.push(LogRecord {
358 timestamp_ms: 0,
359 level: "INFO".into(),
360 target: "test".into(),
361 file: String::new(),
362 line: 0,
363 message: "x".into(),
364 fields: None,
365 });
366 assert!(ring.snapshot(None, None, None, None).is_empty());
367 }
368}