@objectstack/observability 17.0.0 → 17.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,321 @@
1
1
  # @objectstack/observability
2
2
 
3
+ ## 17.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 914c413: fix(observability): **BREAKING** — `http_request_errors_total` is retired (ADR-0049 enforce-or-remove, #9834)
8
+
9
+ **⛔ If you have a Grafana panel, an alert rule or a recording rule keyed on
10
+ `http_request_errors_total`, it will read a FLAT ZERO after this upgrade.** That
11
+ zero is the removal, not a healthy server, and it is the one way this change can
12
+ hurt you — nothing throws, nothing warns, the series simply stops receiving
13
+ samples. Rewrite the query before you deploy.
14
+
15
+ Maintainer ruling 2026-08-20: **RETIRE**. The name was declared in `SEMCONV` as
16
+ part of a stable namespace *"so hosts can wire alerts/dashboards against it"*,
17
+ but the only emitter was `@objectstack/runtime`'s `instrumentRouteHandler`,
18
+ applied only by the dispatcher's own route Proxy — so the series never saw
19
+ auth's `getRawApp()` mount, the REST data API via `RouteManager`, or any other
20
+ inbound surface. Its two siblings in the same HTTP family moved to the
21
+ `IHttpServer.afterResponse` transport seam (`http_requests_total`, #9650/#9835;
22
+ `http_request_duration_ms`, #9834/#10004) and this one could not follow:
23
+ `HttpResponseObservation` carries `{method, routePattern, status, elapsedMs}`
24
+ and **no throw signal of any kind**, so every transport-side shape would have
25
+ counted a *different* population rather than the same one more widely.
26
+
27
+ Migration (FROM → TO):
28
+
29
+ | Wrote | Write instead |
30
+ |---|---|
31
+ | `rate(http_request_errors_total[5m])` in a panel or alert | `rate(http_requests_total{status=~"5.."}[5m])` — emitted by the transport, so it covers every inbound surface instead of the dispatcher's routes only |
32
+ | `sum by (route) (http_request_errors_total)` | `sum by (route) (http_requests_total{status=~"5.."})` |
33
+ | `SEMCONV.httpRequestErrorsTotal` / `RUNTIME_METRICS.httpRequestErrorsTotal` in host code | Delete the read. Both members are gone; `tsc` reports the missing property at the read site. |
34
+
35
+ One-line fix: replace the metric name with `http_requests_total{status=~"5.."}`.
36
+
37
+ <!-- adr-0087: registered http-request-errors-total-retired -->
38
+
39
+ **The replacement is wider, not merely different.** The retired counter was
40
+ divergent from a 5xx rate in *both* directions, measured: the dispatcher answers
41
+ its own errors through `errorResponseBase`, which sets a status and does **not**
42
+ re-throw — so the counter **missed** those — while its `catch` incremented
43
+ unconditionally, so a **thrown 4xx WAS counted** as an error. And
44
+ `http_requests_total` already carries a `status` label, so a status-class error
45
+ counter was fully derivable from data the transport already publishes. Prove the
46
+ new query wider rather than merely non-empty: make an auth route or a REST
47
+ data-API route answer 5xx and confirm it moves, where the retired counter would
48
+ not have moved at all.
49
+
50
+ **If what you were actually alerting on was "a handler threw rather than
51
+ returning an error envelope"** — the one signal this counter uniquely carried —
52
+ that is the `errorReporter`, not a metric. Wire an `ErrorReporter` adapter
53
+ (Sentry / Datadog / your own); it still fires on every 5xx throw and is
54
+ untouched by this change.
55
+
56
+ What is NOT removed: `http_requests_total`, `http_request_duration_ms`,
57
+ request-id propagation, the 5xx error reporter, and the
58
+ `res.__obsRecordedError` side channel that carries a swallowed error to it. The
59
+ dispatcher still instruments every route it mounts; it just no longer publishes
60
+ a fourth series whose name promised more coverage than it had.
61
+
62
+ ### Patch Changes
63
+
64
+ - Updated dependencies [6936d07]
65
+ - Updated dependencies [59eb04d]
66
+ - Updated dependencies [9f05b7d]
67
+ - Updated dependencies [7d2d112]
68
+ - Updated dependencies [5fa0d72]
69
+ - Updated dependencies [02b3b07]
70
+ - Updated dependencies [914c413]
71
+ - Updated dependencies [55809a0]
72
+ - Updated dependencies [52db1d1]
73
+ - Updated dependencies [5649efb]
74
+ - Updated dependencies [2306a76]
75
+ - Updated dependencies [e5ea701]
76
+ - Updated dependencies [a40dcc1]
77
+ - Updated dependencies [def0d3e]
78
+ - Updated dependencies [8d0bb79]
79
+ - Updated dependencies [5acb58d]
80
+ - Updated dependencies [2e3cf95]
81
+ - Updated dependencies [4c93387]
82
+ - Updated dependencies [a037f7c]
83
+ - Updated dependencies [3ee8ddf]
84
+ - Updated dependencies [16cef97]
85
+ - Updated dependencies [a79bd35]
86
+ - Updated dependencies [6ceaa4b]
87
+ - Updated dependencies [15ea214]
88
+ - Updated dependencies [de19489]
89
+ - Updated dependencies [c684d00]
90
+ - Updated dependencies [923c424]
91
+ - Updated dependencies [1ec36b7]
92
+ - Updated dependencies [5f2e54c]
93
+ - Updated dependencies [189373b]
94
+ - Updated dependencies [35ad101]
95
+ - Updated dependencies [ceb33a9]
96
+ - Updated dependencies [73d9795]
97
+ - Updated dependencies [8012960]
98
+ - Updated dependencies [f34f56b]
99
+ - Updated dependencies [f399618]
100
+ - Updated dependencies [75e9301]
101
+ - Updated dependencies [2810695]
102
+ - @objectstack/spec@17.2.0
103
+
104
+ ## 17.1.0
105
+
106
+ ### Patch Changes
107
+
108
+ - 899052a: docs(observability): record what a zero on `cache_*` MEANS — "no configured consumer", not "no cache activity" (#9954)
109
+
110
+ `SEMCONV` declares the `cache_*` families as a stable namespace explicitly so
111
+ hosts can wire alerts and dashboards against it. An operator who does that gets
112
+ a flat zero on `cache_lookups_total` in a default install — and nothing in the
113
+ declaration or the operator docs said why, so the only available readings were
114
+ "0% hit rate" or "the adapter is broken". Both are wrong.
115
+
116
+ **Nothing about emission changes.** The path is proven working: #9832 wired the
117
+ cache adapter to the host's registry and #9951 pins a real lookup observing
118
+ `cache_lookups_total{adapter=memory,result=miss}`. The zero is *true*; what it
119
+ failed to communicate is its cause.
120
+
121
+ The cause, re-measured on `origin/main` rather than taken from the card: no
122
+ consumer of the `cache` service is unconditional. Every production consumer is
123
+ a rate-limit or budget counter store, and each is gated on a declaration
124
+ somebody has to write:
125
+
126
+ - `packages/plugins/plugin-auth/src/auth-plugin.ts` — better-auth's per-IP
127
+ counters, reached only when `rate_limit_max` or `rate_limit_window_seconds`
128
+ is explicitly supplied in auth settings.
129
+ - `packages/runtime/src/dispatcher-plugin.ts` — the inbound rate limiter and
130
+ the declarative per-endpoint buckets. Both register *nothing at all* when no
131
+ budget is declared (`createInboundRateLimitMiddleware` returns `null`;
132
+ `limiterFor` returns `null` on an endpoint with no armed `rateLimit`), so an
133
+ unmetered deployment never reaches the cache.
134
+ - the per-number OTP send budget, reached only on an SMS send path.
135
+
136
+ Declare none of them — the default slate — and the family sits at 0 while the
137
+ server handles traffic normally.
138
+
139
+ So the annotation states the invariant rather than a roster: *every* consumer
140
+ is an explicitly-declared rate-limit/budget counter store. That sentence stays
141
+ true when another conditional consumer is added, and goes false exactly when an
142
+ unconditional one appears — which is when it should be revisited.
143
+
144
+ This is the mirror image of the HTTP note that landed alongside it: there a
145
+ zero means "not instrumented"; here a zero is a true count of a service nothing
146
+ asked anything of. The two are deliberately worded so they cannot be read as
147
+ the same statement.
148
+
149
+ The note is attached as per-family JSDoc rather than a `//` group header, and
150
+ that placement is load-bearing: `tsup`'s dts generation drops line comments, so
151
+ only the JSDoc form reaches `dist/index.d.ts` — i.e. the operator's IDE hover,
152
+ which is where somebody wiring a `cache_lookups_total` panel actually meets the
153
+ declaration. Verified in the built artifact, not assumed.
154
+ - 1e050a5: fix(observability): emit `http_request_duration_ms` from the transport seam, so
155
+ p95 latency sees every inbound surface instead of dispatcher routes only
156
+ (#9834)
157
+
158
+ #9835 moved `http_requests_total` to the `IHttpServer.afterResponse` seam and
159
+ stopped there, which left the two derived signals the operator guidance names
160
+ inconsistent with each other: 5xx rate covered auth's `getRawApp()` mount and
161
+ the REST data API, while p95 latency still saw only the routes the dispatcher's
162
+ own `Proxy` wrapped. A missing latency panel is at least loud; the worse
163
+ reading is the p95 that IS drawn, computed from dispatcher routes only and
164
+ presented as the server's.
165
+
166
+ - `@objectstack/observability`: new `armHttpRequestDurationHistogram(server,
167
+ metrics)`, the duration family's counterpart to `armHttpRequestCounter` —
168
+ same `afterResponse` seam, its own `Symbol.for` first-wins latch, so a host
169
+ that armed one family can still arm the other. `ArmHttpMetricResult` is the
170
+ family-neutral spelling of the result union; `ArmHttpRequestCounterResult`
171
+ stays as an alias.
172
+ - `@objectstack/plugin-hono-server`: `installHttpMetricsSeam` arms both
173
+ families, so the transport owns every transport-observable HTTP metric in the
174
+ shipped composition rather than splitting ownership across layers.
175
+ - `@objectstack/runtime`: the dispatcher offers its registry to the histogram
176
+ seam as well, and `instrumentRouteHandler` gains
177
+ `emitHttpRequestDurationMs` (default `true`) — passed `false` exactly when
178
+ the transport implements the seam, which is what keeps the dispatcher's own
179
+ routes at ONE observation instead of reintroducing the #9833 double count one
180
+ family over.
181
+
182
+ **⚠️ The observation window changes with the emitter.** The per-route wrapper
183
+ timed `await handler(req, res)` — handler latency. The transport times from
184
+ first seeing the request to the response existing, so the middleware chain and
185
+ body parse are now included and samples can only move UP. This is the number a
186
+ latency panel should show (the request's latency, not one layer's share of it),
187
+ but it is a visible shift in an existing series: compare p95 across the upgrade
188
+ boundary deliberately. Documented in `docs/OBSERVABILITY.md` and the
189
+ production-readiness guide.
190
+
191
+ `http_request_errors_total` is deliberately NOT moved. The observation carries
192
+ no throw signal — only `{method, routePattern, status, elapsedMs}` — so a
193
+ transport-side emitter would have to key off a status class, which counts a
194
+ different population than "the handler threw". That is a semantics decision,
195
+ recorded on #9834 rather than guessed at here; the counter stays ungated on
196
+ every transport and keeps its documented meaning.
197
+ - 7ff3975: feat(spec): `IHttpServer` gains an optional `afterResponse` response-observing
198
+ hook so HTTP metrics are transport-agnostic instead of Hono-only (#9835)
199
+
200
+ The contract addition (additive — a new optional member plus the
201
+ `HttpResponseObservation` / `HttpResponseObserver` types and the reserved
202
+ `UNMATCHED_ROUTE_PATTERN` label): a transport invokes each registered observer
203
+ exactly once per answered request with `{ method, routePattern, status,
204
+ elapsedMs }`, after the response exists — the observation point the `use()`
205
+ middleware contract cannot express (it runs before dispatch and never sees a
206
+ status). `routePattern` is REQUIRED to be the registered route pattern
207
+ (`/api/v1/data/:id`), never the concrete path, so no adapter re-decides metric
208
+ cardinality. Optionality is feature-detected runtime-real
209
+ (`typeof server.afterResponse === 'function'`); a transport that does not
210
+ implement the seam reports **no** HTTP metrics — zero there means "not
211
+ instrumented", never "no traffic".
212
+
213
+ Implementations and consumers in the same change:
214
+
215
+ - `@objectstack/plugin-hono-server`: `HonoHttpServer` implements the seam (the
216
+ ruled #9650 raw-app middleware becomes its delivery path — same reach,
217
+ including `getRawApp()` mounts and middleware-refused 429s); unrouted
218
+ requests are now labelled with the reserved `unmatched` pattern (previously
219
+ they could surface as `/*`).
220
+ - `@objectstack/observability`: new `armHttpRequestCounter(server, metrics)`
221
+ arms the `http_requests_total` counter through the seam at most once per
222
+ server (first caller wins), which is what makes "exactly one counter per
223
+ server" structural.
224
+ - `@objectstack/runtime`: the dispatcher offers its `observability.metrics`
225
+ registry to the seam (a host that wires only the dispatcher now counts every
226
+ inbound surface) and suppresses its own per-route copy of
227
+ `http_requests_total` when the transport implements the seam — retiring the
228
+ #9833 double count. Request-id echo, the duration histogram, the error
229
+ counter and the error reporter are unchanged.
230
+ - `@objectstack/http-conformance`: `NodeHttpServer` implements the seam, and a
231
+ new cross-adapter conformance suite locks the semantics for both adapters.
232
+ - `@objectstack/core`: re-exports the new contract types/constant.
233
+ - Updated dependencies [56656aa]
234
+ - Updated dependencies [07e630e]
235
+ - Updated dependencies [2f65b1b]
236
+ - Updated dependencies [720ee95]
237
+ - Updated dependencies [f287435]
238
+ - Updated dependencies [9aa8890]
239
+ - Updated dependencies [7c9c1dd]
240
+ - Updated dependencies [75b7c24]
241
+ - Updated dependencies [d5552ca]
242
+ - Updated dependencies [d9813a9]
243
+ - Updated dependencies [8640fb2]
244
+ - Updated dependencies [2420641]
245
+ - Updated dependencies [2ad91c3]
246
+ - Updated dependencies [f57fb38]
247
+ - Updated dependencies [00777a0]
248
+ - Updated dependencies [d491625]
249
+ - Updated dependencies [420804d]
250
+ - Updated dependencies [716ac9b]
251
+ - Updated dependencies [62b1427]
252
+ - Updated dependencies [7ea1372]
253
+ - Updated dependencies [23abe27]
254
+ - Updated dependencies [985a9cd]
255
+ - Updated dependencies [a8189ae]
256
+ - Updated dependencies [26e70fb]
257
+ - Updated dependencies [42b05af]
258
+ - Updated dependencies [2b292ce]
259
+ - Updated dependencies [abcf853]
260
+ - Updated dependencies [8b9eba5]
261
+ - Updated dependencies [d575779]
262
+ - Updated dependencies [94f7ef8]
263
+ - Updated dependencies [c5ac5e4]
264
+ - Updated dependencies [a777944]
265
+ - Updated dependencies [dd88e1c]
266
+ - Updated dependencies [856527c]
267
+ - Updated dependencies [870f710]
268
+ - Updated dependencies [79c46da]
269
+ - Updated dependencies [7ff3975]
270
+ - Updated dependencies [29d055b]
271
+ - Updated dependencies [65589d6]
272
+ - Updated dependencies [2c86fe3]
273
+ - Updated dependencies [e196c6a]
274
+ - Updated dependencies [4ab7523]
275
+ - Updated dependencies [19539b4]
276
+ - Updated dependencies [11b779e]
277
+ - Updated dependencies [739fe5b]
278
+ - Updated dependencies [4bfe1a5]
279
+ - Updated dependencies [2065e31]
280
+ - Updated dependencies [b69d0f5]
281
+ - Updated dependencies [4d47afe]
282
+ - Updated dependencies [e4e5c6e]
283
+ - Updated dependencies [9a56784]
284
+ - Updated dependencies [d00d2f6]
285
+ - Updated dependencies [df0c12d]
286
+ - Updated dependencies [d31785f]
287
+ - Updated dependencies [c308a4f]
288
+ - Updated dependencies [e2899f6]
289
+ - Updated dependencies [3851f87]
290
+ - Updated dependencies [2a29caa]
291
+ - Updated dependencies [09a6eee]
292
+ - Updated dependencies [1a7f907]
293
+ - Updated dependencies [cd455c8]
294
+ - Updated dependencies [30d3752]
295
+ - Updated dependencies [c80e7ae]
296
+ - Updated dependencies [09a9a8a]
297
+ - Updated dependencies [07026cf]
298
+ - Updated dependencies [5d4f3d5]
299
+ - Updated dependencies [4d80e8b]
300
+ - Updated dependencies [30b1c63]
301
+ - Updated dependencies [079b457]
302
+ - Updated dependencies [e43b211]
303
+ - Updated dependencies [890b38f]
304
+ - Updated dependencies [8bee54b]
305
+ - Updated dependencies [7a537ce]
306
+ - Updated dependencies [593c4bf]
307
+ - Updated dependencies [ff08691]
308
+ - Updated dependencies [60e0f90]
309
+ - Updated dependencies [90c5285]
310
+ - Updated dependencies [7901b2d]
311
+ - Updated dependencies [56bca91]
312
+ - Updated dependencies [79394d7]
313
+ - Updated dependencies [730fd9a]
314
+ - Updated dependencies [44bc51d]
315
+ - Updated dependencies [73cfddf]
316
+ - Updated dependencies [d634e66]
317
+ - @objectstack/spec@17.1.0
318
+
3
319
  ## 17.0.0
4
320
 
5
321
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -37,6 +37,8 @@ __export(index_exports, {
37
37
  RUNTIME_METRICS: () => RUNTIME_METRICS,
38
38
  SEMCONV: () => SEMCONV,
39
39
  allowPerfDisclosure: () => allowPerfDisclosure,
40
+ armHttpRequestCounter: () => armHttpRequestCounter,
41
+ armHttpRequestDurationHistogram: () => armHttpRequestDurationHistogram,
40
42
  countServerTiming: () => countServerTiming,
41
43
  currentPerfTiming: () => currentPerfTiming,
42
44
  formatServerTiming: () => formatServerTiming,
@@ -59,16 +61,35 @@ var OBSERVABILITY_ERRORS_SERVICE = "observability:errors";
59
61
 
60
62
  // src/semconv.ts
61
63
  var SEMCONV = {
62
- // ── HTTP — emitted by `@objectstack/runtime`'s instrumentRouteHandler ──
63
- /** Counter, labels: `method`, `route`, `status`. */
64
+ // ── HTTP — emitter differs per family, see each ────────────────────
65
+ /**
66
+ * Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT
67
+ * through the `IHttpServer.afterResponse` seam (#9835), so it covers
68
+ * every inbound request on the server rather than only the routes the
69
+ * runtime dispatcher registers.
70
+ */
64
71
  httpRequestsTotal: "http_requests_total",
65
- /** Histogram (ms), labels: `method`, `route`. */
66
- httpRequestDurationMs: "http_request_duration_ms",
67
72
  /**
68
- * Counter, labels: `method`, `route`. Incremented when an
69
- * in-flight handler throws after the response is sent.
73
+ * Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT
74
+ * through the same seam (#9834). It measures the REQUEST as the transport
75
+ * sees it — first sight to the response existing, middleware chain and
76
+ * body parse included — not the handler's share of it.
70
77
  */
71
- httpRequestErrorsTotal: "http_request_errors_total",
78
+ httpRequestDurationMs: "http_request_duration_ms",
79
+ // ⛔ RETIRED — `http_request_errors_total` was removed in
80
+ // `@objectstack/observability` 17.2.0 (#9834, ADR-0049 enforce-or-remove).
81
+ // ⛔ Do not re-add the name. It was DECLARED here as a stable server-wide
82
+ // signal and EMITTED only from `@objectstack/runtime`'s per-route wrapper,
83
+ // on a THROWN handler — so it never saw auth's `getRawApp()` mount, the
84
+ // REST data API, or any error a handler answered politely through
85
+ // `errorResponseBase` (which sets a status and does not re-throw). No
86
+ // transport-side emitter could preserve that population either: the
87
+ // `IHttpServer.afterResponse` observation carries `{method, routePattern,
88
+ // status, elapsedMs}` and no throw signal at all.
89
+ // ⇒ Read the 5xx rate from `http_requests_total{status=~"5.."}` instead.
90
+ // The transport emits that family through the seam, so it covers every
91
+ // inbound surface (#9650 / #9835 / #10004) and carries the status label
92
+ // this counter only stood in for. Maintainer ruling 2026-08-20.
72
93
  // ── Storage — emitted by `@objectstack/service-storage` adapters ──
73
94
  /** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
74
95
  storageOperationsTotal: "storage_operations_total",
@@ -77,11 +98,41 @@ var SEMCONV = {
77
98
  /** Counter, labels: `adapter`, `op`, `errorClass`. */
78
99
  storageErrorsTotal: "storage_errors_total",
79
100
  // ── Cache — emitted by `@objectstack/service-cache` adapters ──
80
- /** Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`). */
101
+ // Uniform emitter; what varies is who CONSULTS the service — see below.
102
+ /**
103
+ * Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).
104
+ *
105
+ * ⚠️ A flat zero means "NO CONFIGURED CONSUMER", not "no cache activity",
106
+ * and — unlike the HTTP families above — it is NOT an instrumentation gap.
107
+ * The adapters hold the host's registry and count every call they receive
108
+ * (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails
109
+ * to communicate is WHY.
110
+ *
111
+ * The why: nothing consults the `cache` service unconditionally. Every
112
+ * production consumer is a rate-limit / budget counter store, and each is
113
+ * gated on a declaration somebody has to write — better-auth's per-IP
114
+ * counters (`rate_limit_max` / `rate_limit_window_seconds` in auth
115
+ * settings), the dispatcher's inbound limiter and its declarative
116
+ * per-endpoint buckets (an armed `rateLimit` budget; with none declared
117
+ * the dispatcher registers no limiter at all), and the per-number OTP send
118
+ * budget (an SMS send path). A default install declares none of them, so
119
+ * this counter stays at 0 while the server handles traffic normally.
120
+ *
121
+ * ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%
122
+ * hit rate or a broken adapter. Before trusting a cache hit-rate panel,
123
+ * confirm at least one consumer above is actually armed.
124
+ */
81
125
  cacheLookupsTotal: "cache_lookups_total",
82
- /** Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). */
126
+ /**
127
+ * Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same
128
+ * "zero = no configured consumer" reading as `cacheLookupsTotal` above.
129
+ */
83
130
  cacheWritesTotal: "cache_writes_total",
84
- /** Counter, labels: `adapter`, `op`, `errorClass`. */
131
+ /**
132
+ * Counter, labels: `adapter`, `op`, `errorClass`. Same "zero = no
133
+ * configured consumer" reading as `cacheLookupsTotal` above — a zero is
134
+ * "nothing was asked of the cache", not "every call succeeded".
135
+ */
85
136
  cacheErrorsTotal: "cache_errors_total",
86
137
  // ── Background jobs — emitted by `@objectstack/runtime`'s AppPlugin ──
87
138
  /**
@@ -101,10 +152,49 @@ var SEMCONV = {
101
152
  };
102
153
  var RUNTIME_METRICS = {
103
154
  httpRequestsTotal: SEMCONV.httpRequestsTotal,
104
- httpRequestDurationMs: SEMCONV.httpRequestDurationMs,
105
- httpRequestErrorsTotal: SEMCONV.httpRequestErrorsTotal
155
+ httpRequestDurationMs: SEMCONV.httpRequestDurationMs
156
+ // `httpRequestErrorsTotal` retired with its SEMCONV declaration above
157
+ // (#9834). The alias is not a compatibility window of its own: there is no
158
+ // emitter left to read, so keeping the name here would hand callers a
159
+ // string nothing ever writes.
106
160
  };
107
161
 
162
+ // src/http-transport-metrics.ts
163
+ var HTTP_REQUEST_COUNTER_ARMED = /* @__PURE__ */ Symbol.for(
164
+ "objectstack.observability.httpRequestCounterArmed"
165
+ );
166
+ function armHttpRequestCounter(server, metrics) {
167
+ if (typeof server.afterResponse !== "function") return "unsupported";
168
+ const latched = server;
169
+ if (latched[HTTP_REQUEST_COUNTER_ARMED]) return "already-armed";
170
+ latched[HTTP_REQUEST_COUNTER_ARMED] = true;
171
+ server.afterResponse((observation) => {
172
+ metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {
173
+ method: observation.method,
174
+ route: observation.routePattern,
175
+ status: String(observation.status)
176
+ });
177
+ });
178
+ return "armed";
179
+ }
180
+ var HTTP_REQUEST_DURATION_ARMED = /* @__PURE__ */ Symbol.for(
181
+ "objectstack.observability.httpRequestDurationArmed"
182
+ );
183
+ function armHttpRequestDurationHistogram(server, metrics) {
184
+ if (typeof server.afterResponse !== "function") return "unsupported";
185
+ const latched = server;
186
+ if (latched[HTTP_REQUEST_DURATION_ARMED]) return "already-armed";
187
+ latched[HTTP_REQUEST_DURATION_ARMED] = true;
188
+ server.afterResponse((observation) => {
189
+ metrics.histogram(
190
+ RUNTIME_METRICS.httpRequestDurationMs,
191
+ observation.elapsedMs,
192
+ { method: observation.method, route: observation.routePattern }
193
+ );
194
+ });
195
+ return "armed";
196
+ }
197
+
108
198
  // src/metrics-exporters.ts
109
199
  var NoopMetricsRegistry = class {
110
200
  counter() {
@@ -704,6 +794,8 @@ function isPerfDisclosurePrincipal(ec) {
704
794
  RUNTIME_METRICS,
705
795
  SEMCONV,
706
796
  allowPerfDisclosure,
797
+ armHttpRequestCounter,
798
+ armHttpRequestDurationHistogram,
707
799
  countServerTiming,
708
800
  currentPerfTiming,
709
801
  formatServerTiming,