1use crate::serve::history::{RUN_LOG_TRUNCATED_SEQ, RunHistory, RunLogLine};
17use dashmap::DashMap;
18use std::collections::{HashMap, VecDeque};
19use std::sync::Arc;
20use std::sync::Mutex;
21use std::sync::OnceLock;
22use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
23use std::time::Duration;
24use tokio::sync::{broadcast, mpsc};
25
26pub const RING_CAPACITY: usize = 10_000;
29
30pub const BROADCAST_CAPACITY: usize = 1024;
33
34pub const LOG_DRAIN: Duration = Duration::from_secs(60);
38
39const PERSIST_CHANNEL_CAPACITY: usize = 16_384;
43
44const PERSIST_BATCH: usize = 256;
47
48enum PersistMsg {
51 Line {
52 run_id: String,
53 seq: u64,
54 ts: String,
55 level: String,
56 line: String,
57 },
58 End {
59 run_id: String,
60 },
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
66pub struct LogLine {
67 pub seq: u64,
68 pub line: String,
69}
70
71#[derive(Clone, Debug)]
73pub enum LogMsg {
74 Line(LogLine),
76 End,
78}
79
80struct RunBuffer {
82 ring: Mutex<VecDeque<LogLine>>,
83 seq: AtomicU64,
84 tx: broadcast::Sender<LogMsg>,
85 ended: AtomicBool,
86}
87
88impl RunBuffer {
89 fn new() -> Self {
90 let (tx, _rx) = broadcast::channel(BROADCAST_CAPACITY);
91 Self {
92 ring: Mutex::new(VecDeque::with_capacity(64)),
93 seq: AtomicU64::new(0),
94 tx,
95 ended: AtomicBool::new(false),
96 }
97 }
98
99 fn push(&self, line: String) -> u64 {
103 let seq = self.seq.fetch_add(1, Ordering::Relaxed);
104 let entry = LogLine { seq, line };
105 {
106 let mut ring = self.ring.lock().expect("log ring poisoned");
107 if ring.len() == RING_CAPACITY {
108 ring.pop_front();
109 }
110 ring.push_back(entry.clone());
111 }
112 let _ = self.tx.send(LogMsg::Line(entry));
114 seq
115 }
116
117 fn snapshot(&self) -> Vec<LogLine> {
118 self.ring
119 .lock()
120 .expect("log ring poisoned")
121 .iter()
122 .cloned()
123 .collect()
124 }
125
126 fn finish(&self) {
130 self.ended.store(true, Ordering::SeqCst);
131 let _ = self.tx.send(LogMsg::End);
132 }
133}
134
135#[derive(Clone, Default)]
139pub struct LogHub {
140 inner: Arc<DashMap<String, Arc<RunBuffer>>>,
141 persist: Arc<OnceLock<mpsc::Sender<PersistMsg>>>,
145}
146
147impl LogHub {
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 fn buffer(&self, run_id: &str) -> Arc<RunBuffer> {
154 if let Some(b) = self.inner.get(run_id) {
155 return Arc::clone(b.value());
156 }
157 Arc::clone(
158 self.inner
159 .entry(run_id.to_string())
160 .or_insert_with(|| Arc::new(RunBuffer::new()))
161 .value(),
162 )
163 }
164
165 pub fn enable_persistence(&self, history: Arc<dyn RunHistory>, max_lines_per_run: usize) {
170 let (tx, rx) = mpsc::channel(PERSIST_CHANNEL_CAPACITY);
171 if self.persist.set(tx).is_err() {
172 return; }
174 tokio::spawn(persist_writer(rx, history, max_lines_per_run.max(1)));
175 }
176
177 pub fn capture(&self, run_id: &str, level: &str, ts: String, line: String) {
181 let seq = self.buffer(run_id).push(line.clone());
182 if let Some(tx) = self.persist.get()
183 && tx
184 .try_send(PersistMsg::Line {
185 run_id: run_id.to_string(),
186 seq,
187 ts,
188 level: level.to_string(),
189 line,
190 })
191 .is_err()
192 {
193 metrics::counter!("faucet_serve_run_logs_dropped_total", "reason" => "queue_full")
194 .increment(1);
195 }
196 }
197
198 pub fn append(&self, run_id: &str, line: String) {
201 self.buffer(run_id).push(line);
202 }
203
204 pub fn reader(
213 &self,
214 run_id: &str,
215 ) -> Option<(Vec<LogLine>, broadcast::Receiver<LogMsg>, bool)> {
216 let buf = Arc::clone(self.inner.get(run_id)?.value());
217 let rx = buf.tx.subscribe();
218 let snapshot = buf.snapshot();
219 let ended = buf.ended.load(Ordering::SeqCst);
220 Some((snapshot, rx, ended))
221 }
222
223 pub fn finish(&self, run_id: &str) {
226 if let Some(buf) = self.inner.get(run_id) {
227 buf.finish();
228 }
229 if let Some(tx) = self.persist.get() {
230 let _ = tx.try_send(PersistMsg::End {
231 run_id: run_id.to_string(),
232 });
233 }
234 }
235
236 pub fn drop_run(&self, run_id: &str) {
238 self.inner.remove(run_id);
239 }
240}
241
242#[derive(Default)]
244struct RunPersistState {
245 pending: Vec<RunLogLine>,
247 persisted: u64,
249 truncated: bool,
251}
252
253async fn persist_writer(
258 mut rx: mpsc::Receiver<PersistMsg>,
259 history: Arc<dyn RunHistory>,
260 max_lines_per_run: usize,
261) {
262 let cap = max_lines_per_run as u64;
263 let mut runs: HashMap<String, RunPersistState> = HashMap::new();
264
265 async fn flush(history: &Arc<dyn RunHistory>, run_id: &str, st: &mut RunPersistState) {
266 if st.pending.is_empty() {
267 return;
268 }
269 let batch = std::mem::take(&mut st.pending);
270 let n = batch.len() as u64;
271 if let Err(e) = history.record_run_logs(run_id, &batch).await {
272 tracing::warn!(run_id, error = %e, "persisting run logs failed");
273 metrics::counter!("faucet_serve_run_logs_dropped_total", "reason" => "backend_error")
274 .increment(n);
275 } else {
276 metrics::counter!("faucet_serve_run_log_lines_total").increment(n);
277 }
278 }
279
280 while let Some(msg) = rx.recv().await {
281 match msg {
282 PersistMsg::Line {
283 run_id,
284 seq,
285 ts,
286 level,
287 line,
288 } => {
289 let st = runs.entry(run_id.clone()).or_default();
290 if st.persisted >= cap {
291 if !st.truncated {
292 st.truncated = true;
293 metrics::counter!(
294 "faucet_serve_run_logs_dropped_total", "reason" => "per_run_cap"
295 )
296 .increment(1);
297 }
298 continue;
299 }
300 st.persisted += 1;
301 st.pending.push(RunLogLine {
302 seq,
303 ts,
304 level,
305 line,
306 });
307 if st.pending.len() >= PERSIST_BATCH {
308 flush(&history, &run_id, st).await;
309 }
310 }
311 PersistMsg::End { run_id } => {
312 if let Some(mut st) = runs.remove(&run_id) {
313 flush(&history, &run_id, &mut st).await;
314 if st.truncated {
315 let marker = [RunLogLine {
317 seq: RUN_LOG_TRUNCATED_SEQ,
318 ts: String::new(),
319 level: "WARN".to_string(),
320 line: "log truncated: per-run cap reached".to_string(),
321 }];
322 if let Err(e) = history.record_run_logs(&run_id, &marker).await {
323 tracing::warn!(run_id, error = %e, "persisting run-log truncation marker failed");
324 }
325 }
326 }
327 }
328 }
329 }
330}
331
332#[derive(Debug, Clone, PartialEq, Eq)]
335pub enum LogEvent {
336 Log(String),
338 Truncated(u64),
340 End,
342}
343
344pub fn log_events(
348 snapshot: Vec<LogLine>,
349 mut rx: broadcast::Receiver<LogMsg>,
350 ended: bool,
351) -> impl futures::Stream<Item = LogEvent> {
352 use tokio::sync::broadcast::error::RecvError;
353 async_stream::stream! {
354 let mut last_seq: Option<u64> = None;
355 for entry in snapshot {
356 last_seq = Some(entry.seq);
357 yield LogEvent::Log(entry.line);
358 }
359 if ended {
361 yield LogEvent::End;
362 return;
363 }
364 loop {
365 match rx.recv().await {
366 Ok(LogMsg::Line(entry)) => {
367 if last_seq.is_none_or(|s| entry.seq > s) {
368 last_seq = Some(entry.seq);
369 yield LogEvent::Log(entry.line);
370 }
371 }
372 Ok(LogMsg::End) => {
373 yield LogEvent::End;
374 break;
375 }
376 Err(RecvError::Lagged(n)) => {
377 yield LogEvent::Truncated(n);
378 }
379 Err(RecvError::Closed) => {
380 yield LogEvent::End;
381 break;
382 }
383 }
384 }
385 }
386}
387
388use tracing::field::{Field, Visit};
391use tracing::span::{Attributes, Id};
392use tracing::{Event, Subscriber};
393use tracing_subscriber::layer::{Context, Layer};
394use tracing_subscriber::registry::LookupSpan;
395
396#[derive(Clone)]
399struct RunIdExt(String);
400
401#[derive(Default)]
403struct RunIdVisitor(Option<String>);
404
405impl Visit for RunIdVisitor {
406 fn record_str(&mut self, field: &Field, value: &str) {
407 if field.name() == "serve_run_id" {
408 self.0 = Some(value.to_string());
409 }
410 }
411 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
412 if field.name() == "serve_run_id" && self.0.is_none() {
414 self.0 = Some(format!("{value:?}"));
415 }
416 }
417}
418
419#[derive(Default)]
422struct EventLineVisitor {
423 message: String,
424 fields: String,
425}
426
427impl Visit for EventLineVisitor {
428 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
429 use std::fmt::Write;
430 if field.name() == "message" {
431 let _ = write!(self.message, "{value:?}");
432 } else {
433 let _ = write!(self.fields, " {}={:?}", field.name(), value);
434 }
435 }
436}
437
438impl EventLineVisitor {
439 fn finish(self) -> String {
440 if self.fields.is_empty() {
441 self.message
442 } else {
443 format!("{}{}", self.message, self.fields)
444 }
445 }
446}
447
448pub struct RunLogLayer {
452 hub: LogHub,
453}
454
455impl RunLogLayer {
456 pub fn new(hub: LogHub) -> Self {
457 Self { hub }
458 }
459}
460
461impl<S> Layer<S> for RunLogLayer
462where
463 S: Subscriber + for<'a> LookupSpan<'a>,
464{
465 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
466 let mut visitor = RunIdVisitor::default();
467 attrs.record(&mut visitor);
468 if let Some(run_id) = visitor.0
469 && let Some(span) = ctx.span(id)
470 {
471 span.extensions_mut().insert(RunIdExt(run_id));
472 }
473 }
474
475 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
476 let Some(run_id) = ctx.event_scope(event).and_then(|scope| {
477 scope
478 .from_root()
479 .find_map(|span| span.extensions().get::<RunIdExt>().map(|ext| ext.0.clone()))
480 }) else {
481 return;
482 };
483 let mut visitor = EventLineVisitor::default();
484 event.record(&mut visitor);
485 let meta = event.metadata();
486 let line = format!("{} {}: {}", meta.level(), meta.target(), visitor.finish());
487 let line = crate::secrets::registry::redact(&line).into_owned();
488 let ts = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
489 self.hub.capture(&run_id, meta.level().as_str(), ts, line);
490 }
491}
492
493#[cfg(test)]
494mod tests {
495 use super::*;
496 use futures::StreamExt;
497
498 #[test]
499 fn ring_caps_and_orders_by_seq() {
500 let hub = LogHub::new();
501 for i in 0..(RING_CAPACITY + 5) {
502 hub.append("r", format!("line-{i}"));
503 }
504 let (snapshot, _rx, _ended) = hub.reader("r").unwrap();
505 assert_eq!(snapshot.len(), RING_CAPACITY, "ring must cap at capacity");
506 assert_eq!(snapshot.first().unwrap().line, "line-5");
508 assert!(
509 snapshot.windows(2).all(|w| w[0].seq < w[1].seq),
510 "sequence numbers must be strictly increasing"
511 );
512 }
513
514 #[test]
515 fn reader_none_for_unknown_run() {
516 let hub = LogHub::new();
517 assert!(hub.reader("nope").is_none());
518 }
519
520 #[test]
521 fn finish_sets_ended_flag() {
522 let hub = LogHub::new();
523 hub.append("r", "x".into());
524 hub.finish("r");
525 let (snapshot, _rx, ended) = hub.reader("r").unwrap();
526 assert!(ended, "reader must observe ended after finish");
527 assert_eq!(snapshot.len(), 1);
528 }
529
530 #[test]
531 fn drop_run_frees_buffer() {
532 let hub = LogHub::new();
533 hub.append("r", "x".into());
534 assert!(hub.reader("r").is_some());
535 hub.drop_run("r");
536 assert!(hub.reader("r").is_none());
537 }
538
539 #[tokio::test]
540 async fn ended_buffer_streams_snapshot_then_end() {
541 let hub = LogHub::new();
542 hub.append("r", "a".into());
543 hub.append("r", "b".into());
544 hub.finish("r");
545 let (snapshot, rx, ended) = hub.reader("r").unwrap();
546 let events: Vec<LogEvent> = log_events(snapshot, rx, ended).collect().await;
547 assert_eq!(
548 events,
549 vec![
550 LogEvent::Log("a".into()),
551 LogEvent::Log("b".into()),
552 LogEvent::End
553 ]
554 );
555 }
556
557 #[tokio::test]
558 async fn snapshot_then_live_dedups_by_seq() {
559 let (tx, rx) = broadcast::channel(8);
560 let _ = tx.send(LogMsg::Line(LogLine {
562 seq: 0,
563 line: "a".into(),
564 }));
565 let _ = tx.send(LogMsg::Line(LogLine {
566 seq: 1,
567 line: "b".into(),
568 }));
569 let _ = tx.send(LogMsg::End);
570 let snapshot = vec![LogLine {
571 seq: 0,
572 line: "a".into(),
573 }];
574 let events: Vec<LogEvent> = log_events(snapshot, rx, false).collect().await;
575 assert_eq!(
576 events,
577 vec![
578 LogEvent::Log("a".into()),
579 LogEvent::Log("b".into()),
580 LogEvent::End
581 ]
582 );
583 }
584
585 #[tokio::test]
586 async fn truncated_emitted_on_broadcast_lag() {
587 let (tx, rx) = broadcast::channel(2);
589 for i in 0..5u64 {
590 let _ = tx.send(LogMsg::Line(LogLine {
591 seq: i,
592 line: format!("l{i}"),
593 }));
594 }
595 let _ = tx.send(LogMsg::End);
596 let events: Vec<LogEvent> = log_events(vec![], rx, false).collect().await;
597 assert!(
598 events.iter().any(|e| matches!(e, LogEvent::Truncated(_))),
599 "a lagging reader must get a Truncated event: {events:?}"
600 );
601 assert_eq!(events.last(), Some(&LogEvent::End));
602 }
603
604 #[test]
605 fn layer_captures_events_in_run_span_only() {
606 use tracing_subscriber::layer::SubscriberExt;
607 let hub = LogHub::new();
608 let subscriber = tracing_subscriber::registry().with(RunLogLayer::new(hub.clone()));
609 tracing::subscriber::with_default(subscriber, || {
610 tracing::info!("orphan event");
612 let span = tracing::info_span!("faucet.serve.run", serve_run_id = "run-xyz");
613 let _g = span.enter();
614 tracing::info!("hello from the run");
615 });
616 let (snapshot, _rx, _ended) = hub.reader("run-xyz").expect("buffer for the run exists");
617 assert!(
618 snapshot
619 .iter()
620 .any(|l| l.line.contains("hello from the run")),
621 "in-span event must be captured: {snapshot:?}"
622 );
623 assert!(
624 snapshot.iter().all(|l| !l.line.contains("orphan")),
625 "events outside the run span must not be captured"
626 );
627 assert!(hub.reader("nonexistent").is_none());
629 }
630}