1use std::collections::VecDeque;
17use std::sync::{Arc, Mutex, OnceLock};
18use std::time::{SystemTime, UNIX_EPOCH};
19
20use tracing::{Event, Subscriber};
21use tracing_subscriber::Layer;
22use tracing_subscriber::layer::Context;
23use tracing_subscriber::registry::LookupSpan;
24
25#[derive(Clone, Debug)]
27pub struct LogEntry {
28 pub ts: String,
30 pub method: String,
31 pub url: String,
32 pub status: Option<u16>,
33 pub elapsed_ms: Option<u64>,
34 pub message: String,
37}
38
39#[derive(Clone, Debug)]
41pub struct LogCapture {
42 inner: Arc<Mutex<VecDeque<LogEntry>>>,
43 capacity: usize,
44}
45
46impl LogCapture {
47 pub fn new(capacity: usize) -> Self {
48 Self {
49 inner: Arc::new(Mutex::new(VecDeque::with_capacity(capacity))),
50 capacity,
51 }
52 }
53
54 pub fn push(&self, entry: LogEntry) {
56 let mut buf = self.inner.lock().expect("log capture mutex poisoned");
57 if buf.len() == self.capacity {
58 buf.pop_front();
59 }
60 buf.push_back(entry);
61 }
62
63 pub fn snapshot(&self) -> Vec<LogEntry> {
66 let buf = self.inner.lock().expect("log capture mutex poisoned");
67 buf.iter().cloned().collect()
68 }
69}
70
71pub struct CaptureLayer {
75 capture: LogCapture,
76}
77
78impl CaptureLayer {
79 pub fn new(capture: LogCapture) -> Self {
80 Self { capture }
81 }
82}
83
84impl<S> Layer<S> for CaptureLayer
85where
86 S: Subscriber + for<'a> LookupSpan<'a>,
87{
88 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
89 if event.metadata().target() != "bee::http" {
90 return;
91 }
92 let mut v = FieldVisitor::default();
93 event.record(&mut v);
94 self.capture.push(LogEntry {
95 ts: format_now_hms(),
96 method: v.method.unwrap_or_default(),
97 url: v.url.unwrap_or_default(),
98 status: v.status,
99 elapsed_ms: v.elapsed_ms,
100 message: v.message.unwrap_or_default(),
101 });
102 }
103}
104
105#[derive(Default)]
106struct FieldVisitor {
107 method: Option<String>,
108 url: Option<String>,
109 status: Option<u16>,
110 elapsed_ms: Option<u64>,
111 message: Option<String>,
112}
113
114impl tracing::field::Visit for FieldVisitor {
115 fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
116 match field.name() {
117 "method" => self.method = Some(value.to_string()),
118 "url" => self.url = Some(value.to_string()),
119 _ => {}
120 }
121 }
122
123 fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
124 match field.name() {
125 "status" => self.status = Some(value as u16),
126 "elapsed_ms" => self.elapsed_ms = Some(value),
127 _ => {}
128 }
129 }
130
131 fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
132 if value >= 0 {
133 self.record_u64(field, value as u64);
134 }
135 }
136
137 fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
138 if field.name() == "message" {
139 self.message = Some(format!("{value:?}").trim_matches('"').to_string());
140 } else if field.name() == "method" && self.method.is_none() {
141 self.method = Some(format!("{value:?}").trim_matches('"').to_string());
142 } else if field.name() == "url" && self.url.is_none() {
143 self.url = Some(format!("{value:?}").trim_matches('"').to_string());
144 }
145 }
146}
147
148static GLOBAL: OnceLock<LogCapture> = OnceLock::new();
149
150pub fn install(capacity: usize) -> LogCapture {
154 GLOBAL.get_or_init(|| LogCapture::new(capacity)).clone()
155}
156
157pub fn handle() -> Option<LogCapture> {
160 GLOBAL.get().cloned()
161}
162
163fn format_now_hms() -> String {
164 let secs = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .map(|d| d.as_secs())
167 .unwrap_or_default();
168 let in_day = secs % 86_400;
169 let h = in_day / 3600;
170 let m = (in_day / 60) % 60;
171 let s = in_day % 60;
172 format!("{h:02}:{m:02}:{s:02}")
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn ring_buffer_evicts_oldest() {
181 let cap = LogCapture::new(2);
182 for i in 0..3 {
183 cap.push(LogEntry {
184 ts: format!("00:00:{i:02}"),
185 method: "GET".into(),
186 url: format!("/{i}"),
187 status: Some(200),
188 elapsed_ms: Some(i),
189 message: "test".into(),
190 });
191 }
192 let snap = cap.snapshot();
193 assert_eq!(snap.len(), 2);
194 assert_eq!(snap[0].url, "/1");
195 assert_eq!(snap[1].url, "/2");
196 }
197
198 #[test]
199 fn install_returns_same_handle_on_second_call() {
200 let a = install(123);
203 let b = install(456);
204 assert!(Arc::ptr_eq(&a.inner, &b.inner));
206 }
207
208 #[test]
209 fn format_now_hms_is_eight_chars() {
210 assert_eq!(format_now_hms().len(), 8);
211 }
212}