ipx-directory traces, and ipx's trace follows its calls there (#213)

The exporter setup and the traceparent parser move from ipx into ipx_directory::otel, which ipx
already depends on, so both binaries share them. The directory makes a span a request, a child of
the caller's traceparent, and a trace a refresher round with a span a feed; ipx sends the current
span's traceparent on every call to the directory and wraps each share in a span. The dashboard's
Traces row and the prod-check skill cover all three services.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 16:19:32 +00:00
parent a3708769a4
commit 44e75df99e
12 changed files with 188 additions and 94 deletions

View File

@@ -8,6 +8,7 @@
//! people's behalf.
pub mod config;
pub mod otel;
pub mod store;
use std::sync::{Arc, RwLock};
@@ -177,14 +178,23 @@ pub async fn refresher(dir: Shared) {
loop {
match dir.store.due(REFRESH_EVERY, 25).await {
Ok(due) => {
for d in &due {
if let Err(e) = dir.refresh(d).await {
tracing::warn!(feed = %d.id, error = %format!("{e:#}"), "could not store a feed check");
}
}
if !due.is_empty() {
// One line a round, for the dashboard's refresher panel (#210); failures have their own.
tracing::info!(checked = due.len(), "feeds checked");
// A round is a trace, each feed's check a span in it (#213).
let round = tracing::info_span!("refresh", feeds = due.len());
tracing::Instrument::instrument(
async {
for d in &due {
let check = tracing::info_span!("feed", feed = %d.id);
if let Err(e) = tracing::Instrument::instrument(dir.refresh(d), check).await {
tracing::warn!(feed = %d.id, error = %format!("{e:#}"), "could not store a feed check");
}
}
// One line a round, for the dashboard's refresher panel (#210); failures have their own.
tracing::info!(checked = due.len(), "feeds checked");
},
round,
)
.await;
continue;
}
}
@@ -264,7 +274,26 @@ struct Who(String);
async fn access_log(req: Request, next: Next) -> Response {
let (method, path) = (req.method().clone(), req.uri().path().to_owned());
let start = Instant::now();
let resp = next.run(req).await;
// A span for each request but the healthcheck's, a child of the caller's when it sent a
// traceparent, as ipx does on its calls here (#213).
let span = if path == "/healthz" {
tracing::Span::none()
} else {
tracing::info_span!(
"http",
otel.name = %format!("{method} {path}"),
http.request.method = %method,
url.path = %path,
http.response.status_code = tracing::field::Empty,
)
};
if let Some(parent) = otel::remote_parent(req.headers(), None) {
use tracing_opentelemetry::OpenTelemetrySpanExt;
let _ = span.set_parent(parent);
}
let resp = tracing::Instrument::instrument(next.run(req), span.clone()).await;
span.record("http.response.status_code", resp.status().as_u16());
let _in = span.enter();
let who = resp.extensions().get::<Who>().map_or("", |w| w.0.as_str()).to_owned();
let status = resp.status().as_u16();
if path != "/healthz" {