use std::fmt;
use std::sync::{Arc, Mutex};
use tracing::field::{Field, Visit};
use tracing::{Level, Subscriber};
use tracing_subscriber::layer::{Context, Layer};
type Event = (Level, Vec<String>);
#[derive(Clone, Default)]
pub(crate) struct Levels(Arc<Mutex<Vec<Event>>>);
impl Levels {
pub(crate) fn operator_visible(&self) -> Vec<Level> {
self.lines()
.iter()
.filter(|(level, _)| *level <= Level::INFO)
.map(|(level, _)| *level)
.collect()
}
pub(crate) fn said(&self, level: Level, needle: &str) -> bool {
self.lines().iter().any(|(seen, fields)| {
*seen == level && fields.iter().any(|field| field.as_str() == needle)
})
}
pub(crate) fn mentioned(&self, level: Level, needle: &str) -> bool {
self.lines().iter().any(|(seen, fields)| {
*seen == level && fields.iter().any(|field| field.contains(needle))
})
}
fn lines(&self) -> Vec<Event> {
self.0
.lock()
.expect("no test panics while holding the recorded lines")
.clone()
}
}
impl fmt::Debug for Levels {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("Levels").field(&self.lines()).finish()
}
}
impl<S: Subscriber> Layer<S> for Levels {
fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) {
let mut fields = Fields(Vec::new());
event.record(&mut fields);
self.0
.lock()
.expect("no test panics while holding the recorded lines")
.push((*event.metadata().level(), fields.0));
}
}
#[cfg(test)]
pub(crate) struct Recording {
_default: tracing::subscriber::DefaultGuard,
_lock: std::sync::MutexGuard<'static, ()>,
}
#[cfg(test)]
pub(crate) fn recording() -> (Levels, Recording) {
use std::sync::{Mutex, Once, PoisonError};
use tracing_subscriber::layer::SubscriberExt;
static LOCK: Mutex<()> = Mutex::new(());
static GLOBAL: Once = Once::new();
let lock = LOCK.lock().unwrap_or_else(PoisonError::into_inner);
GLOBAL.call_once(|| {
let _ = tracing::subscriber::set_global_default(tracing_subscriber::registry());
tracing::callsite::rebuild_interest_cache();
});
let levels = Levels::default();
let recorder = tracing_subscriber::registry().with(levels.clone());
let default = tracing::subscriber::set_default(recorder);
(
levels,
Recording {
_default: default,
_lock: lock,
},
)
}
struct Fields(Vec<String>);
impl Visit for Fields {
fn record_str(&mut self, field: &Field, value: &str) {
self.0.push(format!("{}={value}", field.name()));
}
fn record_debug(&mut self, field: &Field, value: &dyn fmt::Debug) {
self.0.push(format!("{}={value:?}", field.name()));
}
}
#[cfg(test)]
mod tests {
use tracing::Level;
use super::recording;
#[test]
fn a_needle_matches_its_field_exactly_and_never_a_longer_value() {
let (levels, _recording) = recording();
tracing::info!(run_id = "r10", "a run outlived its call");
assert!(levels.said(Level::INFO, "run_id=r10"));
assert!(!levels.said(Level::INFO, "run_id=r1"));
assert!(levels.said(Level::INFO, "message=a run outlived its call"));
assert!(!levels.said(Level::INFO, "outlived its call"));
assert!(!levels.said(Level::WARN, "run_id=r10"));
}
}