From ba18b445ecdba0adc1455288d5e366721f0ce57a Mon Sep 17 00:00:00 2001 From: rays Date: Sat, 7 Nov 2026 13:34:24 +0000 Subject: [PATCH] Log lines name their trace under a scoped subscriber too (#408) WithTrace found a line's span with Span::current(), which asks the thread's default dispatcher; inside an event under a scoped subscriber (with_default), tracing's re-entrancy guard answers with none, so every line went out without trace_id and span_id. Production, on the global dispatcher, was not affected, but the log test had to go global for it. The JSON layer now carries a small layer of its own that keeps the subscriber it is registered with (on_register_dispatch, a weak handle), and the formatter takes the registry's current span from its own context and asks tracing-opentelemetry for its context through that handle (get_otel_context), with no dispatcher lookup. The log test runs scoped again; with the old formatter it fails for want of the ids. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RqkrVDqdiQEMWAkNGQ2tYA --- crates/directory/src/otel.rs | 30 ++++++++++++++++++++++++++---- crates/ipx/src/logging.rs | 9 +++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/directory/src/otel.rs b/crates/directory/src/otel.rs index 3c36d31..6d9e821 100644 --- a/crates/directory/src/otel.rs +++ b/crates/directory/src/otel.rs @@ -104,6 +104,8 @@ where S: tracing::Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>, W: for<'w> tracing_subscriber::fmt::MakeWriter<'w> + Send + Sync + 'static, { + use tracing_subscriber::Layer as _; + let registered = Registered::default(); tracing_subscriber::fmt::layer() .fmt_fields(tracing_subscriber::fmt::format::JsonFields::new()) .event_format(WithTrace( @@ -112,11 +114,26 @@ where .flatten_event(true) .with_current_span(current_span) .with_span_list(false), + registered.clone(), )) .with_writer(writer) + .and_then(registered) } -pub struct WithTrace(pub F); +/// The subscriber the log layer is part of, as it was handed over on registration. The span's +/// OpenTelemetry context is reached through it: `Span::current()` asks the thread's default +/// dispatcher, which inside an event under a scoped subscriber (`with_default`) is hidden by +/// tracing's re-entrancy guard, and every line went out without its ids (#408). +#[derive(Clone, Default)] +struct Registered(std::sync::Arc>); + +impl tracing_subscriber::Layer for Registered { + fn on_register_dispatch(&self, subscriber: &tracing::Dispatch) { + let _ = self.0.set(subscriber.downgrade()); + } +} + +struct WithTrace(F, Registered); impl tracing_subscriber::fmt::FormatEvent for WithTrace where @@ -131,9 +148,14 @@ where ev: &tracing::Event<'_>, ) -> std::fmt::Result { use opentelemetry::trace::TraceContextExt; - use tracing_opentelemetry::OpenTelemetrySpanExt; - let current = tracing::Span::current(); - let sc = if current.is_none() { None } else { Some(current.context().span().span_context().clone()) }; + let dispatch = self.1.0.get().and_then(tracing::dispatcher::WeakDispatch::upgrade); + // The registry's own current span, not the one this layer's filter would show. + let sc = ctx + .current_span() + .id() + .zip(dispatch) + .and_then(|(id, d)| tracing_opentelemetry::get_otel_context(id, &d)) + .map(|cx| cx.span().span_context().clone()); let Some(sc) = sc.filter(|c| c.is_valid()) else { return self.0.format_event(ctx, w, ev); }; diff --git a/crates/ipx/src/logging.rs b/crates/ipx/src/logging.rs index 7dc57f0..75de95c 100644 --- a/crates/ipx/src/logging.rs +++ b/crates/ipx/src/logging.rs @@ -69,11 +69,8 @@ mod tests { let sub = tracing_subscriber::registry() .with(json_layer(move || Lines(b.clone()), true)) .with(tracing_opentelemetry::layer().with_tracer(provider.tracer("test"))); - // Global, as production's is: under a scoped one (with_default), Span::current() inside the - // formatter finds no dispatcher, so no line would name its trace. Other tests' events - // land here too, hence feed names only this test uses. - tracing::subscriber::set_global_default(sub).unwrap(); - { + // Scoped, where the formatter once found no dispatcher and no line named its trace (#408). + tracing::subscriber::with_default(sub, || { let out = Emitter::terminal(); out.emit(Event::FeedDone { feed: "log-test-f".into(), new: 2, downloaded: 1, failed: 0, torrents: 0 }); let scan = tracing::info_span!("scan"); @@ -86,7 +83,7 @@ mod tests { bytes: 1234, }); out.emit(Event::FeedError { feed: "log-test-g".into(), msg: "HTTP 404 Not Found".into() }); - } + }); let text = String::from_utf8(buf.lock().unwrap().clone()).unwrap(); let lines: Vec = text.lines().map(|l| serde_json::from_str(l).expect(l)).collect(); let find = |ev: &str| {