1use std::collections::VecDeque;
16use std::fmt;
17use std::marker::PhantomData;
18use std::sync::{Arc, Mutex};
19
20use tracing::field::{Field, Visit};
21use tracing::span::Attributes;
22use tracing::{Event, Id, Subscriber};
23use tracing_subscriber::Layer;
24use tracing_subscriber::layer::{Context, SubscriberExt};
25use tracing_subscriber::registry::{LookupSpan, Registry};
26
27use crate::error::LeanDiagnosticCode;
28
29pub const DIAGNOSTIC_CAPTURE_DEFAULT_CAPACITY: usize = 256;
37
38#[derive(Clone, Debug, Eq, PartialEq)]
46pub struct CapturedEvent {
47 pub target: String,
50 pub level: &'static str,
53 pub name: &'static str,
58 pub span: Option<String>,
63 pub code: Option<LeanDiagnosticCode>,
66 pub message: String,
68 pub fields: Vec<(&'static str, String)>,
71}
72
73#[must_use = "Drop the DiagnosticCapture only when you are done collecting"]
82pub struct DiagnosticCapture {
83 inner: Arc<Mutex<CaptureBuffer>>,
84 _default_guard: tracing::subscriber::DefaultGuard,
87 _not_send_sync: PhantomData<*mut ()>,
93}
94
95impl DiagnosticCapture {
96 pub fn install() -> Self {
104 Self::with_capacity(DIAGNOSTIC_CAPTURE_DEFAULT_CAPACITY)
105 }
106
107 pub fn with_capacity(capacity: usize) -> Self {
109 let inner = Arc::new(Mutex::new(CaptureBuffer::new(capacity)));
110 let layer = CaptureLayer {
111 buffer: Arc::clone(&inner),
112 };
113 let subscriber = Registry::default().with(layer);
114 let default_guard = tracing::subscriber::set_default(subscriber);
115 Self {
116 inner,
117 _default_guard: default_guard,
118 _not_send_sync: PhantomData,
119 }
120 }
121
122 #[must_use]
126 pub fn events(&self) -> Vec<CapturedEvent> {
127 let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
128 inner.events.iter().cloned().collect()
129 }
130
131 #[must_use]
134 pub fn overflowed(&self) -> usize {
135 let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
136 inner.overflowed
137 }
138
139 #[must_use]
141 pub fn capacity(&self) -> usize {
142 let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
143 inner.capacity
144 }
145}
146
147impl fmt::Debug for DiagnosticCapture {
148 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149 let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
150 f.debug_struct("DiagnosticCapture")
151 .field("events", &inner.events.len())
152 .field("overflowed", &inner.overflowed)
153 .finish_non_exhaustive()
154 }
155}
156
157struct CaptureBuffer {
159 events: VecDeque<CapturedEvent>,
160 overflowed: usize,
161 capacity: usize,
162}
163
164impl CaptureBuffer {
165 fn new(capacity: usize) -> Self {
166 let capacity = capacity.max(1);
167 Self {
168 events: VecDeque::with_capacity(capacity),
169 overflowed: 0,
170 capacity,
171 }
172 }
173
174 fn push(&mut self, event: CapturedEvent) {
175 if self.events.len() >= self.capacity {
176 self.events.pop_front();
177 self.overflowed = self.overflowed.saturating_add(1);
178 }
179 self.events.push_back(event);
180 }
181}
182
183struct CaptureLayer {
185 buffer: Arc<Mutex<CaptureBuffer>>,
186}
187
188impl<S> Layer<S> for CaptureLayer
189where
190 S: Subscriber + for<'a> LookupSpan<'a>,
191{
192 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
193 let metadata = attrs.metadata();
197 if !metadata.target().starts_with("lean_rs") {
198 return;
199 }
200 let mut visitor = FieldVisitor::default();
201 attrs.record(&mut visitor);
202 let span_name = metadata.name();
203 let event = CapturedEvent {
204 target: metadata.target().to_owned(),
205 level: level_str(*metadata.level()),
206 name: "span_open",
207 span: Some(span_name.to_owned()),
208 code: visitor.code,
209 message: visitor.message.unwrap_or_default(),
210 fields: visitor.other_fields,
211 };
212 if let Ok(mut buf) = self.buffer.lock() {
215 buf.push(event);
216 }
217 let _ = (id, ctx);
221 }
222
223 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
224 let metadata = event.metadata();
225 if !metadata.target().starts_with("lean_rs") {
226 return;
227 }
228 let mut visitor = FieldVisitor::default();
229 event.record(&mut visitor);
230 let span_name = ctx
231 .event_span(event)
232 .map(|s| s.name().to_owned())
233 .or_else(|| ctx.lookup_current().map(|s| s.name().to_owned()));
234 let captured = CapturedEvent {
235 target: metadata.target().to_owned(),
236 level: level_str(*metadata.level()),
237 name: metadata.name(),
238 span: span_name,
239 code: visitor.code,
240 message: visitor.message.unwrap_or_default(),
241 fields: visitor.other_fields,
242 };
243 if let Ok(mut buf) = self.buffer.lock() {
244 buf.push(captured);
245 }
246 }
247}
248
249const fn level_str(level: tracing::Level) -> &'static str {
250 match level {
251 tracing::Level::ERROR => "error",
252 tracing::Level::WARN => "warn",
253 tracing::Level::INFO => "info",
254 tracing::Level::DEBUG => "debug",
255 tracing::Level::TRACE => "trace",
256 }
257}
258
259#[derive(Default)]
262struct FieldVisitor {
263 code: Option<LeanDiagnosticCode>,
264 message: Option<String>,
265 other_fields: Vec<(&'static str, String)>,
266}
267
268impl Visit for FieldVisitor {
269 fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
270 let rendered = format!("{value:?}");
271 self.record_str(field, &rendered);
272 }
273
274 fn record_str(&mut self, field: &Field, value: &str) {
275 match field.name() {
276 "code" => {
277 if let Some(known) = parse_code_str(value) {
278 self.code = Some(known);
279 } else {
280 self.other_fields.push(("code", value.to_owned()));
281 }
282 }
283 "message" => self.message = Some(value.to_owned()),
284 other => self.other_fields.push((other, value.to_owned())),
285 }
286 }
287}
288
289fn parse_code_str(raw: &str) -> Option<LeanDiagnosticCode> {
296 let trimmed = raw.trim_matches('"');
297 match trimmed {
298 "lean_rs.runtime_init" | "RuntimeInit" => Some(LeanDiagnosticCode::RuntimeInit),
299 "lean_rs.linking" | "Linking" => Some(LeanDiagnosticCode::Linking),
300 "lean_rs.module_init" | "ModuleInit" => Some(LeanDiagnosticCode::ModuleInit),
301 "lean_rs.symbol_lookup" | "SymbolLookup" => Some(LeanDiagnosticCode::SymbolLookup),
302 "lean_rs.abi_conversion" | "AbiConversion" => Some(LeanDiagnosticCode::AbiConversion),
303 "lean_rs.lean_exception" | "LeanException" => Some(LeanDiagnosticCode::LeanException),
304 "lean_rs.elaboration" | "Elaboration" => Some(LeanDiagnosticCode::Elaboration),
305 "lean_rs.unsupported" | "Unsupported" => Some(LeanDiagnosticCode::Unsupported),
306 "lean_rs.internal" | "Internal" => Some(LeanDiagnosticCode::Internal),
307 _ => None,
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use tracing::{info, info_span};
315
316 #[test]
317 fn captures_lean_rs_event() {
318 let capture = DiagnosticCapture::install();
319 info!(target: "lean_rs", code = "lean_rs.linking", "linker failed");
320 let events = capture.events();
321 assert!(
322 events
323 .iter()
324 .any(|e| e.code == Some(LeanDiagnosticCode::Linking) && e.message == "linker failed"),
325 "expected one linking event, got {events:?}",
326 );
327 }
328
329 #[test]
330 fn ignores_other_targets() {
331 let capture = DiagnosticCapture::install();
332 info!(target: "some_other_crate", "boring");
333 assert!(capture.events().is_empty());
334 }
335
336 #[test]
337 fn captures_span_open() {
338 let capture = DiagnosticCapture::install();
339 let _g = info_span!(target: "lean_rs", "lean_rs.host.session.import").entered();
340 let events = capture.events();
341 assert!(
342 events
343 .iter()
344 .any(|e| e.span.as_deref() == Some("lean_rs.host.session.import")),
345 "expected a span_open record, got {events:?}",
346 );
347 }
348
349 #[test]
350 fn bounded_buffer_drops_oldest() {
351 let capture = DiagnosticCapture::with_capacity(2);
352 info!(target: "lean_rs", "one");
353 info!(target: "lean_rs", "two");
354 info!(target: "lean_rs", "three");
355 let events = capture.events();
356 assert_eq!(events.len(), 2);
357 let messages: Vec<&str> = events.iter().map(|e| e.message.as_str()).collect();
358 assert_eq!(messages, ["two", "three"]);
359 assert_eq!(capture.overflowed(), 1);
360 }
361}