You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(observability): admin-only per-request timing detail via X-OS-Debug-Timing: json (#2408) (#3169)
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.
Claude-Session: https://claude.ai/code/session_011upHdyr5AnNc6dWAu63qxD
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/OBSERVABILITY.md
+27-3Lines changed: 27 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -271,6 +271,28 @@ they can never pull timings, so this is safe to leave available on a live
271
271
environment. This is the path to reach for when diagnosing "why is *this* request
272
272
slow?" against a running environment.
273
273
274
+
For a per-query breakdown, an admin sends `json` instead:
275
+
276
+
```
277
+
X-OS-Debug-Timing: json
278
+
```
279
+
280
+
which adds an **admin-only**`X-OS-Debug-Timing-Detail` response header — compact
281
+
JSON listing the slowest SQL statements (by shape), the single slowest, and the
282
+
query count:
283
+
284
+
```json
285
+
{"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}, …]}}
286
+
```
287
+
288
+
The statements are **parametrized** — knex's `?` placeholders, never the
289
+
bindings — so the query *shape* (the useful part for spotting N round-trips) is
290
+
exposed while literal row values never leave the server. The detail is
291
+
**admin-only even under global mode**: an ordinary caller who sends `json` still
292
+
gets the basic `Server-Timing` header (if global mode is on) but never the
293
+
`X-OS-Debug-Timing-Detail` payload. The list is capped and the labels sanitized
294
+
to header-safe ASCII.
295
+
274
296
To hard-disable both paths (no middleware registered at all), set
|`auth`| Dispatcher | Identity / session resolution — the prime suspect for unexplained data-API overhead. |
288
-
|`db`| SQL driver | Total SQL time across the request; `desc` is the **query count** (folded from knex's per-query events, attributed to the originating request via `AsyncLocalStorage` so it is correct under concurrency). SQL text is never emitted. |
310
+
|`db`| SQL driver | Total SQL time across the request; `desc` is the **query count** (folded from knex's per-query events, attributed to the originating request via `AsyncLocalStorage` so it is correct under concurrency). The basic header carries no SQL text; individual **parametrized** statements appear only in the admin-only `X-OS-Debug-Timing: json` detail payload. |
289
311
|`hooks`| ObjectQL engine | Total business-hook execution time; `desc` is the hook count. |
290
312
291
313
Each phase is recorded through a request-scoped collector that is a no-op when
0 commit comments