Uh oh!
There was an error while loading. Please reload this page.
fix(hono-server): emit http_requests_total from the transport so every inbound mount is counted (#9650) - #9746
Conversation
… both mounts (#9650) Measurement harness only - no fix. Pins what the counter observes on the current wiring and what each of the two pre-declared candidate seams covers. Co-Authored-By: Claude <noreply@anthropic.com>
…te seams (#9650) Measurement only, no fix: the seam choice is a design fork and is being escalated rather than picked. - section 1 pins what http_requests_total observes on the current wiring: the dispatcher's own routes yes, the auth getRawApp() mount no, the REST data route mounted through the real RouteManager no. - section 2 measures seam A (register the instrumented proxy back as the http.server service): getRawApp passes through unwrapped, only get/post/delete are trapped, registerService refuses a second registration, and a consumer resolving in an earlier start() keeps the raw handle. - section 3 measures the raw-app middleware seam at three install points and shows the framework-agnostic use() seam cannot observe status. Co-Authored-By: Claude <noreply@anthropic.com>
…p-metrics-auth-and-rest-coverage
📓 Docs Drift CheckThis PR changes 1 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a8e6e2d54383df7891b69b74e7e4539d337cc1ed && git checkout a8e6e2d54383df7891b69b74e7e4539d337cc1ed
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9ff11921a26f44a62ca834651adaa54105468536 8957c72227868200a75fd3ea44a77176e353b7c7 && git checkout -B drift-repro 9ff11921a26f44a62ca834651adaa54105468536 && git merge --no-ff 8957c72227868200a75fd3ea44a77176e353b7c7
node scripts/docs-audit/affected-docs.mjs --json 9ff11921a26f44a62ca834651adaa54105468536
|
The service-lookup slots carried `getService<any>` purely so a stub could expose an `id` marker. Typed to IHttpServer (core-service-contracts.ts:155) and switched to identity comparison instead, which needs no marker member and is the stronger assertion: a Proxy is never === its target. No baseline and no eslint config were touched. Co-Authored-By: Claude <noreply@anthropic.com>
…p-metrics-auth-and-rest-coverage
…y inbound mount is counted (#9650) The counter had one emitter — a Proxy the runtime dispatcher built over its own IHttpServer handle — so it saw only the routes the dispatcher registered. Auth (getRawApp) and the REST data API (RouteManager), the two highest-traffic inbound surfaces, were outside it while the docs told operators to alert on exactly that counter. Emit it from the Hono adapter instead, as a raw-app middleware installed at the end of HonoServerPlugin.init() beside installMiddlewareSeam() — the one layer every inbound request converges on. Route label is the matched PATTERN, never the concrete path. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WeN7F6jQFpcqW2BN56RdPa
) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WeN7F6jQFpcqW2BN56RdPa
…p-metrics-auth-and-rest-coverage
…gins, not origin) Caught by check:type-check-debt, not by `pnpm --filter @objectstack/runtime typecheck` — that program excludes **/*.test.ts, so the file it would have flagged is not in it. TEST_DEBT is back at its frozen 227, 0 from this file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WeN7F6jQFpcqW2BN56RdPa
Uh oh!
There was an error while loading. Please reload this page.
Part of #9650. This PR carries the fix, replacing the measurement-only scope it opened with. It supersedes this PR's earlier "measurement only, no fix" framing: the design fork that made the card expensive was ruled on 2026-08-18, and what remained was the seam plus its pins. Everything below is measured on
8957c72227(origin/mainmerged at9ff11921a2).The ruling this implements
Why the transport, and not one layer up
The counter had exactly one emitter: a
Proxythe dispatcher built over its ownIHttpServerhandle. Measured on this branch, that left at least 14 inbound surfaces uncounted in two structurally different classes — and the first class is unreachable by construction:getRawApp()mounts (11): auth, metadata HMR, cloud-connection, marketplace proxy and install-local, runtime-config, trigger-api, webhooks, approvals, the console SPA, the unknown-hostname guard. These never touchIHttpServer, so no wrapper at that level can ever see them.http.serverconsumers mounting through verb methods (7): the REST data API viaRouteManager, storage, i18n, settings, datasource admin, the dispatcher itself, and the non-Hono conformance adapter.The change
packages/plugins/plugin-hono-server/src/adapter.tsinstallHttpMetricsSeam(metrics)— idempotent, mounts oneapp.use('*')that emitshttp_requests_total{method,route,status}afternext()packages/plugins/plugin-hono-server/src/hono-plugin.tsobservability.metricsoption; resolves the canonical chain and installs the seam immediately beforeinstallMiddlewareSeam()packages/runtime/src/http-metrics-inbound-coverage.hono.integration.test.tsRoute label is the matched pattern.
routePath(c)fromhono/route— the non-deprecated spelling of the ruledc.req.routePath, returning the same value with a safe fallback instead of an index throw. Measured: two requests to/api/v1/data/abc123and/api/v1/data/def456produce one series,/api/v1/data/:id; the auth wildcard is labelled/api/v1/auth/*, not/api/v1/auth/sign-in/email.Order is load-bearing, in both directions. Before the middleware seam, so a request the
use()chain short-circuits — the inbound rate limiter's 429 — is still counted; a refused request is exactly the one an operator alerts on. Still at the end ofinit(), because every route in the platform mounts in some plugin'sstart()and Hono composes matched handlers in registration order.Resolution chain, the canonical one from
ObservabilityServicePlugin: explicit option, then theobservability:metricsservice, then nothing at all — no backend configured means no middleware installed, so an unconfigured deployment pays no per-request cost.Documented expectations, stated rather than left to be discovered
packages/qa/http-conformancedeliberately omitsgetRawApp()and has no equivalent, so it emits nothing. A zero there means "not instrumented", never "no traffic". The transport-agnostic successor is filed as spec: giveIHttpServera response-observing hook so HTTP metrics are transport-agnostic instead of Hono-only #9835, per the ruling.OPTIONSthe transport's own CORS built-in answers is not counted — it short-circuits before this seam and never reaches a route. Pinned as a case.The acceptance criterion, and how it was proved
The two assertions that read "does NOT count" now read "counts", and they flip together — a seam reaching one mount and not the other would reproduce the card one surface over. The positive control (the dispatcher's own route IS counted) stays: without it, "now counted" would be indistinguishable from a metrics injection that counts everything.
The harness is unchanged where it matters: a real Hono adapter booted in the shipped
serve.tsplugin order, the realRouteManagerfrom@objectstack/rest, and the exactgetRawApp().all(basePath + '/*')construct fromauth-plugin.ts:1622-1630.Ablation. Predictions written to a file before the run: delete the call block, expect RED, and specifically 7 of 18 red with the seven named. Both legs rebuilt, and
scripts/ablation-dist-preflight.mjsproved the artifact the suite consumes actually changed — marker absent after ablation, present after restore.✓ marker absent from all 6 built filesTests 7 failed | 11 passed (18)— exactly the seven predicted✓ marker present in 2 built filesTests 18 passed (18)The CORS case failed on its positive leg (
expected [] to include 'POST') whilenot.toContain('OPTIONS')passed vacuously against an empty array — which is why that positive leg is there.Verification, gate union re-run at
8957c72227Derived with
node scripts/pm/dispatch-gates.mjsfrom the actual changed paths (11 path-matched + 5 convention-triggered), pluspnpm lintandcheck:slot-lookup, which that derivation still does not name.pnpm --filter @objectstack/runtime exec vitest run(the pin)Tests 18 passed (18)pnpm --filter @objectstack/plugin-hono-server testTest Files 18 passed,Tests 211 passed (211)pnpm --filter @objectstack/hono testTests 73 passed (73)pnpm --filter @objectstack/plugin-hono-server typecheckpnpm lint(eslint . --no-inline-config)check:slot-lookupcheck:route-envelopecheck:type-check-debt(--re-measure, built closure)surplus: nonecheck:nul-bytescheck:engine-double-contractcheck:where-matchercheck:query-options-erasurecheck:type-check-coveragecheck:cross-package-test-inputs/check:test-source-alias/check:type-source-resolutioncheck:changeset-gate-self-tests,check-empty-changeset,check-adr-0087-registration,check-changeset-no-major,check:objectui-changesetscripts/docs-audit/check-affected-docs.mjsFull-workspace build before the ratchet:
Tasks: 70 successful, 70 total.The phantom green caught a real defect
pnpm --filter @objectstack/runtime typecheckis green and does not read the harness file — that tsconfig excludes**/*.test.ts.check:type-check-debtdid read it and went red at TEST_DEBT 227 → 228: a CORS option spelledoriginwhereHonoCorsOptionsdeclaresorigins, so the test's CORS config was silently falling back to defaults. Fixed at the call site; re-measured directly by compiling the hidden test layer: 227 total, 0 from this file, matching the frozen ledger exactly. ⛔ The ledger was not raised.Cross-card note —
scripts/check-route-envelope.mjscounters did not move#9436 owns that file this round. Verified by running the gate twice on the same tree, once with
adapter.tsreverted to the merge base: identical output both times (161 hand-built bodies;current-user-endpoints.tsclosed at unenveloped 9). This PR moves no counter there. ⛔ Neither that script noreslint.config.mjs'sUNCONTRACTED_SLOTSnorslot-lookup-baseline.jsonwas touched.What still blocks the card, and why the closing keyword is withheld
The seam is correct and inert in a shipped
objectstack servedeployment:serve.tsregisters noObservabilityServicePluginanywhere in the repo and buildsnew HonoServerPlugin({ port })with no observability, so neither link of the resolution chain exists and no middleware is installed. Filed as sub-issues of #9650:objectstack serveregisters noObservabilityServicePlugin, soobservability:metricsis unregistered — every consumer but the dispatcher resolves to nothing #9832 — the CLI never registersobservability:metrics, so this seam (and cache, and storage) resolve to nothing;instrumentRouteHandlerProxy double-countshttp_requests_totalfor its own routes once the transport seam emits #9833 — the dispatcher'sinstrumentRouteHandlerProxy double-counts its own routes once both are wired (measured: 2 for the dispatcher route, 1 for the auth mount, one request each). Not separable from the request-id header, duration histogram, error counter and error reporter, andpackages/runtimewas outside this dispatch's file surface;http_request_duration_msandhttp_request_errors_totalstill carry #9650's coverage hole — the p95-latency half of the operator guidance sees dispatcher routes only #9834 —http_request_duration_msandhttp_request_errors_totalstill carry the same coverage hole, so the p95-latency half of the operator guidance is still dispatcher-only.Also filed, as the ruling directed: #9835, the
IHttpServerresponse-observing contract hook.Generated by Claude Code