//! Test-only tracing capture: count events whose `message` starts with a //! known prefix. //! //! Producers should export the exact log-line prefixes as `pub const`s next //! to the `tracing::debug!` call sites (e.g. `kigi_hunk_tracker`'s //! `REFRESH_SCAN_LOG_PREFIX`) so tests never duplicate the strings. use std::sync::Arc; use std::sync::atomic::{AtomicUsize, Ordering}; /// Extracts the formatted `message` field of one event. #[derive(Default)] struct MessageVisitor(String); impl tracing::field::Visit for MessageVisitor { fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { if field.name() == "message" { self.0 = format!("{value:?}"); } } } /// A `tracing_subscriber::Layer` counting, per registered prefix, the events /// whose `message` starts with it. Clones share the counts. #[derive(Clone)] pub struct MessagePrefixCounter { counters: Arc>, } impl MessagePrefixCounter { pub fn new(prefixes: &[&'static str]) -> Self { Self { counters: Arc::new(prefixes.iter().map(|p| (*p, AtomicUsize::new(0))).collect()), } } /// Events counted so far for `prefix`. Panics on a prefix that was never /// registered — that is a bug in the test, not a zero count. pub fn count(&self, prefix: &str) -> usize { self.counters .iter() .find(|(p, _)| *p == prefix) .unwrap_or_else(|| panic!("prefix not registered with this counter: {prefix:?}")) .1 .load(Ordering::Relaxed) } } impl tracing_subscriber::Layer for MessagePrefixCounter { fn on_event( &self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>, ) { let mut visitor = MessageVisitor::default(); event.record(&mut visitor); for (prefix, count) in self.counters.iter() { if visitor.0.starts_with(prefix) { count.fetch_add(1, Ordering::Relaxed); } } } } /// Install a **thread-scoped** default subscriber counting `prefixes`; hold /// the guard for the test's lifetime. Only observes events emitted on the /// current thread — tasks under test must run on a current-thread runtime. pub fn install_prefix_counter_thread( prefixes: &[&'static str], ) -> (tracing::subscriber::DefaultGuard, MessagePrefixCounter) { use tracing_subscriber::layer::SubscriberExt as _; let counter = MessagePrefixCounter::new(prefixes); let subscriber = tracing_subscriber::registry().with(counter.clone()); (tracing::subscriber::set_default(subscriber), counter) } /// Install the **process-global** subscriber counting `prefixes` — for tests /// whose subject spawns its own threads/runtimes. Panics if a global /// subscriber already exists: the test binary must own it. /// /// `stderr_env_filter` additionally tees formatted logs matching the given /// `EnvFilter` directive to stderr (local debugging). pub fn install_prefix_counter_global( prefixes: &[&'static str], stderr_env_filter: Option<&str>, ) -> MessagePrefixCounter { use tracing_subscriber::layer::{Layer as _, SubscriberExt as _}; use tracing_subscriber::util::SubscriberInitExt as _; let counter = MessagePrefixCounter::new(prefixes); let fmt = stderr_env_filter.map(|filter| { tracing_subscriber::fmt::layer() .with_writer(std::io::stderr) .with_filter(tracing_subscriber::EnvFilter::new(filter)) }); tracing_subscriber::registry() .with(counter.clone()) .with(fmt) .try_init() .expect("this test binary must own the global subscriber"); counter }