use std::sync::{Arc, Mutex};
use oxen_server::tasks::{inherit_hub, spawn_blocking};
use tracing::Instrument;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::registry::LookupSpan;
type SpanParent = (String, Option<String>);
#[derive(Clone, Default)]
struct SpanParents(Arc<Mutex<Vec<SpanParent>>>);
impl SpanParents {
fn install() -> Self {
let recorder = Self::default();
tracing::subscriber::set_global_default(
tracing_subscriber::registry().with(recorder.clone()),
)
.expect("this binary installs the only subscriber");
recorder
}
fn recorded(&self) -> Vec<SpanParent> {
self.0
.lock()
.expect("the recorder should not be poisoned")
.clone()
}
fn parent_of(&self, name: &str) -> Option<Option<String>> {
self.recorded()
.into_iter()
.find(|(span, _)| span == name)
.map(|(_, parent)| parent)
}
}
impl<S> tracing_subscriber::Layer<S> for SpanParents
where
S: tracing::Subscriber + for<'a> LookupSpan<'a>,
{
fn on_new_span(
&self,
_attrs: &tracing::span::Attributes<'_>,
id: &tracing::Id,
ctx: tracing_subscriber::layer::Context<'_, S>,
) {
let Some(span) = ctx.span(id) else {
return;
};
let parent = span.parent().map(|parent| parent.name().to_string());
if let Ok(mut recorded) = self.0.lock() {
recorded.push((span.name().to_string(), parent));
}
}
}
#[actix_web::test]
async fn work_off_the_request_thread_stays_in_the_request_span() {
let recorder = SpanParents::install();
let request = tracing::info_span!("request under test");
async {
spawn_blocking(|| tracing::info_span!("unpacking under test").in_scope(|| ()))
.await
.expect("the blocking task should not have panicked");
}
.instrument(request.clone())
.await;
assert_eq!(
recorder.parent_of("blocking task"),
Some(Some("request under test".to_string())),
"recorded: {:?}",
recorder.recorded()
);
assert_eq!(
recorder.parent_of("unpacking under test"),
Some(Some("blocking task".to_string())),
"a span opened on the pool thread should hang under the blocking task; recorded: {:?}",
recorder.recorded()
);
let deferred = {
let _entered = request.enter();
inherit_hub(async { tracing::info_span!("deferred work under test").in_scope(|| ()) })
};
deferred.await;
assert_eq!(
recorder.parent_of("deferred work under test"),
Some(Some("request under test".to_string())),
"recorded: {:?}",
recorder.recorded()
);
let bare = {
let _entered = request.enter();
async { tracing::info_span!("bare deferred work under test").in_scope(|| ()) }
};
bare.await;
assert_eq!(
recorder.parent_of("bare deferred work under test"),
Some(None),
"recorded: {:?}",
recorder.recorded()
);
}