use std::collections::VecDeque;
use std::fmt;
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing::span::Attributes;
use tracing::{Event, Id, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::{Context, SubscriberExt};
use tracing_subscriber::registry::{LookupSpan, Registry};
use crate::error::LeanDiagnosticCode;
pub const DIAGNOSTIC_CAPTURE_DEFAULT_CAPACITY: usize = 256;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CapturedEvent {
pub target: String,
pub level: &'static str,
pub name: &'static str,
pub span: Option<String>,
pub code: Option<LeanDiagnosticCode>,
pub message: String,
pub fields: Vec<(&'static str, String)>,
}
#[must_use = "Drop the DiagnosticCapture only when you are done collecting"]
pub struct DiagnosticCapture {
inner: Arc<Mutex<CaptureBuffer>>,
_default_guard: tracing::subscriber::DefaultGuard,
_not_send_sync: PhantomData<*mut ()>,
}
impl DiagnosticCapture {
pub fn install() -> Self {
Self::with_capacity(DIAGNOSTIC_CAPTURE_DEFAULT_CAPACITY)
}
pub fn with_capacity(capacity: usize) -> Self {
let inner = Arc::new(Mutex::new(CaptureBuffer::new(capacity)));
let layer = CaptureLayer {
buffer: Arc::clone(&inner),
};
let subscriber = Registry::default().with(layer);
let default_guard = tracing::subscriber::set_default(subscriber);
Self {
inner,
_default_guard: default_guard,
_not_send_sync: PhantomData,
}
}
#[must_use]
pub fn events(&self) -> Vec<CapturedEvent> {
let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
inner.events.iter().cloned().collect()
}
#[must_use]
pub fn overflowed(&self) -> usize {
let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
inner.overflowed
}
#[must_use]
pub fn capacity(&self) -> usize {
let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
inner.capacity
}
}
impl fmt::Debug for DiagnosticCapture {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let inner = self.inner.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
f.debug_struct("DiagnosticCapture")
.field("events", &inner.events.len())
.field("overflowed", &inner.overflowed)
.finish_non_exhaustive()
}
}
struct CaptureBuffer {
events: VecDeque<CapturedEvent>,
overflowed: usize,
capacity: usize,
}
impl CaptureBuffer {
fn new(capacity: usize) -> Self {
let capacity = capacity.max(1);
Self {
events: VecDeque::with_capacity(capacity),
overflowed: 0,
capacity,
}
}
fn push(&mut self, event: CapturedEvent) {
if self.events.len() >= self.capacity {
self.events.pop_front();
self.overflowed = self.overflowed.saturating_add(1);
}
self.events.push_back(event);
}
}
struct CaptureLayer {
buffer: Arc<Mutex<CaptureBuffer>>,
}
impl<S> Layer<S> for CaptureLayer
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
let metadata = attrs.metadata();
if !metadata.target().starts_with("lean_rs") {
return;
}
let mut visitor = FieldVisitor::default();
attrs.record(&mut visitor);
let span_name = metadata.name();
let event = CapturedEvent {
target: metadata.target().to_owned(),
level: level_str(*metadata.level()),
name: "span_open",
span: Some(span_name.to_owned()),
code: visitor.code,
message: visitor.message.unwrap_or_default(),
fields: visitor.other_fields,
};
if let Ok(mut buf) = self.buffer.lock() {
buf.push(event);
}
let _ = (id, ctx);
}
fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
let metadata = event.metadata();
if !metadata.target().starts_with("lean_rs") {
return;
}
let mut visitor = FieldVisitor::default();
event.record(&mut visitor);
let span_name = ctx
.event_span(event)
.map(|s| s.name().to_owned())
.or_else(|| ctx.lookup_current().map(|s| s.name().to_owned()));
let captured = CapturedEvent {
target: metadata.target().to_owned(),
level: level_str(*metadata.level()),
name: metadata.name(),
span: span_name,
code: visitor.code,
message: visitor.message.unwrap_or_default(),
fields: visitor.other_fields,
};
if let Ok(mut buf) = self.buffer.lock() {
buf.push(captured);
}
}
}
const fn level_str(level: tracing::Level) -> &'static str {
match level {
tracing::Level::ERROR => "error",
tracing::Level::WARN => "warn",
tracing::Level::INFO => "info",
tracing::Level::DEBUG => "debug",
tracing::Level::TRACE => "trace",
}
}
#[derive(Default)]
struct FieldVisitor {
code: Option<LeanDiagnosticCode>,
message: Option<String>,
other_fields: Vec<(&'static str, String)>,
}
impl Visit for FieldVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
let rendered = format!("{value:?}");
self.record_str(field, &rendered);
}
fn record_str(&mut self, field: &Field, value: &str) {
match field.name() {
"code" => {
if let Some(known) = parse_code_str(value) {
self.code = Some(known);
} else {
self.other_fields.push(("code", value.to_owned()));
}
}
"message" => self.message = Some(value.to_owned()),
other => self.other_fields.push((other, value.to_owned())),
}
}
}
fn parse_code_str(raw: &str) -> Option<LeanDiagnosticCode> {
let trimmed = raw.trim_matches('"');
match trimmed {
"lean_rs.runtime_init" | "RuntimeInit" => Some(LeanDiagnosticCode::RuntimeInit),
"lean_rs.linking" | "Linking" => Some(LeanDiagnosticCode::Linking),
"lean_rs.module_init" | "ModuleInit" => Some(LeanDiagnosticCode::ModuleInit),
"lean_rs.symbol_lookup" | "SymbolLookup" => Some(LeanDiagnosticCode::SymbolLookup),
"lean_rs.abi_conversion" | "AbiConversion" => Some(LeanDiagnosticCode::AbiConversion),
"lean_rs.lean_exception" | "LeanException" => Some(LeanDiagnosticCode::LeanException),
"lean_rs.elaboration" | "Elaboration" => Some(LeanDiagnosticCode::Elaboration),
"lean_rs.unsupported" | "Unsupported" => Some(LeanDiagnosticCode::Unsupported),
"lean_rs.internal" | "Internal" => Some(LeanDiagnosticCode::Internal),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use tracing::{info, info_span};
#[test]
fn captures_lean_rs_event() {
let capture = DiagnosticCapture::install();
info!(target: "lean_rs", code = "lean_rs.linking", "linker failed");
let events = capture.events();
assert!(
events
.iter()
.any(|e| e.code == Some(LeanDiagnosticCode::Linking) && e.message == "linker failed"),
"expected one linking event, got {events:?}",
);
}
#[test]
fn ignores_other_targets() {
let capture = DiagnosticCapture::install();
info!(target: "some_other_crate", "boring");
assert!(capture.events().is_empty());
}
#[test]
fn captures_span_open() {
let capture = DiagnosticCapture::install();
let _g = info_span!(target: "lean_rs", "lean_rs.host.session.import").entered();
let events = capture.events();
assert!(
events
.iter()
.any(|e| e.span.as_deref() == Some("lean_rs.host.session.import")),
"expected a span_open record, got {events:?}",
);
}
#[test]
fn bounded_buffer_drops_oldest() {
let capture = DiagnosticCapture::with_capacity(2);
info!(target: "lean_rs", "one");
info!(target: "lean_rs", "two");
info!(target: "lean_rs", "three");
let events = capture.events();
assert_eq!(events.len(), 2);
let messages: Vec<&str> = events.iter().map(|e| e.message.as_str()).collect();
assert_eq!(messages, ["two", "three"]);
assert_eq!(capture.overflowed(), 1);
}
}