yggdrasil/api/logs/
capture.rs1use std::sync::atomic::{AtomicU64, Ordering};
20use std::sync::{LazyLock, Mutex};
21
22use chrono::{DateTime, Utc};
23use tokio::sync::{broadcast, mpsc};
24use tracing::field::{Field, Visit};
25use tracing::Event;
26use tracing::Subscriber;
27use tracing_subscriber::layer::{Context, Layer};
28use tracing_subscriber::EnvFilter;
29
30const MPSC_CAP: usize = 4096;
33
34const BROADCAST_CAP: usize = 1024;
36
37const MAX_MESSAGE_BYTES: usize = 4 * 1024;
39
40const EXCLUDED_TARGETS: [&str; 2] = ["yggdrasil::api::logs", "yggdrasil::tasks::log_writer"];
42
43#[derive(Debug, Clone)]
45pub struct LogRecord {
46 pub ts: DateTime<Utc>,
48 pub level: String,
50 pub target: String,
52 pub message: String,
54}
55
56struct LogChannels {
58 db_tx: mpsc::Sender<LogRecord>,
60 db_rx: Mutex<Option<mpsc::Receiver<LogRecord>>>,
62 live_tx: broadcast::Sender<LogRecord>,
64 dropped: AtomicU64,
66}
67
68static CHANNELS: LazyLock<LogChannels> = LazyLock::new(|| {
70 let (db_tx, db_rx) = mpsc::channel(MPSC_CAP);
71 let (live_tx, _) = broadcast::channel(BROADCAST_CAP);
72 LogChannels {
73 db_tx,
74 db_rx: Mutex::new(Some(db_rx)),
75 live_tx,
76 dropped: AtomicU64::new(0),
77 }
78});
79
80pub fn take_db_receiver() -> Option<mpsc::Receiver<LogRecord>> {
82 CHANNELS.db_rx.lock().ok().and_then(|mut g| g.take())
83}
84
85pub fn subscribe_live() -> broadcast::Receiver<LogRecord> {
87 CHANNELS.live_tx.subscribe()
88}
89
90pub fn dropped_count() -> u64 {
92 CHANNELS.dropped.load(Ordering::Relaxed)
93}
94
95pub fn record_dropped(n: u64) {
97 CHANNELS.dropped.fetch_add(n, Ordering::Relaxed);
98}
99
100pub fn log_viewer_filter() -> EnvFilter {
103 std::env::var("LOG_VIEWER_LEVEL")
104 .ok()
105 .and_then(|v| EnvFilter::try_new(v).ok())
106 .unwrap_or_else(|| EnvFilter::new("info"))
107}
108
109pub struct CaptureLayer;
115
116impl<S: Subscriber> Layer<S> for CaptureLayer {
117 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
118 let meta = event.metadata();
119 let target = meta.target();
120
121 if EXCLUDED_TARGETS.iter().any(|p| target.starts_with(p)) {
123 return;
124 }
125
126 let mut visitor = MessageVisitor::default();
128 event.record(&mut visitor);
129 let mut message = visitor.message.unwrap_or_default();
130 for (key, value) in visitor.extra {
131 message.push(' ');
132 message.push_str(&key);
133 message.push('=');
134 message.push_str(&value);
135 }
136 truncate_message(&mut message);
137
138 let record = LogRecord {
139 ts: Utc::now(),
140 level: meta.level().as_str().to_string(),
141 target: target.to_string(),
142 message,
143 };
144
145 let has_live_receivers = CHANNELS.live_tx.receiver_count() > 0;
146 if has_live_receivers {
147 if CHANNELS.db_tx.try_send(record.clone()).is_err() {
150 CHANNELS.dropped.fetch_add(1, Ordering::Relaxed);
151 }
152 let _ = CHANNELS.live_tx.send(record);
153 } else if CHANNELS.db_tx.try_send(record).is_err() {
154 CHANNELS.dropped.fetch_add(1, Ordering::Relaxed);
155 }
156 }
157}
158
159#[derive(Default)]
161struct MessageVisitor {
162 message: Option<String>,
163 extra: Vec<(String, String)>,
164}
165
166impl Visit for MessageVisitor {
167 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
170 if field.name() == "message" {
171 self.message = Some(format!("{value:?}"));
172 } else {
173 self.extra
174 .push((field.name().to_string(), format!("{value:?}")));
175 }
176 }
177
178 fn record_str(&mut self, field: &Field, value: &str) {
180 if field.name() == "message" {
181 self.message = Some(value.to_string());
182 } else {
183 self.extra
184 .push((field.name().to_string(), value.to_string()));
185 }
186 }
187}
188
189fn truncate_message(s: &mut String) {
191 if s.len() > MAX_MESSAGE_BYTES {
192 let mut end = MAX_MESSAGE_BYTES;
193 while !s.is_char_boundary(end) {
194 end -= 1;
195 }
196 s.truncate(end);
197 }
198}
199
200#[cfg(all(test, feature = "server"))]
201mod tests {
202 use super::*;
203
204 #[test]
205 fn truncate_respects_char_boundary() {
206 let mut s = "a".repeat(100);
208 truncate_message(&mut s);
209 assert_eq!(s.len(), 100);
210
211 let mut s = "日".repeat(MAX_MESSAGE_BYTES); truncate_message(&mut s);
214 assert!(s.len() <= MAX_MESSAGE_BYTES);
215 assert!(s.is_char_boundary(s.len()));
216 }
217
218 #[test]
219 fn dropped_counter_accumulates() {
220 let before = dropped_count();
221 record_dropped(7);
222 assert_eq!(dropped_count(), before + 7);
223 }
224
225 #[test]
226 fn db_receiver_take_once() {
227 let first = take_db_receiver();
230 let second = take_db_receiver();
231 assert!(second.is_none());
232 drop(first);
233 }
234}