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): decompose Server-Timing into auth/db/hooks/serialize spans (perf-tuning mode) (#3107)
Completes #2408. The opt-in Server-Timing header now breaks a request's server
time into the phases that actually explain it, so an operator can read the
breakdown in DevTools -> Network -> Timing without standing up an external
tracing backend.
- collector: PerfTiming.count() / countServerTiming() fold high-frequency
events (per query, per hook) into one aggregate member
(name;dur=<sum>;desc="<count> <unit>").
- db: SqlDriver wires knex query / query-response events (keyed by
__knexQueryUid), attributing per-query time to the originating request via
AsyncLocalStorage (correct under concurrency; SQL text is never emitted).
- auth: the dispatcher times identity/session resolution.
- hooks: fed through the engine's existing HookMetricsRecorder seam, wired from
the runtime so objectql's lean core tier stays observability-free.
- serialize: the HTTP adapter times response JSON encoding.
Fix a latent dual-instance bug found by a real end-to-end HTTP test: the ambient
AsyncLocalStorage store was a plain module-level const, so the ESM and CJS builds
(or an inlined bundle) each got their own store. A request scope opened by the
HTTP server was then invisible to the SQL driver / engine, silently dropping the
cross-layer spans. The store is now pinned to a global-registry symbol so every
copy shares one.
Every span is a no-op when perf-tuning is off (serverTiming / OS_SERVER_TIMING),
so there is zero measurable overhead on the normal path. Gating is unchanged
(env flag / option); only durations and counts are exposed, never SQL text.
Claude-Session: https://claude.ai/code/session_01VoJNgMJRhgqm78xF7MxFhX
Co-authored-by: Claude <noreply@anthropic.com>
feat(observability): decompose `Server-Timing` into auth / db / hooks / serialize spans (perf-tuning mode)
9
+
10
+
The opt-in `Server-Timing` header now breaks a request's server time into the phases that actually explain it, so an operator can open DevTools → Network → Timing and see where the time went without standing up an external tracing backend:
11
+
12
+
-**`db`** — total SQL time with a **query count**. The SQL driver wires knex's `query` / `query-response` events (keyed by `__knexQueryUid`) and folds each query into one aggregate member (`db;dur=210;desc="6 queries"`) — the query count is the number most useful for spotting N sequential round-trips. Timing is attributed to the originating request via `AsyncLocalStorage`, so it is correct under concurrency and never cross-attributes. SQL text is never emitted, only durations and a count.
13
+
-**`auth`** — identity / session resolution in the dispatcher, the prime suspect for unexplained data-API overhead.
14
+
-**`hooks`** — total business-hook execution time with a hook count, fed through the engine's existing `HookMetricsRecorder` seam (wired from the runtime, so `@objectstack/objectql`'s lean `core` tier stays observability-free).
15
+
-**`serialize`** — response JSON encoding in the HTTP adapter.
16
+
17
+
Adds `countServerTiming(name, dur, unit)` (and `PerfTiming.count`) to fold high-frequency phases into a single aggregate member instead of flooding the header. Every phase is a no-op when perf-tuning is off (`serverTiming: true` / `OS_SERVER_TIMING=true`), so there is zero measurable overhead on the normal path.
|`auth`| Dispatcher | Identity / session resolution — the prime suspect for unexplained data-API overhead. |
270
+
|`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. |
271
+
|`hooks`| ObjectQL engine | Total business-hook execution time; `desc` is the hook count. |
272
+
273
+
Each phase is recorded through a request-scoped collector that is a no-op when
274
+
the mode is off, so every one of them costs nothing on the normal path. The
275
+
`db` / `hooks` aggregates fold high-frequency events into a single member via
276
+
`countServerTiming` (below) rather than emitting one member per event.
0 commit comments