use std::fmt;
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing::span::Record;
use tracing::{Event, Id, Level, Subscriber};
use tracing_subscriber::Layer;
use tracing_subscriber::layer::{Context, SubscriberExt};
type Sink = Arc<Mutex<Vec<String>>>;
struct FieldVisitor(String);
impl Visit for FieldVisitor {
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
if !self.0.is_empty() {
self.0.push(' ');
}
let _ = fmt::write(&mut self.0, format_args!("{}={:?}", field.name(), value));
}
}
struct EventCaptureLayer {
events: Sink,
}
impl<S> Layer<S> for EventCaptureLayer
where
S: Subscriber,
{
fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
if *event.metadata().level() <= Level::DEBUG {
let mut visitor = FieldVisitor(String::new());
event.record(&mut visitor);
if let Ok(mut slot) = self.events.lock() {
slot.push(visitor.0);
}
}
}
}
struct SpanRecordLayer {
records: Sink,
}
impl<S> Layer<S> for SpanRecordLayer
where
S: Subscriber,
{
fn on_record(&self, _id: &Id, values: &Record<'_>, _ctx: Context<'_, S>) {
let mut visitor = FieldVisitor(String::new());
values.record(&mut visitor);
if let Ok(mut slot) = self.records.lock() {
slot.push(visitor.0);
}
}
}
pub(crate) fn ensure_global_registry() {
static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
if INIT.set(()).is_ok() {
let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
}
}
pub(crate) fn capture_debugs_with_span_records<T>(
f: impl FnOnce() -> T,
) -> (T, Vec<String>, Vec<String>) {
ensure_global_registry();
let events: Sink = Default::default();
let span_records: Sink = Default::default();
let subscriber = tracing_subscriber::registry()
.with(EventCaptureLayer {
events: Arc::clone(&events),
})
.with(SpanRecordLayer {
records: Arc::clone(&span_records),
});
let out = tracing::subscriber::with_default(subscriber, f);
let collected = events
.lock()
.ok()
.map(|slot| slot.clone())
.unwrap_or_default();
let spans = span_records
.lock()
.ok()
.map(|slot| slot.clone())
.unwrap_or_default();
(out, collected, spans)
}
pub(crate) fn captured_field<'a>(record: &'a str, name: &str) -> Option<&'a str> {
let start = record
.match_indices(&format!("{name}="))
.find(|(i, _)| *i == 0 || record[..*i].ends_with(' '))?
.0
+ name.len()
+ 1;
let rest = &record[start..];
let end = rest
.match_indices(' ')
.find_map(|(i, _)| {
let after = &rest[i + 1..];
let eq = after.find('=')?;
let key = &after[..eq];
(!key.is_empty() && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '_'))
.then_some(i)
})
.unwrap_or(rest.len());
Some(rest[..end].trim_end())
}
pub(crate) fn record_field<'a>(
captured: &'a [String],
message_part: &str,
field: &str,
) -> Option<&'a str> {
let record = captured.iter().find(|line| line.contains(message_part))?;
captured_field(record, field)
}