@objectstack/observability 17.0.0-rc.6 → 17.1.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 +1045 -0
- package/dist/index.cjs +92 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +147 -9
- package/dist/index.d.ts +147 -9
- package/dist/index.js +90 -8
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger } from '@objectstack/spec/contracts';
|
|
1
|
+
import { IHttpServer, Logger } from '@objectstack/spec/contracts';
|
|
2
2
|
export { Logger } from '@objectstack/spec/contracts';
|
|
3
3
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
4
4
|
|
|
@@ -118,13 +118,27 @@ declare const OBSERVABILITY_ERRORS_SERVICE = "observability:errors";
|
|
|
118
118
|
* stay in the deployment repo.
|
|
119
119
|
*/
|
|
120
120
|
declare const SEMCONV: {
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT
|
|
123
|
+
* through the `IHttpServer.afterResponse` seam (#9835), so it covers
|
|
124
|
+
* every inbound request on the server rather than only the routes the
|
|
125
|
+
* runtime dispatcher registers.
|
|
126
|
+
*/
|
|
122
127
|
readonly httpRequestsTotal: "http_requests_total";
|
|
123
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT
|
|
130
|
+
* through the same seam (#9834). It measures the REQUEST as the transport
|
|
131
|
+
* sees it — first sight to the response existing, middleware chain and
|
|
132
|
+
* body parse included — not the handler's share of it.
|
|
133
|
+
*/
|
|
124
134
|
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
125
135
|
/**
|
|
126
|
-
* Counter, labels: `method`, `route`. Incremented when an
|
|
127
|
-
*
|
|
136
|
+
* Counter, labels: `method`, `route`. Incremented when an in-flight
|
|
137
|
+
* handler throws after the response is sent. Emitted by
|
|
138
|
+
* `@objectstack/runtime`'s `instrumentRouteHandler`, and NOT movable to
|
|
139
|
+
* the seam above as-is: the observation carries a status but no throw
|
|
140
|
+
* signal, so a transport-side emitter would count a different population
|
|
141
|
+
* (#9834 records the fork).
|
|
128
142
|
*/
|
|
129
143
|
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
130
144
|
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
@@ -133,11 +147,40 @@ declare const SEMCONV: {
|
|
|
133
147
|
readonly storageOperationDurationMs: "storage_operation_duration_ms";
|
|
134
148
|
/** Counter, labels: `adapter`, `op`, `errorClass`. */
|
|
135
149
|
readonly storageErrorsTotal: "storage_errors_total";
|
|
136
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ A flat zero means "NO CONFIGURED CONSUMER", not "no cache activity",
|
|
154
|
+
* and — unlike the HTTP families above — it is NOT an instrumentation gap.
|
|
155
|
+
* The adapters hold the host's registry and count every call they receive
|
|
156
|
+
* (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails
|
|
157
|
+
* to communicate is WHY.
|
|
158
|
+
*
|
|
159
|
+
* The why: nothing consults the `cache` service unconditionally. Every
|
|
160
|
+
* production consumer is a rate-limit / budget counter store, and each is
|
|
161
|
+
* gated on a declaration somebody has to write — better-auth's per-IP
|
|
162
|
+
* counters (`rate_limit_max` / `rate_limit_window_seconds` in auth
|
|
163
|
+
* settings), the dispatcher's inbound limiter and its declarative
|
|
164
|
+
* per-endpoint buckets (an armed `rateLimit` budget; with none declared
|
|
165
|
+
* the dispatcher registers no limiter at all), and the per-number OTP send
|
|
166
|
+
* budget (an SMS send path). A default install declares none of them, so
|
|
167
|
+
* this counter stays at 0 while the server handles traffic normally.
|
|
168
|
+
*
|
|
169
|
+
* ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%
|
|
170
|
+
* hit rate or a broken adapter. Before trusting a cache hit-rate panel,
|
|
171
|
+
* confirm at least one consumer above is actually armed.
|
|
172
|
+
*/
|
|
137
173
|
readonly cacheLookupsTotal: "cache_lookups_total";
|
|
138
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same
|
|
176
|
+
* "zero = no configured consumer" reading as `cacheLookupsTotal` above.
|
|
177
|
+
*/
|
|
139
178
|
readonly cacheWritesTotal: "cache_writes_total";
|
|
140
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Counter, labels: `adapter`, `op`, `errorClass`. Same "zero = no
|
|
181
|
+
* configured consumer" reading as `cacheLookupsTotal` above — a zero is
|
|
182
|
+
* "nothing was asked of the cache", not "every call succeeded".
|
|
183
|
+
*/
|
|
141
184
|
readonly cacheErrorsTotal: "cache_errors_total";
|
|
142
185
|
/**
|
|
143
186
|
* Counter, labels: `app`, `job`. Incremented when a DECLARED background
|
|
@@ -165,6 +208,101 @@ declare const RUNTIME_METRICS: {
|
|
|
165
208
|
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
166
209
|
};
|
|
167
210
|
|
|
211
|
+
/**
|
|
212
|
+
* What an `arm*` call in this module did:
|
|
213
|
+
*
|
|
214
|
+
* - `'armed'` — the emitting observer was registered on this call.
|
|
215
|
+
* - `'already-armed'` — some earlier caller already armed this server for
|
|
216
|
+
* THIS metric family; the seam emits it, and this call registered nothing
|
|
217
|
+
* (first-wins).
|
|
218
|
+
* - `'unsupported'` — the transport does not implement the
|
|
219
|
+
* `IHttpServer.afterResponse` seam; it reports NO HTTP metrics (#9835:
|
|
220
|
+
* zero there means "not instrumented", never "no traffic"), and the
|
|
221
|
+
* caller must decide how to degrade — the runtime dispatcher falls back
|
|
222
|
+
* to instrumenting its own routes.
|
|
223
|
+
*/
|
|
224
|
+
type ArmHttpMetricResult = 'armed' | 'already-armed' | 'unsupported';
|
|
225
|
+
/**
|
|
226
|
+
* The name {@link armHttpRequestCounter} shipped with. Kept as an alias
|
|
227
|
+
* rather than renamed: the export is already in the pending release, and the
|
|
228
|
+
* two families arm through separate entry points anyway.
|
|
229
|
+
*/
|
|
230
|
+
type ArmHttpRequestCounterResult = ArmHttpMetricResult;
|
|
231
|
+
/**
|
|
232
|
+
* Arm `http_requests_total{method,route,status}` on a transport through the
|
|
233
|
+
* `IHttpServer.afterResponse` observation seam (#9835) — AT MOST ONCE per
|
|
234
|
+
* server, whoever calls first.
|
|
235
|
+
*
|
|
236
|
+
* ## Why arming is centralized here
|
|
237
|
+
*
|
|
238
|
+
* The contract's ownership rule says a request must never be double-counted,
|
|
239
|
+
* and two composition layers legitimately hold both a server and a metrics
|
|
240
|
+
* registry: the transport's own hosting plugin (`HonoServerPlugin`, which
|
|
241
|
+
* the 2026-08-18 ruling on #9650 made the counter's home) and the runtime
|
|
242
|
+
* dispatcher (whose `observability.metrics` config is the wiring the docs
|
|
243
|
+
* demonstrate). When a host hands ONE registry to both — the ordinary case —
|
|
244
|
+
* two independently-registered observers would land every request on the
|
|
245
|
+
* same series twice: exactly the #9833 distortion, rebuilt one seam over.
|
|
246
|
+
* Routing every arming through this function makes "exactly one
|
|
247
|
+
* counter-emitting observer per server" structural: the first caller arms
|
|
248
|
+
* (in the shipped composition that is the transport plugin, in Phase 1),
|
|
249
|
+
* every later caller is told the seam already counts.
|
|
250
|
+
*
|
|
251
|
+
* The label shape is pinned by the contract: `route` is the transport's
|
|
252
|
+
* `routePattern` — the registered PATTERN, never the concrete path — and
|
|
253
|
+
* `status` is stringified for the label set. Emission goes through the
|
|
254
|
+
* transport's observer-isolation guarantee, so a throwing registry cannot
|
|
255
|
+
* break a response.
|
|
256
|
+
*
|
|
257
|
+
* @param server - The transport. Pass the RAW registered `http.server`
|
|
258
|
+
* instance, not a wrapper: the latch is per object identity, and a wrapper
|
|
259
|
+
* would both fork the latch and (per #5122) risk erasing the optional
|
|
260
|
+
* member this function feature-detects.
|
|
261
|
+
* @param metrics - The registry the counter lands in. First caller wins; a
|
|
262
|
+
* second registry offered later is NOT added (the contract's one-owner
|
|
263
|
+
* rule), and the result says so.
|
|
264
|
+
*/
|
|
265
|
+
declare function armHttpRequestCounter(server: IHttpServer, metrics: MetricsRegistry): ArmHttpMetricResult;
|
|
266
|
+
/**
|
|
267
|
+
* Arm `http_request_duration_ms{method,route}` on a transport through the
|
|
268
|
+
* `IHttpServer.afterResponse` observation seam — AT MOST ONCE per server,
|
|
269
|
+
* whoever calls first. The duration half of #9834, built on the mechanism
|
|
270
|
+
* #9835 proved out for `http_requests_total`.
|
|
271
|
+
*
|
|
272
|
+
* ## Why the histogram has to move too
|
|
273
|
+
*
|
|
274
|
+
* #9835 moved only the counter, which left the docs' two derived signals
|
|
275
|
+
* inconsistent with each other: 5xx rate saw every inbound surface while p95
|
|
276
|
+
* latency still saw the dispatcher's own routes. An operator reading one
|
|
277
|
+
* dashboard got request volume for `/api/v1/*` beside a latency panel with no
|
|
278
|
+
* series for it — and the worse reading is the p95 that IS drawn, computed
|
|
279
|
+
* from dispatcher routes only and presented as the server's.
|
|
280
|
+
*
|
|
281
|
+
* ## ⚠️ The observation WINDOW changes with the emitter
|
|
282
|
+
*
|
|
283
|
+
* The dispatcher's per-route wrapper timed `await handler(req, res)` — handler
|
|
284
|
+
* latency. This seam times the transport's own `use('*')` around
|
|
285
|
+
* `await next()`, which is what {@link HttpResponseObservation.elapsedMs}
|
|
286
|
+
* means: "from the transport first seeing the request to the response
|
|
287
|
+
* existing". That includes the middleware chain and body parse, so the series
|
|
288
|
+
* can only move UP, never down. It is the number an operator's latency panel
|
|
289
|
+
* should have been showing — the request's latency rather than one layer's
|
|
290
|
+
* share of it — but it is a visible change in an existing series, so it is
|
|
291
|
+
* stated here, in the changeset, and in `docs/OBSERVABILITY.md` rather than
|
|
292
|
+
* left for a dashboard to discover.
|
|
293
|
+
*
|
|
294
|
+
* The label set is unchanged and stays the SEMCONV-declared `{method,route}`:
|
|
295
|
+
* `route` is the transport's `routePattern` (the registered PATTERN, never the
|
|
296
|
+
* concrete path), and no `status` label is added — a histogram split by status
|
|
297
|
+
* is a different series shape than the one the docs tell operators to graph.
|
|
298
|
+
*
|
|
299
|
+
* @param server - The transport. Pass the RAW registered `http.server`
|
|
300
|
+
* instance, not a wrapper — the latch is per object identity, and (per
|
|
301
|
+
* #5122) a wrapper risks erasing the optional member this feature-detects.
|
|
302
|
+
* @param metrics - The registry the histogram lands in. First caller wins.
|
|
303
|
+
*/
|
|
304
|
+
declare function armHttpRequestDurationHistogram(server: IHttpServer, metrics: MetricsRegistry): ArmHttpMetricResult;
|
|
305
|
+
|
|
168
306
|
/**
|
|
169
307
|
* No-op metrics registry — the default. Discards every observation.
|
|
170
308
|
* Production deployments should swap this for a real registry; tests
|
|
@@ -617,4 +755,4 @@ declare function isPerfDisclosurePrivileged(): boolean;
|
|
|
617
755
|
*/
|
|
618
756
|
declare function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean;
|
|
619
757
|
|
|
620
|
-
export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrincipal, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
|
|
758
|
+
export { type ArmHttpMetricResult, type ArmHttpRequestCounterResult, type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, armHttpRequestCounter, armHttpRequestDurationHistogram, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrincipal, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Logger } from '@objectstack/spec/contracts';
|
|
1
|
+
import { IHttpServer, Logger } from '@objectstack/spec/contracts';
|
|
2
2
|
export { Logger } from '@objectstack/spec/contracts';
|
|
3
3
|
import { ExecutionContext } from '@objectstack/spec/kernel';
|
|
4
4
|
|
|
@@ -118,13 +118,27 @@ declare const OBSERVABILITY_ERRORS_SERVICE = "observability:errors";
|
|
|
118
118
|
* stay in the deployment repo.
|
|
119
119
|
*/
|
|
120
120
|
declare const SEMCONV: {
|
|
121
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT
|
|
123
|
+
* through the `IHttpServer.afterResponse` seam (#9835), so it covers
|
|
124
|
+
* every inbound request on the server rather than only the routes the
|
|
125
|
+
* runtime dispatcher registers.
|
|
126
|
+
*/
|
|
122
127
|
readonly httpRequestsTotal: "http_requests_total";
|
|
123
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT
|
|
130
|
+
* through the same seam (#9834). It measures the REQUEST as the transport
|
|
131
|
+
* sees it — first sight to the response existing, middleware chain and
|
|
132
|
+
* body parse included — not the handler's share of it.
|
|
133
|
+
*/
|
|
124
134
|
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
125
135
|
/**
|
|
126
|
-
* Counter, labels: `method`, `route`. Incremented when an
|
|
127
|
-
*
|
|
136
|
+
* Counter, labels: `method`, `route`. Incremented when an in-flight
|
|
137
|
+
* handler throws after the response is sent. Emitted by
|
|
138
|
+
* `@objectstack/runtime`'s `instrumentRouteHandler`, and NOT movable to
|
|
139
|
+
* the seam above as-is: the observation carries a status but no throw
|
|
140
|
+
* signal, so a transport-side emitter would count a different population
|
|
141
|
+
* (#9834 records the fork).
|
|
128
142
|
*/
|
|
129
143
|
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
130
144
|
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
@@ -133,11 +147,40 @@ declare const SEMCONV: {
|
|
|
133
147
|
readonly storageOperationDurationMs: "storage_operation_duration_ms";
|
|
134
148
|
/** Counter, labels: `adapter`, `op`, `errorClass`. */
|
|
135
149
|
readonly storageErrorsTotal: "storage_errors_total";
|
|
136
|
-
/**
|
|
150
|
+
/**
|
|
151
|
+
* Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).
|
|
152
|
+
*
|
|
153
|
+
* ⚠️ A flat zero means "NO CONFIGURED CONSUMER", not "no cache activity",
|
|
154
|
+
* and — unlike the HTTP families above — it is NOT an instrumentation gap.
|
|
155
|
+
* The adapters hold the host's registry and count every call they receive
|
|
156
|
+
* (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails
|
|
157
|
+
* to communicate is WHY.
|
|
158
|
+
*
|
|
159
|
+
* The why: nothing consults the `cache` service unconditionally. Every
|
|
160
|
+
* production consumer is a rate-limit / budget counter store, and each is
|
|
161
|
+
* gated on a declaration somebody has to write — better-auth's per-IP
|
|
162
|
+
* counters (`rate_limit_max` / `rate_limit_window_seconds` in auth
|
|
163
|
+
* settings), the dispatcher's inbound limiter and its declarative
|
|
164
|
+
* per-endpoint buckets (an armed `rateLimit` budget; with none declared
|
|
165
|
+
* the dispatcher registers no limiter at all), and the per-number OTP send
|
|
166
|
+
* budget (an SMS send path). A default install declares none of them, so
|
|
167
|
+
* this counter stays at 0 while the server handles traffic normally.
|
|
168
|
+
*
|
|
169
|
+
* ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%
|
|
170
|
+
* hit rate or a broken adapter. Before trusting a cache hit-rate panel,
|
|
171
|
+
* confirm at least one consumer above is actually armed.
|
|
172
|
+
*/
|
|
137
173
|
readonly cacheLookupsTotal: "cache_lookups_total";
|
|
138
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same
|
|
176
|
+
* "zero = no configured consumer" reading as `cacheLookupsTotal` above.
|
|
177
|
+
*/
|
|
139
178
|
readonly cacheWritesTotal: "cache_writes_total";
|
|
140
|
-
/**
|
|
179
|
+
/**
|
|
180
|
+
* Counter, labels: `adapter`, `op`, `errorClass`. Same "zero = no
|
|
181
|
+
* configured consumer" reading as `cacheLookupsTotal` above — a zero is
|
|
182
|
+
* "nothing was asked of the cache", not "every call succeeded".
|
|
183
|
+
*/
|
|
141
184
|
readonly cacheErrorsTotal: "cache_errors_total";
|
|
142
185
|
/**
|
|
143
186
|
* Counter, labels: `app`, `job`. Incremented when a DECLARED background
|
|
@@ -165,6 +208,101 @@ declare const RUNTIME_METRICS: {
|
|
|
165
208
|
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
166
209
|
};
|
|
167
210
|
|
|
211
|
+
/**
|
|
212
|
+
* What an `arm*` call in this module did:
|
|
213
|
+
*
|
|
214
|
+
* - `'armed'` — the emitting observer was registered on this call.
|
|
215
|
+
* - `'already-armed'` — some earlier caller already armed this server for
|
|
216
|
+
* THIS metric family; the seam emits it, and this call registered nothing
|
|
217
|
+
* (first-wins).
|
|
218
|
+
* - `'unsupported'` — the transport does not implement the
|
|
219
|
+
* `IHttpServer.afterResponse` seam; it reports NO HTTP metrics (#9835:
|
|
220
|
+
* zero there means "not instrumented", never "no traffic"), and the
|
|
221
|
+
* caller must decide how to degrade — the runtime dispatcher falls back
|
|
222
|
+
* to instrumenting its own routes.
|
|
223
|
+
*/
|
|
224
|
+
type ArmHttpMetricResult = 'armed' | 'already-armed' | 'unsupported';
|
|
225
|
+
/**
|
|
226
|
+
* The name {@link armHttpRequestCounter} shipped with. Kept as an alias
|
|
227
|
+
* rather than renamed: the export is already in the pending release, and the
|
|
228
|
+
* two families arm through separate entry points anyway.
|
|
229
|
+
*/
|
|
230
|
+
type ArmHttpRequestCounterResult = ArmHttpMetricResult;
|
|
231
|
+
/**
|
|
232
|
+
* Arm `http_requests_total{method,route,status}` on a transport through the
|
|
233
|
+
* `IHttpServer.afterResponse` observation seam (#9835) — AT MOST ONCE per
|
|
234
|
+
* server, whoever calls first.
|
|
235
|
+
*
|
|
236
|
+
* ## Why arming is centralized here
|
|
237
|
+
*
|
|
238
|
+
* The contract's ownership rule says a request must never be double-counted,
|
|
239
|
+
* and two composition layers legitimately hold both a server and a metrics
|
|
240
|
+
* registry: the transport's own hosting plugin (`HonoServerPlugin`, which
|
|
241
|
+
* the 2026-08-18 ruling on #9650 made the counter's home) and the runtime
|
|
242
|
+
* dispatcher (whose `observability.metrics` config is the wiring the docs
|
|
243
|
+
* demonstrate). When a host hands ONE registry to both — the ordinary case —
|
|
244
|
+
* two independently-registered observers would land every request on the
|
|
245
|
+
* same series twice: exactly the #9833 distortion, rebuilt one seam over.
|
|
246
|
+
* Routing every arming through this function makes "exactly one
|
|
247
|
+
* counter-emitting observer per server" structural: the first caller arms
|
|
248
|
+
* (in the shipped composition that is the transport plugin, in Phase 1),
|
|
249
|
+
* every later caller is told the seam already counts.
|
|
250
|
+
*
|
|
251
|
+
* The label shape is pinned by the contract: `route` is the transport's
|
|
252
|
+
* `routePattern` — the registered PATTERN, never the concrete path — and
|
|
253
|
+
* `status` is stringified for the label set. Emission goes through the
|
|
254
|
+
* transport's observer-isolation guarantee, so a throwing registry cannot
|
|
255
|
+
* break a response.
|
|
256
|
+
*
|
|
257
|
+
* @param server - The transport. Pass the RAW registered `http.server`
|
|
258
|
+
* instance, not a wrapper: the latch is per object identity, and a wrapper
|
|
259
|
+
* would both fork the latch and (per #5122) risk erasing the optional
|
|
260
|
+
* member this function feature-detects.
|
|
261
|
+
* @param metrics - The registry the counter lands in. First caller wins; a
|
|
262
|
+
* second registry offered later is NOT added (the contract's one-owner
|
|
263
|
+
* rule), and the result says so.
|
|
264
|
+
*/
|
|
265
|
+
declare function armHttpRequestCounter(server: IHttpServer, metrics: MetricsRegistry): ArmHttpMetricResult;
|
|
266
|
+
/**
|
|
267
|
+
* Arm `http_request_duration_ms{method,route}` on a transport through the
|
|
268
|
+
* `IHttpServer.afterResponse` observation seam — AT MOST ONCE per server,
|
|
269
|
+
* whoever calls first. The duration half of #9834, built on the mechanism
|
|
270
|
+
* #9835 proved out for `http_requests_total`.
|
|
271
|
+
*
|
|
272
|
+
* ## Why the histogram has to move too
|
|
273
|
+
*
|
|
274
|
+
* #9835 moved only the counter, which left the docs' two derived signals
|
|
275
|
+
* inconsistent with each other: 5xx rate saw every inbound surface while p95
|
|
276
|
+
* latency still saw the dispatcher's own routes. An operator reading one
|
|
277
|
+
* dashboard got request volume for `/api/v1/*` beside a latency panel with no
|
|
278
|
+
* series for it — and the worse reading is the p95 that IS drawn, computed
|
|
279
|
+
* from dispatcher routes only and presented as the server's.
|
|
280
|
+
*
|
|
281
|
+
* ## ⚠️ The observation WINDOW changes with the emitter
|
|
282
|
+
*
|
|
283
|
+
* The dispatcher's per-route wrapper timed `await handler(req, res)` — handler
|
|
284
|
+
* latency. This seam times the transport's own `use('*')` around
|
|
285
|
+
* `await next()`, which is what {@link HttpResponseObservation.elapsedMs}
|
|
286
|
+
* means: "from the transport first seeing the request to the response
|
|
287
|
+
* existing". That includes the middleware chain and body parse, so the series
|
|
288
|
+
* can only move UP, never down. It is the number an operator's latency panel
|
|
289
|
+
* should have been showing — the request's latency rather than one layer's
|
|
290
|
+
* share of it — but it is a visible change in an existing series, so it is
|
|
291
|
+
* stated here, in the changeset, and in `docs/OBSERVABILITY.md` rather than
|
|
292
|
+
* left for a dashboard to discover.
|
|
293
|
+
*
|
|
294
|
+
* The label set is unchanged and stays the SEMCONV-declared `{method,route}`:
|
|
295
|
+
* `route` is the transport's `routePattern` (the registered PATTERN, never the
|
|
296
|
+
* concrete path), and no `status` label is added — a histogram split by status
|
|
297
|
+
* is a different series shape than the one the docs tell operators to graph.
|
|
298
|
+
*
|
|
299
|
+
* @param server - The transport. Pass the RAW registered `http.server`
|
|
300
|
+
* instance, not a wrapper — the latch is per object identity, and (per
|
|
301
|
+
* #5122) a wrapper risks erasing the optional member this feature-detects.
|
|
302
|
+
* @param metrics - The registry the histogram lands in. First caller wins.
|
|
303
|
+
*/
|
|
304
|
+
declare function armHttpRequestDurationHistogram(server: IHttpServer, metrics: MetricsRegistry): ArmHttpMetricResult;
|
|
305
|
+
|
|
168
306
|
/**
|
|
169
307
|
* No-op metrics registry — the default. Discards every observation.
|
|
170
308
|
* Production deployments should swap this for a real registry; tests
|
|
@@ -617,4 +755,4 @@ declare function isPerfDisclosurePrivileged(): boolean;
|
|
|
617
755
|
*/
|
|
618
756
|
declare function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean;
|
|
619
757
|
|
|
620
|
-
export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrincipal, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
|
|
758
|
+
export { type ArmHttpMetricResult, type ArmHttpRequestCounterResult, type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, armHttpRequestCounter, armHttpRequestDurationHistogram, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrincipal, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
|
package/dist/index.js
CHANGED
|
@@ -4,14 +4,28 @@ var OBSERVABILITY_ERRORS_SERVICE = "observability:errors";
|
|
|
4
4
|
|
|
5
5
|
// src/semconv.ts
|
|
6
6
|
var SEMCONV = {
|
|
7
|
-
// ── HTTP —
|
|
8
|
-
/**
|
|
7
|
+
// ── HTTP — emitter differs per family, see each ────────────────────
|
|
8
|
+
/**
|
|
9
|
+
* Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT
|
|
10
|
+
* through the `IHttpServer.afterResponse` seam (#9835), so it covers
|
|
11
|
+
* every inbound request on the server rather than only the routes the
|
|
12
|
+
* runtime dispatcher registers.
|
|
13
|
+
*/
|
|
9
14
|
httpRequestsTotal: "http_requests_total",
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT
|
|
17
|
+
* through the same seam (#9834). It measures the REQUEST as the transport
|
|
18
|
+
* sees it — first sight to the response existing, middleware chain and
|
|
19
|
+
* body parse included — not the handler's share of it.
|
|
20
|
+
*/
|
|
11
21
|
httpRequestDurationMs: "http_request_duration_ms",
|
|
12
22
|
/**
|
|
13
|
-
* Counter, labels: `method`, `route`. Incremented when an
|
|
14
|
-
*
|
|
23
|
+
* Counter, labels: `method`, `route`. Incremented when an in-flight
|
|
24
|
+
* handler throws after the response is sent. Emitted by
|
|
25
|
+
* `@objectstack/runtime`'s `instrumentRouteHandler`, and NOT movable to
|
|
26
|
+
* the seam above as-is: the observation carries a status but no throw
|
|
27
|
+
* signal, so a transport-side emitter would count a different population
|
|
28
|
+
* (#9834 records the fork).
|
|
15
29
|
*/
|
|
16
30
|
httpRequestErrorsTotal: "http_request_errors_total",
|
|
17
31
|
// ── Storage — emitted by `@objectstack/service-storage` adapters ──
|
|
@@ -22,11 +36,41 @@ var SEMCONV = {
|
|
|
22
36
|
/** Counter, labels: `adapter`, `op`, `errorClass`. */
|
|
23
37
|
storageErrorsTotal: "storage_errors_total",
|
|
24
38
|
// ── Cache — emitted by `@objectstack/service-cache` adapters ──
|
|
25
|
-
|
|
39
|
+
// Uniform emitter; what varies is who CONSULTS the service — see below.
|
|
40
|
+
/**
|
|
41
|
+
* Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ A flat zero means "NO CONFIGURED CONSUMER", not "no cache activity",
|
|
44
|
+
* and — unlike the HTTP families above — it is NOT an instrumentation gap.
|
|
45
|
+
* The adapters hold the host's registry and count every call they receive
|
|
46
|
+
* (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails
|
|
47
|
+
* to communicate is WHY.
|
|
48
|
+
*
|
|
49
|
+
* The why: nothing consults the `cache` service unconditionally. Every
|
|
50
|
+
* production consumer is a rate-limit / budget counter store, and each is
|
|
51
|
+
* gated on a declaration somebody has to write — better-auth's per-IP
|
|
52
|
+
* counters (`rate_limit_max` / `rate_limit_window_seconds` in auth
|
|
53
|
+
* settings), the dispatcher's inbound limiter and its declarative
|
|
54
|
+
* per-endpoint buckets (an armed `rateLimit` budget; with none declared
|
|
55
|
+
* the dispatcher registers no limiter at all), and the per-number OTP send
|
|
56
|
+
* budget (an SMS send path). A default install declares none of them, so
|
|
57
|
+
* this counter stays at 0 while the server handles traffic normally.
|
|
58
|
+
*
|
|
59
|
+
* ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%
|
|
60
|
+
* hit rate or a broken adapter. Before trusting a cache hit-rate panel,
|
|
61
|
+
* confirm at least one consumer above is actually armed.
|
|
62
|
+
*/
|
|
26
63
|
cacheLookupsTotal: "cache_lookups_total",
|
|
27
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same
|
|
66
|
+
* "zero = no configured consumer" reading as `cacheLookupsTotal` above.
|
|
67
|
+
*/
|
|
28
68
|
cacheWritesTotal: "cache_writes_total",
|
|
29
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Counter, labels: `adapter`, `op`, `errorClass`. Same "zero = no
|
|
71
|
+
* configured consumer" reading as `cacheLookupsTotal` above — a zero is
|
|
72
|
+
* "nothing was asked of the cache", not "every call succeeded".
|
|
73
|
+
*/
|
|
30
74
|
cacheErrorsTotal: "cache_errors_total",
|
|
31
75
|
// ── Background jobs — emitted by `@objectstack/runtime`'s AppPlugin ──
|
|
32
76
|
/**
|
|
@@ -50,6 +94,42 @@ var RUNTIME_METRICS = {
|
|
|
50
94
|
httpRequestErrorsTotal: SEMCONV.httpRequestErrorsTotal
|
|
51
95
|
};
|
|
52
96
|
|
|
97
|
+
// src/http-transport-metrics.ts
|
|
98
|
+
var HTTP_REQUEST_COUNTER_ARMED = /* @__PURE__ */ Symbol.for(
|
|
99
|
+
"objectstack.observability.httpRequestCounterArmed"
|
|
100
|
+
);
|
|
101
|
+
function armHttpRequestCounter(server, metrics) {
|
|
102
|
+
if (typeof server.afterResponse !== "function") return "unsupported";
|
|
103
|
+
const latched = server;
|
|
104
|
+
if (latched[HTTP_REQUEST_COUNTER_ARMED]) return "already-armed";
|
|
105
|
+
latched[HTTP_REQUEST_COUNTER_ARMED] = true;
|
|
106
|
+
server.afterResponse((observation) => {
|
|
107
|
+
metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {
|
|
108
|
+
method: observation.method,
|
|
109
|
+
route: observation.routePattern,
|
|
110
|
+
status: String(observation.status)
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
return "armed";
|
|
114
|
+
}
|
|
115
|
+
var HTTP_REQUEST_DURATION_ARMED = /* @__PURE__ */ Symbol.for(
|
|
116
|
+
"objectstack.observability.httpRequestDurationArmed"
|
|
117
|
+
);
|
|
118
|
+
function armHttpRequestDurationHistogram(server, metrics) {
|
|
119
|
+
if (typeof server.afterResponse !== "function") return "unsupported";
|
|
120
|
+
const latched = server;
|
|
121
|
+
if (latched[HTTP_REQUEST_DURATION_ARMED]) return "already-armed";
|
|
122
|
+
latched[HTTP_REQUEST_DURATION_ARMED] = true;
|
|
123
|
+
server.afterResponse((observation) => {
|
|
124
|
+
metrics.histogram(
|
|
125
|
+
RUNTIME_METRICS.httpRequestDurationMs,
|
|
126
|
+
observation.elapsedMs,
|
|
127
|
+
{ method: observation.method, route: observation.routePattern }
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
return "armed";
|
|
131
|
+
}
|
|
132
|
+
|
|
53
133
|
// src/metrics-exporters.ts
|
|
54
134
|
var NoopMetricsRegistry = class {
|
|
55
135
|
counter() {
|
|
@@ -648,6 +728,8 @@ export {
|
|
|
648
728
|
RUNTIME_METRICS,
|
|
649
729
|
SEMCONV,
|
|
650
730
|
allowPerfDisclosure,
|
|
731
|
+
armHttpRequestCounter,
|
|
732
|
+
armHttpRequestDurationHistogram,
|
|
651
733
|
countServerTiming,
|
|
652
734
|
currentPerfTiming,
|
|
653
735
|
formatServerTiming,
|