Name request traces by route, and no colour codes off a terminal (#87, #88)

A request's trace was named by its path, so every item's GUID in
POST /api/entries/{feed_id}/{guid}/flags made a trace name of its own and
nothing grouped in Tempo. A route layer now renames it once routing has
matched. It renames the OpenTelemetry span directly: tracing-opentelemetry
drops a recorded otel.name once the span has been entered, and access_log
enters it before routing runs.

tracing-subscriber's fmt layer writes ANSI colour by default, so docker
logs and Loki (through Alloy) carried escape codes on every line, which
each query had to strip. Colour is now for a terminal only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-29 13:33:11 +00:00
parent 1724346da7
commit 9e7c93e149
3 changed files with 20 additions and 1 deletions

View File

@@ -191,6 +191,9 @@ async fn main() -> Result<()> {
.with(
tracing_subscriber::fmt::layer()
.with_writer(std::io::stderr)
// Colour for a terminal only: in docker logs and Loki the escapes are noise
// every query has to strip (#88).
.with_ansi(std::io::IsTerminal::is_terminal(&std::io::stderr()))
.with_filter(stderr_filter),
)
.with(logbuf::RingLayer.with_filter(ui_filter))

View File

@@ -4,7 +4,7 @@ use anyhow::{Context, Result};
use axum::http::HeaderMap;
use axum::{
Json, Router,
extract::{Path, Query, Request, State},
extract::{MatchedPath, Path, Query, Request, State},
http::{StatusCode, header},
middleware::{self, Next},
response::{
@@ -79,6 +79,7 @@ pub fn router(state: WebState) -> Router {
.route("/app.css", get(app_css))
.route("/login.js", get(login_js))
.route("/inter.woff2", get(inter))
.route_layer(middleware::from_fn(name_span))
.layer(middleware::from_fn(access_log))
.with_state(state)
}
@@ -1930,6 +1931,19 @@ async fn logs(user: crate::db::User, Query(q): Query<LogQuery>) -> Result<Json<L
Ok(Json(LogPage { lines, latest }))
}
/// Names a request's trace by its route, `POST /api/entries/{feed_id}/{guid}/flags`, once routing
/// has found it. Named by the path `access_log` sees, every item's GUID was a trace name of its
/// own, and nothing grouped. A route layer, because only one runs after routing. Renamed on the
/// OpenTelemetry span itself: recording `otel.name` is ignored once the span has started.
async fn name_span(route: MatchedPath, req: Request, next: Next) -> Response {
use opentelemetry::trace::TraceContextExt;
use tracing_opentelemetry::OpenTelemetrySpanExt;
let span = tracing::Span::current();
span.context().span().update_name(format!("{} {}", req.method(), route.as_str()));
span.record("http.route", route.as_str());
next.run(req).await
}
/// One line per HTTP request, so the web side shows up in the same log as the daemon.
///
/// The log view polls `/api/logs`, so logging that path would generate a line per poll
@@ -1949,6 +1963,7 @@ async fn access_log(req: Request, next: Next) -> Response {
otel.name = %format!("{method} {path}"),
http.request.method = %method,
url.path = %path,
http.route = tracing::field::Empty,
http.response.status_code = tracing::field::Empty,
)
};