Uh oh!
There was an error while loading. Please reload this page.
fix(observability): retire http_request_errors_total (ADR-0049 enforce-or-remove) - #10226
Conversation
…e-or-remove)
The counter was DECLARED in SEMCONV as part of a stable namespace hosts wire
alerts against, but emitted only from the runtime dispatcher's per-route
wrapper, on a THROWN handler — so it never saw auth's getRawApp() mount, the
REST data API, or any error answered politely through errorResponseBase, while
counting thrown 4xx as errors. Its two siblings moved to the transport seam;
this one could not, because HttpResponseObservation carries no throw signal at
all, so every transport-side shape would have counted a different population.
Maintainer ruling 2026-08-20 (option C of four presented): remove the counter
and its emission, point the docs' 5xx-rate guidance at
http_requests_total{status=~"5.."} — which the transport emits for every
inbound surface — and announce the removal loudly for external dashboards.
ADR-0087 semantic ledger entry http-request-errors-total-retired registered at
protocol 17; spec-changes.json and the upgrade guide regenerated.
Part of #9834
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r… for strict typing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift CheckThis PR changes 3 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 121 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 b511a223eecf1e8fab3e37b435366ed52bed7ef2 && git checkout b511a223eecf1e8fab3e37b435366ed52bed7ef2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 04096f17e14dccd9c8c9f1e5e924e1fc4dd99398 54809697dfe842aa9d9370e6477851ab7c5f59e2 && git checkout -B drift-repro 04096f17e14dccd9c8c9f1e5e924e1fc4dd99398 && git merge --no-ff 54809697dfe842aa9d9370e6477851ab7c5f59e2
node scripts/docs-audit/affected-docs.mjs --json 04096f17e14dccd9c8c9f1e5e924e1fc4dd99398
|
Uh oh!
There was an error while loading. Please reload this page.
Part of #9834 — this is the error-counter half of that card; the duration half shipped in #10004.
What the maintainer ruled
Option C: retire
http_request_errors_totalunder ADR-0049 enforce-or-remove(2026-08-20, comment
5354029742, verbatim 「4 张 同意」). Remove the counter andits emission; point the docs' 5xx-rate guidance at
http_requests_total{status=~"5.."}, which has covered every inbound surfacesince #9650 / #10004; register the ADR-0087 conversion for the removed published
name; announce the removal loudly in the changeset for any external dashboard.
Escape clause — checked, not tripped
The ruling carries a binding escape clause: if implementation or any operator
report surfaces real deployed alerting keyed on this name, STOP — the ruling
flips to D (declare-and-rename).
Nothing surfaced. Swept for consumers of both the wire string and the symbol
across
objectstackand the siblingobjectuicheckout, plus a scan for anydashboard / alert-rule / Prometheus config in-tree (there are none). Every hit
was the emitter, its own tests, prose docs, or historical
CHANGELOG.mdrows.The known-and-accepted limit is unchanged and worth restating: this repo is
structurally blind to a Grafana panel or alert rule in a deployed environment.
That gap was stated when C was presented and is what the loud changeset and the
ledger entry exist for — it is not new information, and it is not an operator
report.
The premise, re-measured on
origin/main2d3860df9Re-derived by symbol rather than by line number, and it stands:
packages/observability/src/semconv.tsSEMCONV.httpRequestErrorsTotaldeclaration and itsRUNTIME_METRICSre-export — two sites, not onepackages/runtime/src/observability/instrument.tsmetrics.counter(...)insideinstrumentRouteHandler'scatchinstrument.test.ts·metrics.test.tsdispatcher-plugin.ts·docs/OBSERVABILITY.md·content/docs/deployment/production-readiness.mdx·packages/runtime/README.mdsemconv.tshas two, theSEMCONVmember (:45) and theRUNTIME_METRICSalias (:120). The alias isthe name
@objectstack/runtimeactually re-publishes, so leaving it would havekept the string exported from the package the docs point hosts at.
What changed
Removed — the
SEMCONVdeclaration, theRUNTIME_METRICSalias member, andthe
metrics.countercall in thecatchblock.Deliberately NOT removed, and pinned so: the
catchstill recordserr.statusCode ?? 500(which reacheshttp_requests_total{status}in thefinally), still callserrorReporter.captureExceptionfor 5xx, stillre-throws; request-id echo and the
res.__obsRecordedErrorside channel areuntouched. The retirement removes a series, not the error path.
Tests — triaged one by one, not batch-edited. Four assertion sites asserted
the counter incremented. Each was replaced by the assertion that survives at
that site, plus a tombstone: a
retiredErrorCounterSamples()probe asserting nosample is ever written under the retired name. Two negative pins were added — one
on the emission path (
instrument.test.ts) and one on the published constant(
metrics.test.ts, plus a newpackages/observability/src/__tests__/semconv.test.tsguarding
SEMCONVitself, in both directions: the member key and the wirestring, so a re-add under a different member name is caught too).
One new case records the half of the old population a status-class replacement
would have dropped: a thrown 4xx was counted by the retired counter and is
counted by nothing now.
ADR-0087 conversion — registered as a
SemanticMigration(
http-request-errors-total-retired) at protocol 17, not aMetadataConversionand not a
retiredKey()tombstone: a metric name is a response surface, sono stack, example or template carries it and there is no authored source for a
D2 chain to rewrite. Precedents in the same table:
runtime-httpserver-wrapper-retired,enhanced-api-error-field-errors-renamed. Registered at 17 rather than 18because the removal takes effect on the 17.x line, matching
ups-delegated-from-column-retired(#9998, landed 2026-08-19).
spec-changes.jsonanddocs/protocol-upgrade-guide.mdare regenerated projections. ⛔ No governed ADR file is touched — the ADR-0087
surface here is entirely the code registries.
Verification
Everything below is on head
54809697d. Exit codes were captured byredirecting to a file first (
cmd > log 2>&1; rc=$?), never through a pipe.Tests — full package suites, not a targeted subset:
pnpm --filter @objectstack/observability exec vitest run --maxWorkers=2Test Files 7 passed (7)·Tests 85 passed (85)(6 files / 83 before —+1file,+2cases: the newsemconv.test.ts)pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2Test Files 177 passed (177)·Tests 2651 passed (2651)pnpm --filter @objectstack/runtime typecheck·pnpm --filter @objectstack/spec typecheck@objectstack/observabilitydeclares notypecheckscript (build + test only) — apre-existing ledgered state, not a gap opened here;
check:type-check-coverageaccountsfor it.
Reverse verification — three legs, each mutated and restored, each stating its resolution path.
packages/runtime's vitest config aliasescore/rest/spec/platform-objects/… butnot
@objectstack/observability, so anything readingRUNTIME_METRICSresolves throughexports→dist/. Legs 2 and 3 therefore rebuild and are proven to have reached the builtartifact with
scripts/ablation-dist-preflight.mjs(present on the mutation,--absentonthe restore); leg 1 mutates runtime source that vitest reads directly, and says so rather
than claiming a rebuild it did not need.
metrics.counter(...)ininstrument.ts'scatch(runtime src, source-resolved)instrument.test.ts5 failed | 17 passed (22)SEMCONVdeclarationsemconv.test.ts1 failed | 1 passed (2)RUNTIME_METRICSaliasmetrics.test.ts1 failed | 9 passed (10)⭐ Leg 3 exists because leg 2 did not turn
metrics.test.tsred: mutatingSEMCONValone leaves the
RUNTIME_METRICSalias without the member, so that pin correctly stayedgreen. Reporting leg 2 as proof of all three pins would have been a false green about a
pin that had never been shown able to fail — so the alias got its own leg.
Gates — every exit code captured by redirecting to a file first (
cmd > log 2>&1; rc=$?),never through a pipe. Re-derived with
node scripts/pm/dispatch-gates.mjs(no paths — thescript derives its own change set; 14 paths, 115 families across 26 workflow files), which
named 10 families the dispatch brief could not:
check:changeset-gate-self-tests,check:doc-formula-expressions,check:merge-driver,check:objectui-changeset,check:spec-parsed-alias,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-dev-prereqs.All green, RC 0:
pnpm lint(repo-wide) ·check:nul-bytes·check:doc-anchors·check:docs-redirects·check:docs-audit-scope·check:published-readme-links·check:role-word·check:route-envelope·check:slot-lookup·check:cross-package-test-inputs·check:test-source-alias·check:type-source-resolution·check:spec-parsed-alias·check:merge-driver·check:objectui-changeset·check:changeset-gate-self-tests·check:engine-double-contract·check:where-matcher·check:query-options-erasure·check:type-check-coverage·check:type-check-debt·check-dev-prereqs· speccheck:liveness/check:empty-state/check:strictness-ledger/check:variant-docs/check:migration-registry/check:spec-changes/check:upgrade-guide·scripts/check-adr-0087-registration.mjs·scripts/check-changeset-no-major.mjs·scripts/check-empty-changeset.mjs·scripts/check-cross-package-test-inputs.mjs·scripts/docs-audit/check-affected-docs.mjs.⛔ No ratchet moved, no baseline raised, no test skipped. The two that would have said so
print their own verdicts:
check:type-check-debt— "check-type-check-coverage --re-measure: OK — 33 ledger entr(ies)re-measured in 364.6s, 1924 raw tsc error(s) total, none above its recorded number. surplus:
none — every entry sits exactly at its measurement, so any new error is red." Measured on a
fully built workspace (
turbo run build --filter='!@objectstack/docs'RC 0, thencheck-dev-prereqsRC 0 — it had refused before the build, which is NOT MEASURED, not"not applicable").
check:where-matcher/check:query-options-erasure— "baseline key set verified against2d3860d: no files added."
Verify-lock discipline: every build, test and heavy gate ran through
scripts/pm/os-verify-lock.sh; no hand-rolledflock. Contention was heavy (four devs) — sixacquisitions and seven
VERDICT queue-timeout (exit 99)returns, longest successful wait6m05s, longest hold 6m06s (
check:type-check-debt --re-measure, 364.6s of it inside the gate).⛔ Not enqueueable
The card carries
needs:contract-review(Clause-② yes — published-surfaceremoval). Draft only: no ready-flip, no auto-merge, and the label is not
mine to clear. A different seat clears contract review.
Generated by Claude Code
Generated by Claude Code