Break Server-Timing into sess, db and tpl metrics

`app` alone said how long the origin took; it could not say where. The
header now also carries `sess` (the session/auth layers around the
routed stack), `db` (SQLite statement time, statement count in `desc`)
and `tpl` (askama rendering), each only where it was measured.

`db` needs a tracing layer: sqlx-sqlite runs statements on a worker
thread and only reports elapsed time there, inside the span the caller
handed it. The new `web::timing` module hangs the request's metrics on a
per-request span and reads them back off sqlx's `sqlx::query` event.
The layer's filter admits every span up to DEBUG, which is what lets it
see the request span past the middleware spans below it; the module docs
spell out the process-wide cost of that.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Va5eMEmWEnjMXBsBob5FDW
This commit is contained in:
thalladaandClaude Fable 5.1 committed 2026-09-05 03:12:02 +00:00
1 parent 9c4964ae27
commit 17a88043ff
5 files changed
+471 -23

No files matched your search

+6 -3
View File
@@ -308,9 +308,12 @@ pub async fn feed(State(state): State<AppState>) -> Result<Response, WebError> {
};
updated = updated.max(view.issue.meta.generated_at);
let issue = PublicIssue::from(&view.issue);
let content = FeedEntryTemplate { issue: &issue }
.render()
.map_err(|error| WebError::Internal(error.into()))?;
// Counted towards `tpl` by hand: the feed renders per entry rather
// than through `Html`, which is where every HTML page is measured.
let started = std::time::Instant::now();
let content = FeedEntryTemplate { issue: &issue }.render();
crate::web::timing::record_render(started.elapsed());
let content = content.map_err(|error| WebError::Internal(error.into()))?;
let href = format!(
"{}/issues/{}",
config.server.public_url.trim_end_matches('/'),