1use dashmap::DashMap;
17use std::collections::VecDeque;
18use std::sync::Arc;
19use std::sync::Mutex;
20use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21use std::time::Duration;
22use tokio::sync::broadcast;
23
24pub const RING_CAPACITY: usize = 10_000;
27
28pub const BROADCAST_CAPACITY: usize = 1024;
31
32pub const LOG_DRAIN: Duration = Duration::from_secs(60);
36
37#[derive(Clone, Debug, PartialEq, Eq)]
40pub struct LogLine {
41 pub seq: u64,
42 pub line: String,
43}
44
45#[derive(Clone, Debug)]
47pub enum LogMsg {
48 Line(LogLine),
50 End,
52}
53
54struct RunBuffer {
56 ring: Mutex<VecDeque<LogLine>>,
57 seq: AtomicU64,
58 tx: broadcast::Sender<LogMsg>,
59 ended: AtomicBool,
60}
61
62impl RunBuffer {
63 fn new() -> Self {
64 let (tx, _rx) = broadcast::channel(BROADCAST_CAPACITY);
65 Self {
66 ring: Mutex::new(VecDeque::with_capacity(64)),
67 seq: AtomicU64::new(0),
68 tx,
69 ended: AtomicBool::new(false),
70 }
71 }
72
73 fn push(&self, line: String) {
76 let seq = self.seq.fetch_add(1, Ordering::Relaxed);
77 let entry = LogLine { seq, line };
78 {
79 let mut ring = self.ring.lock().expect("log ring poisoned");
80 if ring.len() == RING_CAPACITY {
81 ring.pop_front();
82 }
83 ring.push_back(entry.clone());
84 }
85 let _ = self.tx.send(LogMsg::Line(entry));
87 }
88
89 fn snapshot(&self) -> Vec<LogLine> {
90 self.ring
91 .lock()
92 .expect("log ring poisoned")
93 .iter()
94 .cloned()
95 .collect()
96 }
97
98 fn finish(&self) {
102 self.ended.store(true, Ordering::SeqCst);
103 let _ = self.tx.send(LogMsg::End);
104 }
105}
106
107#[derive(Clone, Default)]
111pub struct LogHub {
112 inner: Arc<DashMap<String, Arc<RunBuffer>>>,
113}
114
115impl LogHub {
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 fn buffer(&self, run_id: &str) -> Arc<RunBuffer> {
122 if let Some(b) = self.inner.get(run_id) {
123 return Arc::clone(b.value());
124 }
125 Arc::clone(
126 self.inner
127 .entry(run_id.to_string())
128 .or_insert_with(|| Arc::new(RunBuffer::new()))
129 .value(),
130 )
131 }
132
133 pub fn append(&self, run_id: &str, line: String) {
135 self.buffer(run_id).push(line);
136 }
137
138 pub fn reader(
147 &self,
148 run_id: &str,
149 ) -> Option<(Vec<LogLine>, broadcast::Receiver<LogMsg>, bool)> {
150 let buf = Arc::clone(self.inner.get(run_id)?.value());
151 let rx = buf.tx.subscribe();
152 let snapshot = buf.snapshot();
153 let ended = buf.ended.load(Ordering::SeqCst);
154 Some((snapshot, rx, ended))
155 }
156
157 pub fn finish(&self, run_id: &str) {
159 if let Some(buf) = self.inner.get(run_id) {
160 buf.finish();
161 }
162 }
163
164 pub fn drop_run(&self, run_id: &str) {
166 self.inner.remove(run_id);
167 }
168}
169
170#[derive(Debug, Clone, PartialEq, Eq)]
173pub enum LogEvent {
174 Log(String),
176 Truncated(u64),
178 End,
180}
181
182pub fn log_events(
186 snapshot: Vec<LogLine>,
187 mut rx: broadcast::Receiver<LogMsg>,
188 ended: bool,
189) -> impl futures::Stream<Item = LogEvent> {
190 use tokio::sync::broadcast::error::RecvError;
191 async_stream::stream! {
192 let mut last_seq: Option<u64> = None;
193 for entry in snapshot {
194 last_seq = Some(entry.seq);
195 yield LogEvent::Log(entry.line);
196 }
197 if ended {
199 yield LogEvent::End;
200 return;
201 }
202 loop {
203 match rx.recv().await {
204 Ok(LogMsg::Line(entry)) => {
205 if last_seq.is_none_or(|s| entry.seq > s) {
206 last_seq = Some(entry.seq);
207 yield LogEvent::Log(entry.line);
208 }
209 }
210 Ok(LogMsg::End) => {
211 yield LogEvent::End;
212 break;
213 }
214 Err(RecvError::Lagged(n)) => {
215 yield LogEvent::Truncated(n);
216 }
217 Err(RecvError::Closed) => {
218 yield LogEvent::End;
219 break;
220 }
221 }
222 }
223 }
224}
225
226use tracing::field::{Field, Visit};
229use tracing::span::{Attributes, Id};
230use tracing::{Event, Subscriber};
231use tracing_subscriber::layer::{Context, Layer};
232use tracing_subscriber::registry::LookupSpan;
233
234#[derive(Clone)]
237struct RunIdExt(String);
238
239#[derive(Default)]
241struct RunIdVisitor(Option<String>);
242
243impl Visit for RunIdVisitor {
244 fn record_str(&mut self, field: &Field, value: &str) {
245 if field.name() == "serve_run_id" {
246 self.0 = Some(value.to_string());
247 }
248 }
249 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
250 if field.name() == "serve_run_id" && self.0.is_none() {
252 self.0 = Some(format!("{value:?}"));
253 }
254 }
255}
256
257#[derive(Default)]
260struct EventLineVisitor {
261 message: String,
262 fields: String,
263}
264
265impl Visit for EventLineVisitor {
266 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
267 use std::fmt::Write;
268 if field.name() == "message" {
269 let _ = write!(self.message, "{value:?}");
270 } else {
271 let _ = write!(self.fields, " {}={:?}", field.name(), value);
272 }
273 }
274}
275
276impl EventLineVisitor {
277 fn finish(self) -> String {
278 if self.fields.is_empty() {
279 self.message
280 } else {
281 format!("{}{}", self.message, self.fields)
282 }
283 }
284}
285
286pub struct RunLogLayer {
290 hub: LogHub,
291}
292
293impl RunLogLayer {
294 pub fn new(hub: LogHub) -> Self {
295 Self { hub }
296 }
297}
298
299impl<S> Layer<S> for RunLogLayer
300where
301 S: Subscriber + for<'a> LookupSpan<'a>,
302{
303 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
304 let mut visitor = RunIdVisitor::default();
305 attrs.record(&mut visitor);
306 if let Some(run_id) = visitor.0
307 && let Some(span) = ctx.span(id)
308 {
309 span.extensions_mut().insert(RunIdExt(run_id));
310 }
311 }
312
313 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
314 let Some(run_id) = ctx.event_scope(event).and_then(|scope| {
315 scope
316 .from_root()
317 .find_map(|span| span.extensions().get::<RunIdExt>().map(|ext| ext.0.clone()))
318 }) else {
319 return;
320 };
321 let mut visitor = EventLineVisitor::default();
322 event.record(&mut visitor);
323 let meta = event.metadata();
324 let line = format!("{} {}: {}", meta.level(), meta.target(), visitor.finish());
325 let line = crate::secrets::registry::redact(&line).into_owned();
326 self.hub.append(&run_id, line);
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use futures::StreamExt;
334
335 #[test]
336 fn ring_caps_and_orders_by_seq() {
337 let hub = LogHub::new();
338 for i in 0..(RING_CAPACITY + 5) {
339 hub.append("r", format!("line-{i}"));
340 }
341 let (snapshot, _rx, _ended) = hub.reader("r").unwrap();
342 assert_eq!(snapshot.len(), RING_CAPACITY, "ring must cap at capacity");
343 assert_eq!(snapshot.first().unwrap().line, "line-5");
345 assert!(
346 snapshot.windows(2).all(|w| w[0].seq < w[1].seq),
347 "sequence numbers must be strictly increasing"
348 );
349 }
350
351 #[test]
352 fn reader_none_for_unknown_run() {
353 let hub = LogHub::new();
354 assert!(hub.reader("nope").is_none());
355 }
356
357 #[test]
358 fn finish_sets_ended_flag() {
359 let hub = LogHub::new();
360 hub.append("r", "x".into());
361 hub.finish("r");
362 let (snapshot, _rx, ended) = hub.reader("r").unwrap();
363 assert!(ended, "reader must observe ended after finish");
364 assert_eq!(snapshot.len(), 1);
365 }
366
367 #[test]
368 fn drop_run_frees_buffer() {
369 let hub = LogHub::new();
370 hub.append("r", "x".into());
371 assert!(hub.reader("r").is_some());
372 hub.drop_run("r");
373 assert!(hub.reader("r").is_none());
374 }
375
376 #[tokio::test]
377 async fn ended_buffer_streams_snapshot_then_end() {
378 let hub = LogHub::new();
379 hub.append("r", "a".into());
380 hub.append("r", "b".into());
381 hub.finish("r");
382 let (snapshot, rx, ended) = hub.reader("r").unwrap();
383 let events: Vec<LogEvent> = log_events(snapshot, rx, ended).collect().await;
384 assert_eq!(
385 events,
386 vec![
387 LogEvent::Log("a".into()),
388 LogEvent::Log("b".into()),
389 LogEvent::End
390 ]
391 );
392 }
393
394 #[tokio::test]
395 async fn snapshot_then_live_dedups_by_seq() {
396 let (tx, rx) = broadcast::channel(8);
397 let _ = tx.send(LogMsg::Line(LogLine {
399 seq: 0,
400 line: "a".into(),
401 }));
402 let _ = tx.send(LogMsg::Line(LogLine {
403 seq: 1,
404 line: "b".into(),
405 }));
406 let _ = tx.send(LogMsg::End);
407 let snapshot = vec![LogLine {
408 seq: 0,
409 line: "a".into(),
410 }];
411 let events: Vec<LogEvent> = log_events(snapshot, rx, false).collect().await;
412 assert_eq!(
413 events,
414 vec![
415 LogEvent::Log("a".into()),
416 LogEvent::Log("b".into()),
417 LogEvent::End
418 ]
419 );
420 }
421
422 #[tokio::test]
423 async fn truncated_emitted_on_broadcast_lag() {
424 let (tx, rx) = broadcast::channel(2);
426 for i in 0..5u64 {
427 let _ = tx.send(LogMsg::Line(LogLine {
428 seq: i,
429 line: format!("l{i}"),
430 }));
431 }
432 let _ = tx.send(LogMsg::End);
433 let events: Vec<LogEvent> = log_events(vec![], rx, false).collect().await;
434 assert!(
435 events.iter().any(|e| matches!(e, LogEvent::Truncated(_))),
436 "a lagging reader must get a Truncated event: {events:?}"
437 );
438 assert_eq!(events.last(), Some(&LogEvent::End));
439 }
440
441 #[test]
442 fn layer_captures_events_in_run_span_only() {
443 use tracing_subscriber::layer::SubscriberExt;
444 let hub = LogHub::new();
445 let subscriber = tracing_subscriber::registry().with(RunLogLayer::new(hub.clone()));
446 tracing::subscriber::with_default(subscriber, || {
447 tracing::info!("orphan event");
449 let span = tracing::info_span!("faucet.serve.run", serve_run_id = "run-xyz");
450 let _g = span.enter();
451 tracing::info!("hello from the run");
452 });
453 let (snapshot, _rx, _ended) = hub.reader("run-xyz").expect("buffer for the run exists");
454 assert!(
455 snapshot
456 .iter()
457 .any(|l| l.line.contains("hello from the run")),
458 "in-span event must be captured: {snapshot:?}"
459 );
460 assert!(
461 snapshot.iter().all(|l| !l.line.contains("orphan")),
462 "events outside the run span must not be captured"
463 );
464 assert!(hub.reader("nonexistent").is_none());
466 }
467}