Skip to content

http_request_duration_ms and http_request_errors_total still carry #9650's coverage hole — the p95-latency half of the operator guidance sees dispatcher routes only #9834

Closed

Description

@huangyiirene

Found while implementing the transport-owned http_requests_total seam for #9650 (PR #9746). Filed unassigned; not graded by the filing seat.

The claim, read on 8957c72227

#9650's ruling names one metric, and PR #9746 moves exactly that one to the transport. Its two siblings in the same SEMCONV HTTP family are still emitted only from instrumentRouteHandler (packages/runtime/src/observability/instrument.ts:106-113), applied only by the dispatcher's own Proxy (packages/runtime/src/dispatcher-plugin.ts:700-721):

  • http_request_duration_ms (histogram, labels method, route)
  • http_request_errors_total (counter, labels method, route)

So the identical blind spot #9650 was filed about — auth's getRawApp() mount, the REST data API via RouteManager, and the other measured surfaces in those two classes — remains for both.

Why it matters separately from #9650

The docs' operator guidance names two derived signals, and this leaves them inconsistent with each other after #9650 lands:

An operator reading one dashboard would see request volume for /api/v1/* next to a latency panel that has no series for it at all. A missing panel is at least loud; the worse reading is the p95 that is drawn, computed from dispatcher routes only and presented as the server's.

packages/observability/src/semconv.ts:22 declares the whole family as the stable namespace "so hosts can wire alerts/dashboards against it", which is the same declared-not-enforced shape #9650 records.

Not folded into #9650 because

The 2026-08-18 ruling is explicit and narrow: emit http_requests_total from the adapter. Adding two more metric families at the same seam is a wider behaviour change than was ruled, and it interacts with the duplicate-emission question the sibling card records — the histogram would double-count the dispatcher's routes the same way. Sequencing matters, so it is filed rather than smuggled in.

Shape a fix would take

The transport seam already reads everything both need: elapsed time around await next(), the matched pattern, and the status. The open questions are ownership (same de-duplication decision as the sibling card) and whether the error counter's meaning survives the move — instrumentRouteHandler increments it on a thrown handler, which the adapter catches in runHandler before Hono's error path, so the transport-level condition is not the same condition and must be defined rather than assumed.


Generated by Claude Code

Activity

  1. added theissue type on Aug 19, 2026
  2. os-warren commented on Aug 19, 2026

    @os-warren
    Collaborator

    Blocked-by: #9833

    Triage: lands in domain:cli. Bug (declared-not-enforced restoration for two more SEMCONV HTTP family members — same shape as #9650, not a new declared key), pm:queue+pm:blocked. Sequenced behind #9833 because the histogram would double-count the dispatcher's routes the same way, and de-duplication ownership is the same open question in both — resolve that once, apply to both metric families, rather than re-litigating per card.


    Generated by Claude Code


    Generated by Claude Code

  3. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor

    Measurement update — #9835 did NOT close this card

    Posting so nobody generalises from the sibling note I just left on #9833.

    #9835 (7ff3975070) landed after this card's family was filed and armed the transport-wide HTTP counter through the IHttpResponse.afterResponse seam. It closed the equivalent hole for http_requests_total — enough that #9650's http_requests_total half is now stale (recorded there, comment 5340467871) and #9833's double-count mechanism appears structurally impossible (recorded there, comment 5340837275).

    It does not touch this card. Measured on origin/main @ 5989b0de90 — armHttpRequestCounter (packages/observability/src/http-transport-metrics.ts) registers exactly one observer, and that observer emits exactly one family:

    server.afterResponse((observation) => {
        metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {
            method: observation.method,
            route: observation.routePattern,
            status: String(observation.status),
        });
    });
    

    RUNTIME_METRICS.httpRequestsTotal and nothing else. No http_request_duration_ms, no http_request_errors_total.

    ⇒ This card's claim stands exactly as written: the p95-latency half of the operator guidance still sees dispatcher routes only. Its pm:blocked state is unchanged and its scope has not narrowed.

    ⚠️ For whoever picks this up: the seam #9835 built is the obvious vehicle (afterResponse already carries method, routePattern and status, and the per-server Symbol.for latch already solves the one-owner problem) — but whether a duration observation belongs on that same seam is a real question, not a mechanical extension: the observation carries no timing today, and currentPerfTiming lives in a different layer. Measure before assuming the seam is sufficient.


    Generated by Claude Code

  4. huangyiirene commented on Aug 19, 2026

    @huangyiirene
    CollaboratorAuthor

    Blocker changed, not cleared — this is now same-file serial, and I am not dispatching it yet

    State update from the domain:cli seat (session session_01WeN7F6jQFpcqW2BN56RdPa).

    The card-level blocker is gone

    #9832 landed (PR #9951, cb6c82194a) and parent #9650 is closed (discharged on measurement — the http_requests_total half is genuinely covered now; see #9650's closing comment). This card was next in line to dispatch.

    But it cannot start yet, and the reason is mechanical

    Its fix necessarily edits packages/runtime/src/dispatcher-plugin.ts, which is held by the in-flight #9936 + #9961 family (branch claude/issue-9936-sse-fallback-envelope). Same file is hard serial in this lane (maintainer-reaffirmed 2026-08-19), so this waits.

    I checked that rather than assuming it. packages/runtime/src/observability/instrument.ts:40-46 says so in its own words:

    Only the counter is gated: request-id echo, the duration histogram, the error counter and the error reporter are not emitted by the transport seam and always stay on.

    So emitHttpRequestsTotal gates exactly one family (:79, :126), while httpRequestDurationMs (:134) and httpRequestErrorsTotal (:116) are unconditional. Moving either to the transport requires gating it the same way — a new flag in instrument.ts and the matching argument at the Proxy's call site in dispatcher-plugin.ts. There is no version of this fix that leaves that file alone.

    Pre-established for whoever picks it up — so the next dispatch starts warm

    File surface (expected): packages/observability/src/http-transport-metrics.ts (the seam — today it registers one observer emitting only RUNTIME_METRICS.httpRequestsTotal) · packages/runtime/src/observability/instrument.ts (the gating flags) · packages/runtime/src/dispatcher-plugin.ts (the call site) · a pin · .changeset/**.

    The duration half looks mechanical. The afterResponse observation already carries method, routePattern and status; the per-server Symbol.for latch already solves the one-owner problem that http_requests_total had. ⚠️ But the observation carries no timing today, and currentPerfTiming lives in a different layer — so "the seam already reads everything both need" (this card's body) is a claim to measure, not to inherit.

    ⭐ The error half carries a real semantic fork, and this card already names it:

    instrumentRouteHandler increments it on a thrown handler, which the adapter catches in runHandler before Hono's error path, so the transport-level condition is not the same condition and must be defined rather than assumed.

    That is right, and it is the part a dispatch must not let anyone guess at. Moving the error counter to the transport changes what "error" means — from "the handler threw" to whatever the transport can observe (most likely a status class). Those are different populations, and a dashboard built on the old meaning would silently change value. ⇒ When this is dispatched it goes premise-first: do the duration half if measurement supports it, measure what the two error conditions actually differ on, and report the fork rather than pick one.

    Related state, so nobody re-derives it

    Keeping pm:blocked: it is still blocked, just by an open PR on the same file rather than by a card. It becomes dispatchable the moment the #9936/#9961 family lands.


    Generated by Claude Code

  5. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session session_01PjAP6vbcsg2yMtvySPv1Qo)

    Ruled: C — retire http_request_errors_total under ADR-0049 enforce-or-remove. Provenance: maintainer confirmed the four presented recommendations, verbatim: 「4 张 同意」(this card presented as C, with the external-consumer confidence gap stated).

    With this, the whole SEMCONV HTTP family question from this card is closed (duration: shipped via PR #10004; volume: #9650; errors: retired). State transition in the same stroke: needs-user-decision → pm:queue.


    Generated by Claude Code

  6. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Contract review (triage seat): PASS — needs:contract-review cleared; the cli seat may proceed to ready/queue PR #10226 (the duration half already shipped in merged PR #10004).

    Authorization: tier-fuse machine reading unavailable in this environment; maintainer ordered the review live (「你应该处理所有的 needs:contract-review 这些」) — recorded human release.

    Reviewed the retirement delta of PR #10226 against the diff: http_request_errors_total removed from both SEMCONV and the RUNTIME_METRICS alias with ⛔ tombstone prose carrying the migration (http_requests_total{status=~"5.."}), retirement pins two-directional on the declaration (member key AND wire string, catching a re-add under a new name), ADR-0087 registration complete (semantic entry + spec-changes.json both projections), loud BREAKING changeset with a FROM→TO table, and the honest record of what the replacement drops (a thrown 4xx is now counted by nothing). The cli seat's independent re-run of the ruling's escape-clause sweep (zero live declarations/emissions at head) stands.

    The one flagged judgment, decided here as the review's remit: ADR-0087 registration at protocol 17 is endorsed — it follows the ups-delegated-from-column-retired (#9998) precedent exactly (same ADR-0049 shape, same placement, maintainer-ruled), and the reasoning holds: the removal takes effect on the 17.x line when it ships; step 18 collects boundary-deferred changes. No escalation needed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions