atproto blogging
1//! Ring buffer log capture for bug reports.
2//!
3//! Provides a tracing Layer that captures recent log messages to a fixed-size
4//! ring buffer. Logs can be retrieved on-demand for bug reports.
5
6use std::cell::RefCell;
7use std::collections::VecDeque;
8use std::fmt::Write as FmtWrite;
9
10use tracing::field::{Field, Visit};
11use tracing::{Event, Level, Subscriber};
12use tracing_subscriber::Layer;
13use tracing_subscriber::layer::Context;
14
15/// Maximum number of log entries to keep.
16const MAX_ENTRIES: usize = 100;
17
18/// Module prefixes to capture in the ring buffer.
19const CAPTURED_PREFIXES: &[&str] = &["weaver_", "markdown_weaver"];
20
21thread_local! {
22 static LOG_BUFFER: RefCell<VecDeque<String>> = RefCell::new(VecDeque::with_capacity(MAX_ENTRIES));
23}
24
25/// Minimum level to buffer from our modules.
26const BUFFER_MIN_LEVEL: Level = Level::DEBUG;
27
28/// A tracing Layer that captures log messages to a ring buffer.
29/// Console output is handled by WASMLayer in the subscriber stack.
30pub struct LogCaptureLayer;
31
32impl<S: Subscriber> Layer<S> for LogCaptureLayer {
33 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
34 let metadata = event.metadata();
35 let level = metadata.level();
36 let target = metadata.target();
37
38 // Only buffer debug+ logs from our modules
39 let is_our_module = CAPTURED_PREFIXES
40 .iter()
41 .any(|prefix| target.starts_with(prefix));
42 if !is_our_module || *level > BUFFER_MIN_LEVEL {
43 return;
44 }
45
46 // Format the log entry
47 let mut message = String::new();
48 let mut visitor = MessageVisitor(&mut message);
49 event.record(&mut visitor);
50
51 let formatted = format!("[{}] {}: {}", level_str(level), target, message);
52
53 LOG_BUFFER.with(|buf| {
54 let mut buf = buf.borrow_mut();
55 if buf.len() >= MAX_ENTRIES {
56 buf.pop_front();
57 }
58 buf.push_back(formatted);
59 });
60 }
61}
62
63/// Visitor that extracts the message field from a tracing event.
64struct MessageVisitor<'a>(&'a mut String);
65
66impl Visit for MessageVisitor<'_> {
67 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
68 if field.name() == "message" {
69 let _ = write!(self.0, "{:?}", value);
70 } else {
71 if !self.0.is_empty() {
72 self.0.push_str(", ");
73 }
74 let _ = write!(self.0, "{}={:?}", field.name(), value);
75 }
76 }
77
78 fn record_str(&mut self, field: &Field, value: &str) {
79 if field.name() == "message" {
80 self.0.push_str(value);
81 } else {
82 if !self.0.is_empty() {
83 self.0.push_str(", ");
84 }
85 let _ = write!(self.0, "{}={}", field.name(), value);
86 }
87 }
88}
89
90fn level_str(level: &Level) -> &'static str {
91 match *level {
92 Level::ERROR => "ERROR",
93 Level::WARN => "WARN",
94 Level::INFO => "INFO",
95 Level::DEBUG => "DEBUG",
96 Level::TRACE => "TRACE",
97 }
98}
99
100/// Get all captured log entries as a single string.
101#[allow(dead_code)]
102pub fn get_logs() -> String {
103 LOG_BUFFER.with(|buf| {
104 let buf = buf.borrow();
105 buf.iter().cloned().collect::<Vec<_>>().join("\n")
106 })
107}
108
109/// Clear the log buffer.
110#[allow(dead_code)]
111pub fn clear_logs() {
112 LOG_BUFFER.with(|buf| buf.borrow_mut().clear());
113}