Production's log lines name their trace again (#411, #408)

ba18b44 had the JSON layer find a line's span context through the
dispatch it was registered with, and production's logging wrapped that
layer in an Option, whose Layer impl does not pass on_register_dispatch
on: the layer never got its dispatch and every line went out without
trace_id and span_id. The log test built a subscriber of its own,
without the Option, and passed.

Both services now box their stderr layer instead of an Option of each
kind (a Box passes registration on), the formatter falls back to the
default dispatcher when it was never registered, which a global
subscriber's lines find, and the log test builds the subscriber with
the function init uses, writing to its buffer, so a wrapper like this
one is under test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqkrVDqdiQEMWAkNGQ2tYA
This commit is contained in:
2026-11-07 13:41:04 +00:00
parent ba18b445ec
commit f5a3847a21
3 changed files with 46 additions and 28 deletions

View File

@@ -83,11 +83,16 @@ async fn serve(path: &Path) -> Result<()> {
let filter = || EnvFilter::try_from_env("DIRECTORY_LOG").unwrap_or_else(|_| "ipx_directory=info".into());
// Traces to Tempo when OTEL_EXPORTER_OTLP_ENDPOINT is set, joining ipx's (#213).
let otel = ipx_directory::otel::provider("ipx-directory")?;
// Each line's own fields say all its span did, and the span said it again (#363); the trace
// and span ids lead to the trace instead, as ipx's do (#375). Boxed rather than an Option of
// each: Option's Layer does not pass on_register_dispatch on, which the JSON layer needs (#408).
let stderr: Box<dyn tracing_subscriber::Layer<tracing_subscriber::Registry> + Send + Sync> = if json {
ipx_directory::otel::json_layer(std::io::stderr, false).with_filter(filter()).boxed()
} else {
tracing_subscriber::fmt::layer().with_writer(std::io::stderr).with_filter(filter()).boxed()
};
tracing_subscriber::registry()
.with((!json).then(|| tracing_subscriber::fmt::layer().with_writer(std::io::stderr).with_filter(filter())))
// Each line's own fields say all its span did, and the span said it again (#363); the trace
// and span ids lead to the trace instead, as ipx's do (#375).
.with(json.then(|| ipx_directory::otel::json_layer(std::io::stderr, false).with_filter(filter())))
.with(stderr)
.with(otel.as_ref().map(|p| {
use opentelemetry::trace::TracerProvider;
tracing_opentelemetry::layer()

View File

@@ -148,13 +148,15 @@ where
ev: &tracing::Event<'_>,
) -> std::fmt::Result {
use opentelemetry::trace::TraceContextExt;
let dispatch = self.1.0.get().and_then(tracing::dispatcher::WeakDispatch::upgrade);
// Without one (a wrapper that does not pass registration on, as Option does not), the
// default dispatcher, which a global subscriber's lines still find.
let registered = self.1.0.get().and_then(tracing::dispatcher::WeakDispatch::upgrade);
let dispatch = registered.unwrap_or_else(|| tracing::dispatcher::get_default(Clone::clone));
// 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))
.and_then(|id| tracing_opentelemetry::get_otel_context(id, &dispatch))
.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);