feat(observability): admin-only per-request timing detail via X-OS-Debug-Timing: json (#2408)#3169
Merged
Merged
Conversation
…bug-Timing: json (#2408) Completes the optional "richer JSON" diagnostic from #2408. On top of the basic Server-Timing header, an admin/service caller can request a per-query breakdown — the slowest SQL statements + a query count — with `X-OS-Debug-Timing: json`, returned in a separate `X-OS-Debug-Timing-Detail` header (compact JSON). It is admin-only **even under global mode** — an ordinary caller never sees SQL shapes. - observability: `PerfTiming` gains opt-in per-event detail capture (`enableDetail` / `recordDetail` / `details`) + ambient `recordServerTimingDetail`. The disclosure gate gains a `privileged` level (set by `allowPerfDisclosure`, read via `isPerfDisclosurePrivileged`) so the richer detail is gated independently of the basic header. - driver-sql: with detail on, the query listener also records each query's PARAMETRIZED statement (knex `q.sql`, `?` placeholders) — never the bindings, so no literal row value enters the collector. Free when detail is off. - plugin-hono-server: `X-OS-Debug-Timing: json` enables capture; the middleware emits `X-OS-Debug-Timing-Detail` (slowest queries, capped + sanitized to header-safe ASCII) only for a proven admin. Tests: unit coverage for the detail collector, the privileged gate level, the driver's parametrized-SQL capture, the middleware's json/basic/global paths, and a real-HTTP end-to-end test (admin gets the detail header over a genuine socket running real SQL; a non-admin never does, even under global mode). Docs + changeset updated. Basic/global behavior unchanged; `json` is purely additive. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011upHdyr5AnNc6dWAu63qxD
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📓 Docs Drift CheckThis PR changes 4 package(s): 19 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
5 tasks
os-zhuang
marked this pull request as ready for review
July 18, 2026 12:22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Follow-up to #3163, completing the optional richer JSON diagnostic from #2408. The basic
Server-Timingheader tells you how many queries ran and their total time — but not which ones. This adds an admin-only per-query breakdown: an admin/service caller sendsX-OS-Debug-Timing: jsonand gets back the slowest SQL statements (by shape) in a separateX-OS-Debug-Timing-Detailresponse header.Crucially, the detail is admin-only even under global mode — an ordinary caller never sees SQL shapes, and even an admin only ever sees the parametrized statement (knex's
?placeholders), never the bindings, so no literal row value leaves the server.How
@objectstack/observability—PerfTiminggains opt-in per-event detail capture (enableDetail/recordDetail/details) plus the ambientrecordServerTimingDetail. Detail is off by default (zero allocation on the hot path) and bounded by a cap. The disclosure gate gains a second level,privileged(set byallowPerfDisclosure, read viaisPerfDisclosurePrivileged), so the richer detail is gated independently of the basic header.@objectstack/driver-sql— when detail capture is on, the knex query listener additionally records each query's parametrized statement + duration. Free when off (one boolean check).@objectstack/plugin-hono-server—X-OS-Debug-Timing: jsonenables detail capture; the middleware emitsX-OS-Debug-Timing-Detail(the slowest queries, capped and sanitized to header-safe printable ASCII) only when the principal is a proven admin (gate.privileged).Detail payload shape
{"db":{"count":6,"totalMs":210.3,"slowest":{"sql":"select * from widgets where id = ?","dur":88.1},"queries":[{"sql":"select * from widgets where id = ?","dur":88.1}, …]}}Disclosure matrix
X-OS-Debug-Timing: 1, non-adminX-OS-Debug-Timing: 1, adminX-OS-Debug-Timing: json, non-adminX-OS-Debug-Timing: json, adminTests / verification
privilegedgate level, the driver's parametrized-SQL capture (asserts the literal binding never appears), and the middleware's json/basic/global paths incl. the "never leak detail under global mode" invariant.server-timing.integration.test.ts): boots a real Hono server + real SQLite driver, and over a genuine socket confirms an admin sendingX-OS-Debug-Timing: jsongets theX-OS-Debug-Timing-Detailheader with the actual query shapes, while a non-admin never does — even under global mode. This is the "does it work against a running server?" check that a browser dogfood would do, made deterministic.docs/OBSERVABILITY.md+ aminorchangeset updated.All affected package builds (DTS typecheck) pass. Basic and global behavior are unchanged —
jsonis purely additive.Note on the open-core boundary
This is the last framework-side piece of #2408. The remaining item — surfacing this as a cloud console "perf panel" and defining the cloud's own "who is admin" policy — lives in the
cloudrepo (mechanism here, policy there), consistent with the existing observability split.Generated by Claude Code