//! Verifies background-job root spans (#142 / map #135). //! //! Background work has no request parent, so each job opens its own root //! span and must carry it into whatever concurrency it fans out to. Two //! production shapes are pinned here: //! //! 1. **Watcher tick → per-pass runtime.** `watcher::watch_files` runs on a //! plain std thread and enters a `watcher.tick` span; `face_watch` / //! `clip_watch` then build a multi-thread runtime, `block_on` it, and //! `tokio::spawn` one task per candidate. Each task must carry the //! tick's span via `.instrument(parent.clone())`. //! 2. **Rayon fan-out.** `thumbnails::create_thumbnails` walks files on //! rayon workers, where the ambient span isn't current, so child spans //! name their parent explicitly (`info_span!(parent: &span, ...)`). //! //! One process-global subscriber per test binary, so this file holds a //! single test. use opentelemetry::trace::{SpanId, TracerProvider}; use opentelemetry_sdk::trace::{InMemorySpanExporter, SdkTracerProvider, SpanData}; use rayon::prelude::*; use tracing::Instrument; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::registry::Registry; use tracing_subscriber::util::SubscriberInitExt; fn find<'a>(spans: &'a [SpanData], name: &str) -> &'a SpanData { spans .iter() .find(|s| s.name == name) .unwrap_or_else(|| panic!("span {name} not exported")) } #[tracing::instrument(name = "test.detect_one", skip_all)] async fn detect_one() { tokio::task::yield_now().await; } #[test] fn background_work_nests_under_job_root() { let exporter = InMemorySpanExporter::default(); let provider = SdkTracerProvider::builder() .with_simple_exporter(exporter.clone()) .build(); Registry::default() .with(tracing_opentelemetry::OpenTelemetryLayer::new( provider.tracer("background-test"), )) .try_init() .expect("global subscriber not yet set"); // Shape 1: std-thread tick with an entered root, per-pass runtime, spawns. std::thread::spawn(|| { let _tick = tracing::info_span!("test.tick").entered(); let rt = tokio::runtime::Builder::new_multi_thread() .worker_threads(2) .enable_all() .build() .unwrap(); let parent = tracing::Span::current(); rt.block_on(async move { let handles: Vec<_> = (0..3) .map(|_| tokio::spawn(detect_one().instrument(parent.clone()))) .collect(); for h in handles { h.await.unwrap(); } }); }) .join() .unwrap(); // Shape 2: rayon fan-out with an explicit parent. { let root = tracing::info_span!("test.thumbnails"); (0..4).into_par_iter().for_each(|_| { let _child = tracing::info_span!(parent: &root, "test.thumbnail_one"); }); } let _ = provider.force_flush(); let spans = exporter.get_finished_spans().unwrap(); let tick = find(&spans, "test.tick"); assert_eq!(tick.parent_span_id, SpanId::INVALID, "tick is a root span"); let detects: Vec<_> = spans .iter() .filter(|s| s.name == "test.detect_one") .collect(); assert_eq!(detects.len(), 3); for d in detects { assert_eq!(d.span_context.trace_id(), tick.span_context.trace_id()); assert_eq!(d.parent_span_id, tick.span_context.span_id()); } let root = find(&spans, "test.thumbnails"); let children: Vec<_> = spans .iter() .filter(|s| s.name == "test.thumbnail_one") .collect(); assert_eq!(children.len(), 4); for c in children { assert_eq!(c.span_context.trace_id(), root.span_context.trace_id()); assert_eq!(c.parent_span_id, root.span_context.span_id()); } }