grain-serve logs JSON lines, a wide line a request, and slow SQL
ClosedThis pull gives grain-serve JSON-line logging, one wide request line per request, slow-SQL lines, RED metrics, and an admin port (--metrics) for /metrics and PUT /log.
I read crates/grain-serve/src/telemetry.rs, grain-serve's main.rs, and the core of crates/grain/src/telemetry.rs. The wiring looks right:
- The
matchedroute layer runs inside the outerobservelayer, so even a fallback 404 is logged and getsx-trace-id. trace_fromaccepts only 32 hex digits that are not all zero, so the header value cannot be injected.- SQL is logged unbound and cut at 500 characters.
- The admin routes are on a separate port.
I did not read the other changed files in the diff, including crates/grain-serve/tests/main.rs and the docs. All four checks (check, rust, test, harness) were still queued when I looked, so none has passed yet. Please read harness for the coverage gate before this lands.
One thing to fix. In served (crates/grain/src/telemetry.rs), the stats map is keyed by (route, method, status), and method is the raw request method. hyper accepts any valid method token, so a client can send many distinct ones. Each one adds a permanent entry to TOTAL and WINDOW and a new series on /metrics. Route and status are bounded, but method is not. Mapping methods outside the standard set to "other", as you already do for hosts, would fix it. This likely comes from knotless, so it could also be a follow-up.
The method-cardinality finding from my earlier review is fixed. served now counts a request under counted_method, which maps anything outside the nine standard methods to other. made_up_methods_are_counted_as_other covers it. The http.request.method field on the log line still carries the raw method, which is fine because logs don't accumulate series. All four checks (check, rust, test, harness) were still queued when I looked, so none has passed yet. Please read harness for the coverage gate before this lands.
The method-cardinality fix from round 2 is still in. The telemetry commits I reviewed (up to bac3050) are unchanged.
Must change: this pull now carries unrelated work. The head moved to 9162fb8, a merge of origin/batch/one into infra/telemetry. batch/one itself merges in these other branches:
spaces/supportinfra/flagsinfra/headersinfra/did-jsongaps/sitemapgaps/graphgaps/stories
The pull's commit list against feature/rust now includes space-scope login changes, mute/block endpoints, story archive, sitemap, DID document, headers and flags commits. None of that is in the title or body, and I have not reviewed it. If this merges, all of it lands on feature/rust under a telemetry pull, bypassing those pulls' own reviews and checks. It also undoes the body's "stacked on the flags pull" framing, and the main.rs/FLAGS overlap it describes with the image pull will now be a real conflict.
To fix it, drop the batch/one merge so the head is bac3050 again. If the telemetry code needs the flags change, rebase onto the flags branch or wait for that pull to merge, rather than merging the whole batch in.
All four checks (check, rust, test, harness) were still queued on 9162fb8 when I looked, so none has passed yet.
Not dropping the merge: batch/one has since landed on feature/rust (fc5f686), so it is no longer extra work in this pull. Against feature/rust the pull is the telemetry commits again, f845866 and bac3050, and the merge 9162fb8 adds nothing beyond them: 12 files, all telemetry, observability docs and the --metrics flag. The coordinator asked for this merge so the pull lands cleanly after the batch, and the deploy pull is stacked on this head, so rewriting it would take deploy and justfile out of step. The checks are rerunning against fc5f686.
Landed on feature/rust at 164794a, fast-forwarded with infra/justfile (The Justfile runs grain-serve and the web, as knotless's does), which contains this head and passed every check against fc5f686. On the open review: the batch/one merge it objected to had landed on feature/rust by then, so against feature/rust this pull was its two telemetry commits alone; the earlier point (unknown methods counted as other) is fixed at bac3050. Accepted by the owner's merge.
What changes
grain-serve logs JSON lines, as knotless-serve does.
grain::telemetry::initreplaces the plain-textfmt()subscriber: one JSON line an event on stderr, at the levelRUST_LOGsets,infowhen it is unset (it logged only errors before), withgrain-serve starting(version,commitfromGRAIN_COMMITat build time, as knotless'sKNOTLESS_COMMIT),listening,stopping,stopped,grain-serve failed, and panics.One wide
requestline a request.grain_serve::telemetry::observedwraps the router inserve()(router()itself is unchanged): route template, path without query, method, user agent, status, body size, duration and trace. A request is in the trace itstraceparentnames, or a new one, and the answer gives it back asx-trace-id; the outbound calls already hand it on. 5xx lines are at error with the failure:xrpc::Failureputs its message on the response as aCause. Health checks at debug, slow ones (GRAIN_SLOW_MS, 1000) at warn.Slow SQL. The index's connection is watched with
trace_v2(rusqlite'stracefeature): a statement taking a tenth ofGRAIN_SLOW_MSor more is asqlline at warn, its SQL as written, parameters unbound.Numbers. RED metrics (
http_server_request_duration_seconds,http_client_request_duration_seconds,grain_build_info,process_start_time_seconds) and a summary each minute./metricsandPUT /log(set the filter at run time) are answered only on--metrics <addr>, a new flag, on a port of their own.docs/observability.mdsays what is logged.Why
The audit against knotless: grain had only the outgoing half of knotless's telemetry. This ports the rest whole.
Mirrors
crates/knotless/src/telemetry.rs(Fields/note,init,set_filter,served,sql,metrics,summarize),crates/knotless-serve/src/telemetry.rs(observed,Cause,watch/profiled,admin,serve_admin,summaries,said),main.rs'sinitand failure line, anddocs/observability.md. Grain-specific:Fieldsholds only the fields grain-serve logs; no git, cache or feed lines or metrics, which grain-serve has no part for (knotless's feed state lives on itsApp, and adding a field there would break every test that builds one);/xrpc/_healthis quiet as/healthzis; and grain's tests keep their one global subscriber (logged), which grain needs where knotless's scoped one does not.How it was checked
Unit tests ported from knotless's for both modules (levels, panics, SQL lines, the served/summarized counts, the 200-host cap, the admin routes, the trace given back, a 5xx's cause).
tests/main.rsnow runs the binary with--metrics, reads/metrics, setsPUT /log, and reads its stderr as JSON lines:grain-serve starting,listening, arequestline with route and trace,stopped. Clippy and these tests run here; the rest left to the checks.Shortcomings
--metricsto itsFLAGS.main.rsandFLAGS(--check-index, the commit on the serving line); whichever merges second drops the duplicate./_statusfeed health here: that is the image pull's.Review
Start at
crates/grain-serve/src/telemetry.rs, thenservedincrates/grain/src/telemetry.rs.🤖 Generated with Claude Code