@objectstack/observability 17.1.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 +101 -0
- package/dist/index.cjs +19 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -10
- package/dist/index.d.ts +0 -10
- package/dist/index.js +19 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,106 @@
|
|
|
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
|
+
|
|
3
104
|
## 17.1.0
|
|
4
105
|
|
|
5
106
|
### Patch Changes
|
package/dist/index.cjs
CHANGED
|
@@ -76,15 +76,20 @@ var SEMCONV = {
|
|
|
76
76
|
* body parse included — not the handler's share of it.
|
|
77
77
|
*/
|
|
78
78
|
httpRequestDurationMs: "http_request_duration_ms",
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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.
|
|
88
93
|
// ── Storage — emitted by `@objectstack/service-storage` adapters ──
|
|
89
94
|
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
90
95
|
storageOperationsTotal: "storage_operations_total",
|
|
@@ -147,8 +152,11 @@ var SEMCONV = {
|
|
|
147
152
|
};
|
|
148
153
|
var RUNTIME_METRICS = {
|
|
149
154
|
httpRequestsTotal: SEMCONV.httpRequestsTotal,
|
|
150
|
-
httpRequestDurationMs: SEMCONV.httpRequestDurationMs
|
|
151
|
-
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.
|
|
152
160
|
};
|
|
153
161
|
|
|
154
162
|
// src/http-transport-metrics.ts
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/service-names.ts","../src/semconv.ts","../src/http-transport-metrics.ts","../src/metrics-exporters.ts","../src/error-exporters.ts","../src/loggers.ts","../src/perf-timing.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/observability` — vendor-neutral contracts and exporters\n * for ObjectStack metrics, errors, and logs.\n *\n * @see {@link MetricsRegistry} {@link ErrorReporter} {@link Logger}\n */\n\n// Contracts\nexport type { MetricsRegistry, MetricSample, ErrorReporter, CapturedError, Logger } from './contracts.js';\n\n// Service-registry names (consumed by runtime's ObservabilityServicePlugin and lookup sites)\nexport { OBSERVABILITY_METRICS_SERVICE, OBSERVABILITY_ERRORS_SERVICE } from './service-names.js';\n\n// Semantic conventions\nexport { SEMCONV, RUNTIME_METRICS } from './semconv.js';\n\n// Transport-agnostic HTTP metrics, each family armed at most once per server\n// via the `IHttpServer.afterResponse` observation seam (#9835 counter, #9834\n// duration histogram)\nexport {\n armHttpRequestCounter,\n armHttpRequestDurationHistogram,\n type ArmHttpMetricResult,\n type ArmHttpRequestCounterResult,\n} from './http-transport-metrics.js';\n\n// Metric exporters\nexport {\n NoopMetricsRegistry,\n InMemoryMetricsRegistry,\n ConsoleMetricsRegistry,\n OtlpHttpMetricsRegistry,\n type OtlpHttpExporterOptions,\n} from './metrics-exporters.js';\n\n// Error reporters\nexport {\n NoopErrorReporter,\n InMemoryErrorReporter,\n ConsoleErrorReporter,\n} from './error-exporters.js';\n\n// Loggers\nexport {\n NoopLogger,\n ConsoleLogger,\n JsonLogger,\n LOG_LEVELS,\n type LogLevel,\n} from './loggers.js';\n\n// Per-request performance timing (Server-Timing header)\nexport {\n PerfTiming,\n perfNow,\n formatServerTiming,\n runWithPerfTiming,\n currentPerfTiming,\n recordServerTiming,\n startServerTiming,\n measureServerTiming,\n countServerTiming,\n recordServerTimingDetail,\n runWithPerfDisclosure,\n allowPerfDisclosure,\n isPerfDisclosureAllowed,\n isPerfDisclosurePrivileged,\n isPerfDisclosurePrincipal,\n type ServerTimingMark,\n type ServerTimingDetail,\n type PerfDisclosureGate,\n} from './perf-timing.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical service-registry names for the host's observability\n * backends. Plugins look these up to discover the configured\n * {@link MetricsRegistry} / {@link ErrorReporter} without each host\n * having to thread observability config through every plugin\n * constructor.\n *\n * See `@objectstack/runtime` → `ObservabilityServicePlugin` for the\n * registration side.\n */\nexport const OBSERVABILITY_METRICS_SERVICE = 'observability:metrics';\nexport const OBSERVABILITY_ERRORS_SERVICE = 'observability:errors';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Semantic conventions — canonical metric names emitted by the\n * framework. Listed here so hosts can wire alerts/dashboards against\n * a stable namespace and so call sites don't sprinkle string\n * literals through the code base.\n *\n * Naming follows Prometheus conventions:\n *\n * - snake_case identifiers.\n * - `_total` suffix for monotonic counters.\n * - `_ms`, `_seconds`, `_bytes` suffixes for histograms / gauges\n * with units.\n *\n * Groups roughly mirror the framework subsystems that emit them.\n * Cloud-specific metrics (DO restarts, Workers Analytics Engine\n * writes, …) do NOT belong here — they are deployment-specific and\n * stay in the deployment repo.\n */\nexport const SEMCONV = {\n // ── HTTP — emitter differs per family, see each ────────────────────\n /**\n * Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT\n * through the `IHttpServer.afterResponse` seam (#9835), so it covers\n * every inbound request on the server rather than only the routes the\n * runtime dispatcher registers.\n */\n httpRequestsTotal: 'http_requests_total',\n /**\n * Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT\n * through the same seam (#9834). It measures the REQUEST as the transport\n * sees it — first sight to the response existing, middleware chain and\n * body parse included — not the handler's share of it.\n */\n httpRequestDurationMs: 'http_request_duration_ms',\n /**\n * Counter, labels: `method`, `route`. Incremented when an in-flight\n * handler throws after the response is sent. Emitted by\n * `@objectstack/runtime`'s `instrumentRouteHandler`, and NOT movable to\n * the seam above as-is: the observation carries a status but no throw\n * signal, so a transport-side emitter would count a different population\n * (#9834 records the fork).\n */\n httpRequestErrorsTotal: 'http_request_errors_total',\n\n // ── Storage — emitted by `@objectstack/service-storage` adapters ──\n /** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */\n storageOperationsTotal: 'storage_operations_total',\n /** Histogram (ms), labels: `adapter`, `op`. */\n storageOperationDurationMs: 'storage_operation_duration_ms',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n storageErrorsTotal: 'storage_errors_total',\n\n // ── Cache — emitted by `@objectstack/service-cache` adapters ──\n // Uniform emitter; what varies is who CONSULTS the service — see below.\n /**\n * Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).\n *\n * ⚠️ A flat zero means \"NO CONFIGURED CONSUMER\", not \"no cache activity\",\n * and — unlike the HTTP families above — it is NOT an instrumentation gap.\n * The adapters hold the host's registry and count every call they receive\n * (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails\n * to communicate is WHY.\n *\n * The why: nothing consults the `cache` service unconditionally. Every\n * production consumer is a rate-limit / budget counter store, and each is\n * gated on a declaration somebody has to write — better-auth's per-IP\n * counters (`rate_limit_max` / `rate_limit_window_seconds` in auth\n * settings), the dispatcher's inbound limiter and its declarative\n * per-endpoint buckets (an armed `rateLimit` budget; with none declared\n * the dispatcher registers no limiter at all), and the per-number OTP send\n * budget (an SMS send path). A default install declares none of them, so\n * this counter stays at 0 while the server handles traffic normally.\n *\n * ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%\n * hit rate or a broken adapter. Before trusting a cache hit-rate panel,\n * confirm at least one consumer above is actually armed.\n */\n cacheLookupsTotal: 'cache_lookups_total',\n /**\n * Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same\n * \"zero = no configured consumer\" reading as `cacheLookupsTotal` above.\n */\n cacheWritesTotal: 'cache_writes_total',\n /**\n * Counter, labels: `adapter`, `op`, `errorClass`. Same \"zero = no\n * configured consumer\" reading as `cacheLookupsTotal` above — a zero is\n * \"nothing was asked of the cache\", not \"every call succeeded\".\n */\n cacheErrorsTotal: 'cache_errors_total',\n\n // ── Background jobs — emitted by `@objectstack/runtime`'s AppPlugin ──\n /**\n * Counter, labels: `app`, `job`. Incremented when a DECLARED background\n * job could not be handed to the job service — i.e. the app booted green\n * but that job will never run (#4567). Any non-zero value is an outage of\n * the job, not a warning.\n */\n jobScheduleFailuresTotal: 'job_schedule_failures_total',\n\n // ── Package / registry-reader — emitted by `@objectstack/service-package` ──\n /** Counter, labels: `result` (`ok`|`miss`|`error`). */\n registryLookupsTotal: 'registry_lookups_total',\n /** Histogram (ms). */\n registryLookupDurationMs: 'registry_lookup_duration_ms',\n /** Counter, labels: `source` (`r2`|`http`|`local`), `result` (`hit`|`miss`|`error`). */\n registrySourceFetchesTotal: 'registry_source_fetches_total',\n} as const;\n\n/**\n * Backwards-compat alias. `RUNTIME_METRICS` was the original (HTTP-only)\n * constant name shipped from `@objectstack/runtime`; we keep it here so\n * existing code reading `RUNTIME_METRICS.httpRequestsTotal` continues\n * to work after the constants moved into this package.\n */\nexport const RUNTIME_METRICS = {\n httpRequestsTotal: SEMCONV.httpRequestsTotal,\n httpRequestDurationMs: SEMCONV.httpRequestDurationMs,\n httpRequestErrorsTotal: SEMCONV.httpRequestErrorsTotal,\n} as const;\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IHttpServer } from '@objectstack/spec/contracts';\nimport type { MetricsRegistry } from './contracts.js';\nimport { RUNTIME_METRICS } from './semconv.js';\n\n/**\n * What an `arm*` call in this module did:\n *\n * - `'armed'` — the emitting observer was registered on this call.\n * - `'already-armed'` — some earlier caller already armed this server for\n * THIS metric family; the seam emits it, and this call registered nothing\n * (first-wins).\n * - `'unsupported'` — the transport does not implement the\n * `IHttpServer.afterResponse` seam; it reports NO HTTP metrics (#9835:\n * zero there means \"not instrumented\", never \"no traffic\"), and the\n * caller must decide how to degrade — the runtime dispatcher falls back\n * to instrumenting its own routes.\n */\nexport type ArmHttpMetricResult = 'armed' | 'already-armed' | 'unsupported';\n\n/**\n * The name {@link armHttpRequestCounter} shipped with. Kept as an alias\n * rather than renamed: the export is already in the pending release, and the\n * two families arm through separate entry points anyway.\n */\nexport type ArmHttpRequestCounterResult = ArmHttpMetricResult;\n\n/**\n * The per-server latch behind the contract's ownership rule. A registered\n * global-registry symbol (`Symbol.for`), not a module-level WeakSet, so the\n * latch cannot fork if two copies of this module ever coexist in one\n * process — the whole point is that there is exactly ONE latch per server\n * object, whoever asks.\n */\nconst HTTP_REQUEST_COUNTER_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestCounterArmed',\n);\n\n/**\n * Arm `http_requests_total{method,route,status}` on a transport through the\n * `IHttpServer.afterResponse` observation seam (#9835) — AT MOST ONCE per\n * server, whoever calls first.\n *\n * ## Why arming is centralized here\n *\n * The contract's ownership rule says a request must never be double-counted,\n * and two composition layers legitimately hold both a server and a metrics\n * registry: the transport's own hosting plugin (`HonoServerPlugin`, which\n * the 2026-08-18 ruling on #9650 made the counter's home) and the runtime\n * dispatcher (whose `observability.metrics` config is the wiring the docs\n * demonstrate). When a host hands ONE registry to both — the ordinary case —\n * two independently-registered observers would land every request on the\n * same series twice: exactly the #9833 distortion, rebuilt one seam over.\n * Routing every arming through this function makes \"exactly one\n * counter-emitting observer per server\" structural: the first caller arms\n * (in the shipped composition that is the transport plugin, in Phase 1),\n * every later caller is told the seam already counts.\n *\n * The label shape is pinned by the contract: `route` is the transport's\n * `routePattern` — the registered PATTERN, never the concrete path — and\n * `status` is stringified for the label set. Emission goes through the\n * transport's observer-isolation guarantee, so a throwing registry cannot\n * break a response.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper: the latch is per object identity, and a wrapper\n * would both fork the latch and (per #5122) risk erasing the optional\n * member this function feature-detects.\n * @param metrics - The registry the counter lands in. First caller wins; a\n * second registry offered later is NOT added (the contract's one-owner\n * rule), and the result says so.\n */\nexport function armHttpRequestCounter(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_COUNTER_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_COUNTER_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_COUNTER_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {\n method: observation.method,\n route: observation.routePattern,\n status: String(observation.status),\n });\n });\n return 'armed';\n}\n\n/**\n * The duration family's own latch. A SEPARATE registered symbol from the\n * counter's, deliberately: the two families are armed through separate entry\n * points and gated separately on the dispatcher's per-route wrapper, so a\n * host that arms one must not silently latch the other away.\n */\nconst HTTP_REQUEST_DURATION_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestDurationArmed',\n);\n\n/**\n * Arm `http_request_duration_ms{method,route}` on a transport through the\n * `IHttpServer.afterResponse` observation seam — AT MOST ONCE per server,\n * whoever calls first. The duration half of #9834, built on the mechanism\n * #9835 proved out for `http_requests_total`.\n *\n * ## Why the histogram has to move too\n *\n * #9835 moved only the counter, which left the docs' two derived signals\n * inconsistent with each other: 5xx rate saw every inbound surface while p95\n * latency still saw the dispatcher's own routes. An operator reading one\n * dashboard got request volume for `/api/v1/*` beside a latency panel with no\n * series for it — and the worse reading is the p95 that IS drawn, computed\n * from dispatcher routes only and presented as the server's.\n *\n * ## ⚠️ The observation WINDOW changes with the emitter\n *\n * The dispatcher's per-route wrapper timed `await handler(req, res)` — handler\n * latency. This seam times the transport's own `use('*')` around\n * `await next()`, which is what {@link HttpResponseObservation.elapsedMs}\n * means: \"from the transport first seeing the request to the response\n * existing\". That includes the middleware chain and body parse, so the series\n * can only move UP, never down. It is the number an operator's latency panel\n * should have been showing — the request's latency rather than one layer's\n * share of it — but it is a visible change in an existing series, so it is\n * stated here, in the changeset, and in `docs/OBSERVABILITY.md` rather than\n * left for a dashboard to discover.\n *\n * The label set is unchanged and stays the SEMCONV-declared `{method,route}`:\n * `route` is the transport's `routePattern` (the registered PATTERN, never the\n * concrete path), and no `status` label is added — a histogram split by status\n * is a different series shape than the one the docs tell operators to graph.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper — the latch is per object identity, and (per\n * #5122) a wrapper risks erasing the optional member this feature-detects.\n * @param metrics - The registry the histogram lands in. First caller wins.\n */\nexport function armHttpRequestDurationHistogram(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_DURATION_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_DURATION_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_DURATION_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.histogram(\n RUNTIME_METRICS.httpRequestDurationMs,\n observation.elapsedMs,\n { method: observation.method, route: observation.routePattern },\n );\n });\n return 'armed';\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { MetricsRegistry, MetricSample } from './contracts.js';\n\n// ─── Noop ─────────────────────────────────────────────────────────────\n\n/**\n * No-op metrics registry — the default. Discards every observation.\n * Production deployments should swap this for a real registry; tests\n * can use {@link InMemoryMetricsRegistry} to assert emissions.\n */\nexport class NoopMetricsRegistry implements MetricsRegistry {\n counter(): void { }\n histogram(): void { }\n gauge(): void { }\n}\n\n// ─── In-memory (tests + dev inspection) ───────────────────────────────\n\n/**\n * In-memory registry used for tests and local inspection. Stores\n * every observation in insertion order; query via the helpers below\n * or read {@link samples} directly.\n *\n * Not intended for production — unbounded growth.\n */\nexport class InMemoryMetricsRegistry implements MetricsRegistry {\n readonly samples: MetricSample[] = [];\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.samples.push({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n /** Sum of counter increments matching `name` and optionally a label subset. */\n totalCounter(name: string, labelMatch: Record<string, string> = {}): number {\n return this.samples\n .filter(s => s.kind === 'counter' && s.name === name && matchesLabels(s.labels, labelMatch))\n .reduce((acc, s) => acc + s.value, 0);\n }\n\n /** Raw histogram observations matching `name` and optionally a label subset. */\n histogramValues(name: string, labelMatch: Record<string, string> = {}): number[] {\n return this.samples\n .filter(s => s.kind === 'histogram' && s.name === name && matchesLabels(s.labels, labelMatch))\n .map(s => s.value);\n }\n\n /** Last gauge value matching `name` and optionally a label subset, or undefined. */\n lastGauge(name: string, labelMatch: Record<string, string> = {}): number | undefined {\n for (let i = this.samples.length - 1; i >= 0; i--) {\n const s = this.samples[i];\n if (s.kind === 'gauge' && s.name === name && matchesLabels(s.labels, labelMatch)) {\n return s.value;\n }\n }\n return undefined;\n }\n\n /** Clear all recorded samples. */\n reset(): void {\n this.samples.length = 0;\n }\n}\n\nfunction matchesLabels(actual: Record<string, string>, expected: Record<string, string>): boolean {\n for (const [k, v] of Object.entries(expected)) {\n if (actual[k] !== v) return false;\n }\n return true;\n}\n\n// ─── Console (development) ────────────────────────────────────────────\n\n/**\n * Console metrics registry — prints one line per observation. Useful\n * during local development to confirm that instrumentation is firing.\n *\n * Not intended for production: writing every observation to stdout\n * defeats Prometheus / OTLP pipelines and dominates request latency.\n */\nexport class ConsoleMetricsRegistry implements MetricsRegistry {\n constructor(private readonly opts: { sink?: (line: string) => void; prefix?: string } = {}) { }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.emit('counter', name, value, labels);\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('histogram', name, value, labels);\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('gauge', name, value, labels);\n }\n\n private emit(kind: string, name: string, value: number, labels: Record<string, string>): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.log(s); });\n const prefix = this.opts.prefix ?? '[metric]';\n const labelStr = Object.entries(labels)\n .map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n .join(' ');\n sink(`${prefix} ${kind} ${name} ${value}${labelStr ? ' ' + labelStr : ''}`);\n } catch {\n // Per contract: never throw from a metric call site.\n }\n }\n}\n\n// ─── OTLP / HTTP (Prometheus via OTel Collector, Grafana Cloud, …) ────\n\n/**\n * Configuration for {@link OtlpHttpMetricsRegistry}.\n */\nexport interface OtlpHttpExporterOptions {\n /**\n * OTLP/HTTP metrics endpoint, e.g. `http://otel-collector:4318/v1/metrics`.\n * The path is appended automatically if missing.\n */\n endpoint: string;\n\n /** Optional headers (Authorization, x-tenant, …). */\n headers?: Record<string, string>;\n\n /**\n * Resource attributes — `service.name`, `service.namespace`,\n * `deployment.environment`, etc. Merged into the OTLP `resource`\n * block on every export.\n */\n resource?: Record<string, string>;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`.\n * Allows Workers / undici / node-fetch substitution and test\n * doubles.\n */\n fetch?: typeof fetch;\n\n /**\n * Called when an export attempt fails. Default: silently swallow\n * (per the contract that metric emission must not throw / log\n * loudly on the hot path).\n */\n onError?: (error: unknown) => void;\n\n /**\n * Maximum number of samples buffered before {@link OtlpHttpMetricsRegistry.flush}\n * is called automatically. Defaults to 1024.\n */\n maxBufferSize?: number;\n}\n\n/**\n * OTLP/HTTP metrics exporter.\n *\n * Buffers samples in memory and serialises them to the OpenTelemetry\n * Protocol JSON encoding when {@link flush} is called (manually or\n * automatically once the buffer hits the configured size).\n *\n * Intentionally does **not** start an interval timer in the constructor:\n * (a) it makes the exporter usable on Cloudflare Workers where\n * `setInterval` is restricted, and (b) it keeps unit tests deterministic.\n * Long-running hosts should call `flush()` on a schedule\n * (e.g. `setInterval(() => reg.flush(), 10_000)` on Node, or\n * `ctx.waitUntil(reg.flush())` from a Workers fetch handler).\n *\n * Only counters, histograms, and gauges are emitted — no support for\n * exemplars or aggregation temporality switches (the Collector handles\n * those on the upstream side).\n */\nexport class OtlpHttpMetricsRegistry implements MetricsRegistry {\n private buffer: MetricSample[] = [];\n private readonly endpoint: string;\n private readonly headers: Record<string, string>;\n private readonly resource: Record<string, string>;\n private readonly maxBufferSize: number;\n private readonly fetchImpl: typeof fetch;\n private readonly onError: (error: unknown) => void;\n\n constructor(options: OtlpHttpExporterOptions) {\n this.endpoint = normaliseEndpoint(options.endpoint);\n this.headers = options.headers ?? {};\n this.resource = options.resource ?? {};\n this.maxBufferSize = options.maxBufferSize ?? 1024;\n this.fetchImpl = options.fetch ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : noFetch);\n this.onError = options.onError ?? (() => { });\n }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.record({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n private record(sample: MetricSample): void {\n this.buffer.push(sample);\n if (this.buffer.length >= this.maxBufferSize) {\n // Fire-and-forget: do not block the call site.\n void this.flush().catch(this.onError);\n }\n }\n\n /** Snapshot the current buffer (for tests). */\n peek(): readonly MetricSample[] {\n return this.buffer.slice();\n }\n\n /**\n * Send the buffered samples to the OTLP endpoint and clear the\n * buffer. Safe to call concurrently — each invocation takes a\n * snapshot before clearing.\n */\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n const samples = this.buffer;\n this.buffer = [];\n try {\n const body = JSON.stringify(serialiseToOtlp(samples, this.resource));\n const res = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...this.headers },\n body,\n });\n if (!res.ok) {\n this.onError(new Error(`OTLP export returned HTTP ${res.status}`));\n }\n } catch (err) {\n this.onError(err);\n }\n }\n}\n\nfunction normaliseEndpoint(raw: string): string {\n const trimmed = raw.replace(/\\/+$/, '');\n if (/\\/v1\\/metrics$/.test(trimmed)) return trimmed;\n return trimmed + '/v1/metrics';\n}\n\nfunction noFetch(): never {\n throw new Error('OtlpHttpMetricsRegistry: no global fetch available; pass options.fetch');\n}\n\n/**\n * Minimal OTLP/JSON metrics serialiser.\n *\n * Implements the subset of <https://opentelemetry.io/docs/specs/otlp/>\n * that we actually emit — sums for counters, gauges for gauges,\n * histograms encoded as bucketless \"summary\"-style histograms with\n * explicit per-sample data points. The Collector accepts this shape\n * and applies its own bucketing.\n *\n * Aggregation temporality is set to DELTA (2) because every flush\n * sends only the samples accumulated since the last flush.\n */\nfunction serialiseToOtlp(samples: MetricSample[], resource: Record<string, string>): unknown {\n const byName = new Map<string, { kind: MetricSample['kind']; points: MetricSample[] }>();\n for (const s of samples) {\n const existing = byName.get(s.name);\n if (existing) existing.points.push(s);\n else byName.set(s.name, { kind: s.kind, points: [s] });\n }\n\n const metrics = Array.from(byName.entries()).map(([name, { kind, points }]) => {\n if (kind === 'counter') {\n return {\n name,\n sum: {\n dataPoints: points.map(p => toNumberPoint(p)),\n aggregationTemporality: 2,\n isMonotonic: true,\n },\n };\n }\n if (kind === 'gauge') {\n return { name, gauge: { dataPoints: points.map(p => toNumberPoint(p)) } };\n }\n // histogram: emit a histogram with a single bucket boundary so the\n // Collector treats it as a recorded distribution.\n return {\n name,\n histogram: {\n aggregationTemporality: 2,\n dataPoints: points.map(p => ({\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n count: '1',\n sum: p.value,\n bucketCounts: ['0', '1'],\n explicitBounds: [p.value],\n })),\n },\n };\n });\n\n return {\n resourceMetrics: [{\n resource: { attributes: toAttributes(resource) },\n scopeMetrics: [{\n scope: { name: '@objectstack/observability', version: '0.1.0' },\n metrics,\n }],\n }],\n };\n}\n\nfunction toNumberPoint(p: MetricSample): unknown {\n return {\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n asDouble: p.value,\n };\n}\n\nfunction toAttributes(labels: Record<string, string>): unknown[] {\n return Object.entries(labels).map(([key, value]) => ({\n key,\n value: { stringValue: value },\n }));\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ErrorReporter, CapturedError } from './contracts.js';\n\n/** No-op reporter — the default. */\nexport class NoopErrorReporter implements ErrorReporter {\n captureException(): void { }\n}\n\n/** In-memory reporter for tests. */\nexport class InMemoryErrorReporter implements ErrorReporter {\n readonly captured: CapturedError[] = [];\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n this.captured.push({ error, context, at: Date.now() });\n }\n\n reset(): void {\n this.captured.length = 0;\n }\n}\n\n/**\n * Console error reporter — writes a structured JSON line to stderr per\n * captured exception. Convenient default for local development and for\n * any deployment that ships stderr to a log aggregator (e.g. Loki,\n * fluent-bit) but does not have a dedicated APM.\n *\n * Stack traces are included when the captured value is an `Error`.\n */\nexport class ConsoleErrorReporter implements ErrorReporter {\n constructor(private readonly opts: { sink?: (line: string) => void } = {}) { }\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.error(s); });\n const record: Record<string, unknown> = {\n ts: new Date().toISOString(),\n level: 'error',\n msg: error instanceof Error ? error.message : String(error),\n context,\n };\n if (error instanceof Error && error.stack) {\n record.stack = error.stack;\n }\n sink(JSON.stringify(record));\n } catch {\n // Per contract: error reporting must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Logger } from './contracts.js';\n\n/** Recognised log levels in increasing severity order. */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n fatal: 50,\n};\n\n/** No-op logger — discards every message. */\nexport class NoopLogger implements Logger {\n debug(): void { }\n info(): void { }\n warn(): void { }\n error(): void { }\n fatal(): void { }\n child(): Logger { return this; }\n}\n\n/**\n * Console logger — pretty-printed messages for local development.\n *\n * Not suitable for production where structured JSON is required;\n * use {@link JsonLogger} there instead.\n */\nexport class ConsoleLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n } = {},\n ) { }\n\n private get threshold(): number {\n return LEVEL_PRIORITY[this.opts.level ?? 'info'];\n }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> {\n return this.opts.context ?? {};\n }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new ConsoleLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const merged = { ...this.context, ...(meta ?? {}) };\n const tail = Object.keys(merged).length ? ' ' + JSON.stringify(merged) : '';\n const line = `[${level}] ${msg}${tail}`;\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n\n/**\n * JSON logger — one JSON object per line on stdout (errors on stderr).\n *\n * Matches the shape that Loki / fluent-bit / Cloudflare Logpush ingest\n * by default. Every record contains `ts`, `level`, `msg`, and any\n * accumulated child-context plus per-call `meta`.\n *\n * Use this in production. Use {@link ConsoleLogger} during development\n * for human-friendly output.\n */\nexport class JsonLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n /** Optional fields injected into every record (`service`, `env`, …). */\n base?: Record<string, unknown>;\n /** Wall clock for tests. */\n now?: () => Date;\n } = {},\n ) { }\n\n private get threshold(): number { return LEVEL_PRIORITY[this.opts.level ?? 'info']; }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> { return this.opts.context ?? {}; }\n private get base(): Record<string, unknown> { return this.opts.base ?? {}; }\n private get now(): () => Date { return this.opts.now ?? (() => new Date()); }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new JsonLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const record = {\n ts: this.now().toISOString(),\n level,\n msg,\n ...this.base,\n ...this.context,\n ...(meta ?? {}),\n };\n const line = JSON.stringify(record);\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Per-request performance timing - a tiny, dependency-free collector that\n * accumulates named phase durations during a single request and serializes\n * them into the W3C `Server-Timing` response header.\n *\n * @see <https://www.w3.org/TR/server-timing/>\n * @see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>\n *\n * Two ways to record:\n *\n * 1. **Explicit collector.** Hold a {@link PerfTiming} instance and call\n * `start()` / `record()` / `measure()` directly. The HTTP adapter owns\n * the instance for a request.\n *\n * 2. **Ambient collector.** Run a request inside {@link runWithPerfTiming}\n * and any framework code on that async call chain records phases via the\n * free functions ({@link measureServerTiming}, {@link startServerTiming},\n * {@link recordServerTiming}, {@link countServerTiming}) without threading\n * the request object through every layer. When no collector is active the\n * free functions are cheap no-ops, so call sites pay nothing when the\n * feature is off. High-frequency phases (per SQL query, per hook) use\n * {@link countServerTiming} to fold into one aggregate mark carrying a\n * total duration and an event count.\n *\n * `Server-Timing` exposes internal phase durations to any client, which is a\n * (mild) information-disclosure surface - it helps an attacker profile the\n * backend. Emission is therefore opt-in (\"perf-tuning mode\"); the collector\n * itself never decides whether to emit, it only measures.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * One recorded phase of a request's server-side processing, serialized as a\n * single member of the `Server-Timing` header.\n */\nexport interface ServerTimingMark {\n /** Metric name. Coerced to a Server-Timing token on record. */\n name: string;\n /** Duration in milliseconds. */\n dur: number;\n /** Optional human-readable description (rendered as the quoted `desc`). */\n desc?: string;\n}\n\n/**\n * One recorded sub-event when DETAIL capture is on (see\n * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds\n * every query into a single count+duration — a detail sample keeps the\n * individual event so an admin can see *which* queries ran and which was\n * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED\n * statement, bindings stripped — the query shape, never literal row values).\n */\nexport interface ServerTimingDetail {\n /** Event label — e.g. a parametrized SQL statement (no bindings). */\n label: string;\n /** Duration in milliseconds. */\n dur: number;\n}\n\n/**\n * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not\n * affected by wall-clock adjustments); falls back to `Date.now()` on the rare\n * runtime where `performance` is unavailable.\n */\nexport function perfNow(): number {\n try {\n return performance.now();\n } catch {\n return Date.now();\n }\n}\n\n/** Characters not allowed in a Server-Timing metric name (a token). */\nconst NAME_UNSAFE = /[^A-Za-z0-9_-]+/g;\n\nconst UNDERSCORE = 0x5f;\n\n/**\n * Coerce an arbitrary string into a Server-Timing token: non-token runs become\n * a single underscore, then leading/trailing underscores are trimmed.\n *\n * The trim is a linear scan rather than a `/^_+|_+$/g` regex on purpose - the\n * anchored `_+$` quantifier backtracks polynomially on underscore-heavy input\n * (CodeQL js/polynomial-redos), and this name comes from a public-API argument.\n */\nfunction sanitizeName(name: string): string {\n const collapsed = String(name).replace(NAME_UNSAFE, '_');\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed.charCodeAt(start) === UNDERSCORE) start++;\n while (end > start && collapsed.charCodeAt(end - 1) === UNDERSCORE) end--;\n return collapsed.slice(start, end);\n}\n\n/**\n * Make a description safe to embed in a quoted-string. Backslashes and double\n * quotes would terminate the quoting; control chars (incl. CR/LF) could forge\n * headers. Collapse anything outside a conservative printable set to a space.\n */\nfunction sanitizeDesc(desc: string): string {\n let out = '';\n for (const ch of String(desc)) {\n const code = ch.codePointAt(0)!;\n // Printable ASCII excluding `\"` (0x22) and `\\` (0x5C); drop the rest.\n if (code >= 0x20 && code < 0x7f && ch !== '\"' && ch !== '\\\\') {\n out += ch;\n } else {\n out += ' ';\n }\n }\n return out.replace(/ +/g, ' ').trim();\n}\n\n/** Round to at most 2 decimals without trailing-zero noise (`12.3`, not `12.30`). */\nfunction fmtDur(dur: number): string {\n if (!Number.isFinite(dur)) return '0';\n return String(Math.round(dur * 100) / 100);\n}\n\n/**\n * Serialize marks into a `Server-Timing` header value. Marks with an empty\n * name after sanitization are dropped (the grammar requires a token). Returns\n * `''` when there is nothing to emit so callers can skip the header.\n */\nexport function formatServerTiming(marks: readonly ServerTimingMark[]): string {\n const parts: string[] = [];\n for (const m of marks) {\n const name = sanitizeName(m.name);\n if (!name) continue;\n let part = `${name};dur=${fmtDur(m.dur)}`;\n if (m.desc) {\n const desc = sanitizeDesc(m.desc);\n if (desc) part += `;desc=\"${desc}\"`;\n }\n parts.push(part);\n }\n return parts.join(', ');\n}\n\n/**\n * Collector for one request's timing phases. Not thread-safe by design - one\n * instance belongs to one request. All methods are allocation-light and never\n * throw on the hot path.\n */\nexport class PerfTiming {\n private readonly _marks: ServerTimingMark[] = [];\n /**\n * Live aggregate marks by name (see {@link count}). Lazily created so a\n * request that never aggregates pays nothing. Each entry points at a mark\n * already inserted into {@link _marks}, mutated in place as events arrive.\n */\n private _aggregates?: Map<string, { mark: ServerTimingMark; count: number; unit?: string }>;\n /**\n * Per-event detail samples by category, populated only while detail capture\n * is on (see {@link enableDetail}). Lazily created so a request that never\n * enables detail pays nothing.\n */\n private _detail?: Map<string, ServerTimingDetail[]>;\n private _detailOn = false;\n /**\n * Hard cap on stored detail samples per category — detail is only ever on\n * for a deliberate debug request, but a pathological request must not pin\n * unbounded memory. The aggregate {@link count} still reflects the true\n * total; only the retained per-event list is bounded.\n */\n private static readonly DETAIL_CAP = 1000;\n\n /** Record an already-measured phase. */\n record(name: string, dur: number, desc?: string): void {\n this._marks.push({ name, dur, desc });\n }\n\n /**\n * Turn on per-event DETAIL capture for this request. Off by default so the\n * hot path never allocates a per-event list; the HTTP middleware enables it\n * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.\n */\n enableDetail(): void {\n this._detailOn = true;\n }\n\n /** Whether per-event detail capture is on. */\n get detailEnabled(): boolean {\n return this._detailOn;\n }\n\n /**\n * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op\n * unless {@link enableDetail} was called, so the hot-path call site (the SQL\n * driver's query listener) pays only a boolean check when detail is off.\n * Bounded by {@link DETAIL_CAP}; excess events still count toward the\n * aggregate via {@link count} but are not retained individually.\n */\n recordDetail(category: string, label: string, dur: number): void {\n if (!this._detailOn) return;\n const detail = (this._detail ??= new Map());\n let list = detail.get(category);\n if (!list) {\n list = [];\n detail.set(category, list);\n }\n if (list.length >= PerfTiming.DETAIL_CAP) return;\n list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });\n }\n\n /** Retained detail samples for `category`, in record order (empty when none). */\n details(category: string): readonly ServerTimingDetail[] {\n return this._detail?.get(category) ?? [];\n }\n\n /**\n * Begin timing a phase. Returns an idempotent `end()` - the first call\n * records the elapsed duration; later calls are ignored, so it is safe to\n * call from both a success and an error path.\n */\n start(name: string, desc?: string): () => void {\n const t0 = perfNow();\n let done = false;\n return () => {\n if (done) return;\n done = true;\n this.record(name, perfNow() - t0, desc);\n };\n }\n\n /** Time an async (or sync) function, recording its elapsed duration. */\n async measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T> {\n const end = this.start(name, desc);\n try {\n return await fn();\n } finally {\n end();\n }\n }\n\n /**\n * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call\n * adds `dur` to the running total for `name` and increments a counter; the\n * mark serializes as `name;dur=<sum>;desc=\"<count> <unit>\"` (or just the\n * bare count when no `unit` is given).\n *\n * Use this for high-frequency phases — one SQL query, one hook execution —\n * where recording a distinct mark per event would blow the header out to\n * hundreds of entries. The single `db;dur=210;desc=\"6 queries\"` member is\n * both the total DB time and the query count, which is the number most\n * useful for spotting N sequential round-trips.\n *\n * The aggregate mark is inserted into the record stream the first time its\n * name is seen, so it keeps its natural position relative to explicit marks\n * (e.g. before the outer `total`, which is recorded last).\n */\n count(name: string, dur: number, unit?: string): void {\n const add = Number.isFinite(dur) && dur > 0 ? dur : 0;\n const aggregates = (this._aggregates ??= new Map());\n let entry = aggregates.get(name);\n if (!entry) {\n const mark: ServerTimingMark = { name, dur: 0 };\n entry = { mark, count: 0, unit };\n aggregates.set(name, entry);\n this._marks.push(mark);\n }\n entry.count += 1;\n entry.mark.dur += add;\n if (unit) entry.unit = unit;\n entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);\n }\n\n /** Snapshot of recorded marks, in record order. */\n marks(): readonly ServerTimingMark[] {\n return this._marks;\n }\n\n /** Serialize to a `Server-Timing` header value (`''` when empty). */\n toHeader(): string {\n return formatServerTiming(this._marks);\n }\n}\n\n// --- Ambient (request-scoped) collector -------------------------------\n\n/**\n * The ambient collector lives in ONE process-wide `AsyncLocalStorage`, pinned\n * to a global-registry symbol rather than a plain module-level `const`.\n *\n * Why: this module is consumed from many packages and can legitimately be\n * loaded more than once in a single process — the ESM build (`dist/index.js`)\n * and the CJS build (`dist/index.cjs`) are distinct module instances, and a\n * bundler may inline yet another copy. A plain `const store` would give each\n * copy its OWN store, so a request scope opened through one copy (the HTTP\n * server's `runWithPerfTiming`) would be invisible to code reading the ambient\n * collector through another copy (the SQL driver, the ObjectQL engine) — the\n * cross-layer `db` / `auth` / `hooks` spans would silently never record.\n * `Symbol.for` resolves to the same symbol across every copy, so they all share\n * the one store.\n */\nconst STORE_KEY = Symbol.for('@objectstack/observability:perf-timing-store');\nconst globalStore = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfTiming> | undefined>;\nconst store: AsyncLocalStorage<PerfTiming> =\n globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage<PerfTiming>());\n\n/** Run `fn` with `timing` as the ambient collector for the async call chain. */\nexport function runWithPerfTiming<T>(timing: PerfTiming, fn: () => T): T {\n return store.run(timing, fn);\n}\n\n/** The collector for the current request, or `undefined` outside a request. */\nexport function currentPerfTiming(): PerfTiming | undefined {\n return store.getStore();\n}\n\n/** Record a phase on the ambient collector. No-op when none is active. */\nexport function recordServerTiming(name: string, dur: number, desc?: string): void {\n store.getStore()?.record(name, dur, desc);\n}\n\n/**\n * Begin timing a phase on the ambient collector. Returns an `end()` callback;\n * when no collector is active the returned callback is a no-op so call sites\n * stay branch-free.\n */\nexport function startServerTiming(name: string, desc?: string): () => void {\n const t = store.getStore();\n if (!t) return () => {};\n return t.start(name, desc);\n}\n\n/**\n * Time an async function on the ambient collector. When no collector is active\n * the function is awaited with zero timing overhead.\n */\nexport async function measureServerTiming<T>(\n name: string,\n fn: () => T | Promise<T>,\n desc?: string,\n): Promise<T> {\n const t = store.getStore();\n if (!t) return fn();\n return t.measure(name, fn, desc);\n}\n\n/**\n * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the\n * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is\n * active, so the hot-path call sites (the SQL driver's query listener, the hook\n * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.\n */\nexport function countServerTiming(name: string, dur: number, unit?: string): void {\n store.getStore()?.count(name, dur, unit);\n}\n\n/**\n * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto\n * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no\n * collector is active OR detail capture is off, so the hot-path call site pays\n * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.\n */\nexport function recordServerTimingDetail(category: string, label: string, dur: number): void {\n store.getStore()?.recordDetail(category, label, dur);\n}\n\n// --- Disclosure gate (WHO may see the timing) -------------------------\n\n/**\n * Per-request disclosure gate — the policy counterpart to the collector.\n *\n * The {@link PerfTiming} collector only MEASURES; whether the measured\n * `Server-Timing` header is returned to the client is a separate decision. When\n * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has\n * opted the whole environment in, so the gate opens up front and every response\n * carries the header. When it is turned on PER-REQUEST — the caller sends an\n * `X-OS-Debug-Timing` header — the header must stay withheld until the request\n * proves an admin/service identity: phase durations are a mild\n * backend-fingerprinting surface, so an ordinary user must never be able to pull\n * them just by sending a header. The request path flips the gate open with\n * {@link allowPerfDisclosure} once it has resolved a privileged principal.\n *\n * Keeping this out of {@link PerfTiming} preserves the collector's invariant\n * (\"it only measures, it never decides whether to emit\").\n *\n * Two levels, because global mode discloses the basic header to everyone but the\n * richer per-query detail must stay admin-only:\n * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by\n * global mode for everyone, or by a proven admin per-request).\n * - `privileged` — the principal is a proven admin/service. Gates the richer,\n * SQL-shape-bearing detail payload, which must NEVER reach an\n * ordinary caller even when global mode is on.\n */\nexport interface PerfDisclosureGate {\n /** Whether the basic collected timing may be disclosed to the client. */\n allowed: boolean;\n /**\n * Whether the principal is a proven admin/service — gates the richer detail\n * payload independently of `allowed`. Absent = not privileged.\n */\n privileged?: boolean;\n}\n\n/**\n * The disclosure gate lives in its OWN global-registry-pinned\n * `AsyncLocalStorage`, for the same cross-module-copy reason as the collector\n * store above: the middleware seeds the gate and the dispatcher (a different\n * package, possibly a different module copy) flips it open — both must see the\n * one store.\n */\nconst GATE_KEY = Symbol.for('@objectstack/observability:perf-disclosure-gate');\nconst globalGate = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfDisclosureGate> | undefined>;\nconst gateStore: AsyncLocalStorage<PerfDisclosureGate> =\n globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage<PerfDisclosureGate>());\n\n/**\n * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.\n * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`\n * settles to decide whether to emit the header.\n */\nexport function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T {\n return gateStore.run(gate, fn);\n}\n\n/**\n * Open the ambient disclosure gate — the request has proven an admin/service\n * identity, so it may see its own `Server-Timing` header AND the richer detail\n * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A\n * no-op when no gate is active (perf-tuning off), so the call site stays\n * branch-free.\n */\nexport function allowPerfDisclosure(): void {\n const g = gateStore.getStore();\n if (g) {\n g.allowed = true;\n g.privileged = true;\n }\n}\n\n/** Whether the ambient disclosure gate is open. `false` when none is active. */\nexport function isPerfDisclosureAllowed(): boolean {\n return gateStore.getStore()?.allowed ?? false;\n}\n\n/**\n * Whether the ambient principal is a proven admin/service — gates the richer\n * detail payload. `false` when no gate is active or only global-mode disclosure\n * (not a proven admin) opened it.\n */\nexport function isPerfDisclosurePrivileged(): boolean {\n return gateStore.getStore()?.privileged ?? false;\n}\n\n/**\n * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header\n * (#2408 perf-tuning gating). The header exposes internal phase durations — a\n * mild backend-fingerprinting surface — so when timing is opened per-request via\n * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:\n *\n * - `isSystem` — internal/engine self-calls,\n * - `principalKind` `service` / `system` — service tokens & the system seed,\n * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.\n *\n * Ordinary human/guest/agent callers get `false`, so sending the debug header\n * yields no header for them. Global (env/option) perf mode bypasses this — it\n * opened the disclosure gate up front for the whole environment.\n *\n * This is the ONE definition of \"who may pull per-request timings\", shared by\n * every HTTP entry point that resolves a principal — the runtime dispatcher\n * (`timedResolveExecutionContext`), the REST server, and the standalone Hono\n * CRUD surface — so a new admin-serving path can never silently under- or\n * over-disclose by hand-rolling its own rule (#3361).\n */\nexport function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean {\n if (!ec) return false;\n if (ec.isSystem === true) return true;\n if (ec.principalKind === 'service' || ec.principalKind === 'system') return true;\n return ec.posture === 'PLATFORM_ADMIN' || ec.posture === 'TENANT_ADMIN';\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/service-names.ts","../src/semconv.ts","../src/http-transport-metrics.ts","../src/metrics-exporters.ts","../src/error-exporters.ts","../src/loggers.ts","../src/perf-timing.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/observability` — vendor-neutral contracts and exporters\n * for ObjectStack metrics, errors, and logs.\n *\n * @see {@link MetricsRegistry} {@link ErrorReporter} {@link Logger}\n */\n\n// Contracts\nexport type { MetricsRegistry, MetricSample, ErrorReporter, CapturedError, Logger } from './contracts.js';\n\n// Service-registry names (consumed by runtime's ObservabilityServicePlugin and lookup sites)\nexport { OBSERVABILITY_METRICS_SERVICE, OBSERVABILITY_ERRORS_SERVICE } from './service-names.js';\n\n// Semantic conventions\nexport { SEMCONV, RUNTIME_METRICS } from './semconv.js';\n\n// Transport-agnostic HTTP metrics, each family armed at most once per server\n// via the `IHttpServer.afterResponse` observation seam (#9835 counter, #9834\n// duration histogram)\nexport {\n armHttpRequestCounter,\n armHttpRequestDurationHistogram,\n type ArmHttpMetricResult,\n type ArmHttpRequestCounterResult,\n} from './http-transport-metrics.js';\n\n// Metric exporters\nexport {\n NoopMetricsRegistry,\n InMemoryMetricsRegistry,\n ConsoleMetricsRegistry,\n OtlpHttpMetricsRegistry,\n type OtlpHttpExporterOptions,\n} from './metrics-exporters.js';\n\n// Error reporters\nexport {\n NoopErrorReporter,\n InMemoryErrorReporter,\n ConsoleErrorReporter,\n} from './error-exporters.js';\n\n// Loggers\nexport {\n NoopLogger,\n ConsoleLogger,\n JsonLogger,\n LOG_LEVELS,\n type LogLevel,\n} from './loggers.js';\n\n// Per-request performance timing (Server-Timing header)\nexport {\n PerfTiming,\n perfNow,\n formatServerTiming,\n runWithPerfTiming,\n currentPerfTiming,\n recordServerTiming,\n startServerTiming,\n measureServerTiming,\n countServerTiming,\n recordServerTimingDetail,\n runWithPerfDisclosure,\n allowPerfDisclosure,\n isPerfDisclosureAllowed,\n isPerfDisclosurePrivileged,\n isPerfDisclosurePrincipal,\n type ServerTimingMark,\n type ServerTimingDetail,\n type PerfDisclosureGate,\n} from './perf-timing.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical service-registry names for the host's observability\n * backends. Plugins look these up to discover the configured\n * {@link MetricsRegistry} / {@link ErrorReporter} without each host\n * having to thread observability config through every plugin\n * constructor.\n *\n * See `@objectstack/runtime` → `ObservabilityServicePlugin` for the\n * registration side.\n */\nexport const OBSERVABILITY_METRICS_SERVICE = 'observability:metrics';\nexport const OBSERVABILITY_ERRORS_SERVICE = 'observability:errors';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Semantic conventions — canonical metric names emitted by the\n * framework. Listed here so hosts can wire alerts/dashboards against\n * a stable namespace and so call sites don't sprinkle string\n * literals through the code base.\n *\n * Naming follows Prometheus conventions:\n *\n * - snake_case identifiers.\n * - `_total` suffix for monotonic counters.\n * - `_ms`, `_seconds`, `_bytes` suffixes for histograms / gauges\n * with units.\n *\n * Groups roughly mirror the framework subsystems that emit them.\n * Cloud-specific metrics (DO restarts, Workers Analytics Engine\n * writes, …) do NOT belong here — they are deployment-specific and\n * stay in the deployment repo.\n */\nexport const SEMCONV = {\n // ── HTTP — emitter differs per family, see each ────────────────────\n /**\n * Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT\n * through the `IHttpServer.afterResponse` seam (#9835), so it covers\n * every inbound request on the server rather than only the routes the\n * runtime dispatcher registers.\n */\n httpRequestsTotal: 'http_requests_total',\n /**\n * Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT\n * through the same seam (#9834). It measures the REQUEST as the transport\n * sees it — first sight to the response existing, middleware chain and\n * body parse included — not the handler's share of it.\n */\n httpRequestDurationMs: 'http_request_duration_ms',\n // ⛔ RETIRED — `http_request_errors_total` was removed in\n // `@objectstack/observability` 17.2.0 (#9834, ADR-0049 enforce-or-remove).\n // ⛔ Do not re-add the name. It was DECLARED here as a stable server-wide\n // signal and EMITTED only from `@objectstack/runtime`'s per-route wrapper,\n // on a THROWN handler — so it never saw auth's `getRawApp()` mount, the\n // REST data API, or any error a handler answered politely through\n // `errorResponseBase` (which sets a status and does not re-throw). No\n // transport-side emitter could preserve that population either: the\n // `IHttpServer.afterResponse` observation carries `{method, routePattern,\n // status, elapsedMs}` and no throw signal at all.\n // ⇒ Read the 5xx rate from `http_requests_total{status=~\"5..\"}` instead.\n // The transport emits that family through the seam, so it covers every\n // inbound surface (#9650 / #9835 / #10004) and carries the status label\n // this counter only stood in for. Maintainer ruling 2026-08-20.\n\n // ── Storage — emitted by `@objectstack/service-storage` adapters ──\n /** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */\n storageOperationsTotal: 'storage_operations_total',\n /** Histogram (ms), labels: `adapter`, `op`. */\n storageOperationDurationMs: 'storage_operation_duration_ms',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n storageErrorsTotal: 'storage_errors_total',\n\n // ── Cache — emitted by `@objectstack/service-cache` adapters ──\n // Uniform emitter; what varies is who CONSULTS the service — see below.\n /**\n * Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).\n *\n * ⚠️ A flat zero means \"NO CONFIGURED CONSUMER\", not \"no cache activity\",\n * and — unlike the HTTP families above — it is NOT an instrumentation gap.\n * The adapters hold the host's registry and count every call they receive\n * (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails\n * to communicate is WHY.\n *\n * The why: nothing consults the `cache` service unconditionally. Every\n * production consumer is a rate-limit / budget counter store, and each is\n * gated on a declaration somebody has to write — better-auth's per-IP\n * counters (`rate_limit_max` / `rate_limit_window_seconds` in auth\n * settings), the dispatcher's inbound limiter and its declarative\n * per-endpoint buckets (an armed `rateLimit` budget; with none declared\n * the dispatcher registers no limiter at all), and the per-number OTP send\n * budget (an SMS send path). A default install declares none of them, so\n * this counter stays at 0 while the server handles traffic normally.\n *\n * ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%\n * hit rate or a broken adapter. Before trusting a cache hit-rate panel,\n * confirm at least one consumer above is actually armed.\n */\n cacheLookupsTotal: 'cache_lookups_total',\n /**\n * Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same\n * \"zero = no configured consumer\" reading as `cacheLookupsTotal` above.\n */\n cacheWritesTotal: 'cache_writes_total',\n /**\n * Counter, labels: `adapter`, `op`, `errorClass`. Same \"zero = no\n * configured consumer\" reading as `cacheLookupsTotal` above — a zero is\n * \"nothing was asked of the cache\", not \"every call succeeded\".\n */\n cacheErrorsTotal: 'cache_errors_total',\n\n // ── Background jobs — emitted by `@objectstack/runtime`'s AppPlugin ──\n /**\n * Counter, labels: `app`, `job`. Incremented when a DECLARED background\n * job could not be handed to the job service — i.e. the app booted green\n * but that job will never run (#4567). Any non-zero value is an outage of\n * the job, not a warning.\n */\n jobScheduleFailuresTotal: 'job_schedule_failures_total',\n\n // ── Package / registry-reader — emitted by `@objectstack/service-package` ──\n /** Counter, labels: `result` (`ok`|`miss`|`error`). */\n registryLookupsTotal: 'registry_lookups_total',\n /** Histogram (ms). */\n registryLookupDurationMs: 'registry_lookup_duration_ms',\n /** Counter, labels: `source` (`r2`|`http`|`local`), `result` (`hit`|`miss`|`error`). */\n registrySourceFetchesTotal: 'registry_source_fetches_total',\n} as const;\n\n/**\n * Backwards-compat alias. `RUNTIME_METRICS` was the original (HTTP-only)\n * constant name shipped from `@objectstack/runtime`; we keep it here so\n * existing code reading `RUNTIME_METRICS.httpRequestsTotal` continues\n * to work after the constants moved into this package.\n */\nexport const RUNTIME_METRICS = {\n httpRequestsTotal: SEMCONV.httpRequestsTotal,\n httpRequestDurationMs: SEMCONV.httpRequestDurationMs,\n // `httpRequestErrorsTotal` retired with its SEMCONV declaration above\n // (#9834). The alias is not a compatibility window of its own: there is no\n // emitter left to read, so keeping the name here would hand callers a\n // string nothing ever writes.\n} as const;\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IHttpServer } from '@objectstack/spec/contracts';\nimport type { MetricsRegistry } from './contracts.js';\nimport { RUNTIME_METRICS } from './semconv.js';\n\n/**\n * What an `arm*` call in this module did:\n *\n * - `'armed'` — the emitting observer was registered on this call.\n * - `'already-armed'` — some earlier caller already armed this server for\n * THIS metric family; the seam emits it, and this call registered nothing\n * (first-wins).\n * - `'unsupported'` — the transport does not implement the\n * `IHttpServer.afterResponse` seam; it reports NO HTTP metrics (#9835:\n * zero there means \"not instrumented\", never \"no traffic\"), and the\n * caller must decide how to degrade — the runtime dispatcher falls back\n * to instrumenting its own routes.\n */\nexport type ArmHttpMetricResult = 'armed' | 'already-armed' | 'unsupported';\n\n/**\n * The name {@link armHttpRequestCounter} shipped with. Kept as an alias\n * rather than renamed: the export is already in the pending release, and the\n * two families arm through separate entry points anyway.\n */\nexport type ArmHttpRequestCounterResult = ArmHttpMetricResult;\n\n/**\n * The per-server latch behind the contract's ownership rule. A registered\n * global-registry symbol (`Symbol.for`), not a module-level WeakSet, so the\n * latch cannot fork if two copies of this module ever coexist in one\n * process — the whole point is that there is exactly ONE latch per server\n * object, whoever asks.\n */\nconst HTTP_REQUEST_COUNTER_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestCounterArmed',\n);\n\n/**\n * Arm `http_requests_total{method,route,status}` on a transport through the\n * `IHttpServer.afterResponse` observation seam (#9835) — AT MOST ONCE per\n * server, whoever calls first.\n *\n * ## Why arming is centralized here\n *\n * The contract's ownership rule says a request must never be double-counted,\n * and two composition layers legitimately hold both a server and a metrics\n * registry: the transport's own hosting plugin (`HonoServerPlugin`, which\n * the 2026-08-18 ruling on #9650 made the counter's home) and the runtime\n * dispatcher (whose `observability.metrics` config is the wiring the docs\n * demonstrate). When a host hands ONE registry to both — the ordinary case —\n * two independently-registered observers would land every request on the\n * same series twice: exactly the #9833 distortion, rebuilt one seam over.\n * Routing every arming through this function makes \"exactly one\n * counter-emitting observer per server\" structural: the first caller arms\n * (in the shipped composition that is the transport plugin, in Phase 1),\n * every later caller is told the seam already counts.\n *\n * The label shape is pinned by the contract: `route` is the transport's\n * `routePattern` — the registered PATTERN, never the concrete path — and\n * `status` is stringified for the label set. Emission goes through the\n * transport's observer-isolation guarantee, so a throwing registry cannot\n * break a response.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper: the latch is per object identity, and a wrapper\n * would both fork the latch and (per #5122) risk erasing the optional\n * member this function feature-detects.\n * @param metrics - The registry the counter lands in. First caller wins; a\n * second registry offered later is NOT added (the contract's one-owner\n * rule), and the result says so.\n */\nexport function armHttpRequestCounter(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_COUNTER_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_COUNTER_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_COUNTER_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {\n method: observation.method,\n route: observation.routePattern,\n status: String(observation.status),\n });\n });\n return 'armed';\n}\n\n/**\n * The duration family's own latch. A SEPARATE registered symbol from the\n * counter's, deliberately: the two families are armed through separate entry\n * points and gated separately on the dispatcher's per-route wrapper, so a\n * host that arms one must not silently latch the other away.\n */\nconst HTTP_REQUEST_DURATION_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestDurationArmed',\n);\n\n/**\n * Arm `http_request_duration_ms{method,route}` on a transport through the\n * `IHttpServer.afterResponse` observation seam — AT MOST ONCE per server,\n * whoever calls first. The duration half of #9834, built on the mechanism\n * #9835 proved out for `http_requests_total`.\n *\n * ## Why the histogram has to move too\n *\n * #9835 moved only the counter, which left the docs' two derived signals\n * inconsistent with each other: 5xx rate saw every inbound surface while p95\n * latency still saw the dispatcher's own routes. An operator reading one\n * dashboard got request volume for `/api/v1/*` beside a latency panel with no\n * series for it — and the worse reading is the p95 that IS drawn, computed\n * from dispatcher routes only and presented as the server's.\n *\n * ## ⚠️ The observation WINDOW changes with the emitter\n *\n * The dispatcher's per-route wrapper timed `await handler(req, res)` — handler\n * latency. This seam times the transport's own `use('*')` around\n * `await next()`, which is what {@link HttpResponseObservation.elapsedMs}\n * means: \"from the transport first seeing the request to the response\n * existing\". That includes the middleware chain and body parse, so the series\n * can only move UP, never down. It is the number an operator's latency panel\n * should have been showing — the request's latency rather than one layer's\n * share of it — but it is a visible change in an existing series, so it is\n * stated here, in the changeset, and in `docs/OBSERVABILITY.md` rather than\n * left for a dashboard to discover.\n *\n * The label set is unchanged and stays the SEMCONV-declared `{method,route}`:\n * `route` is the transport's `routePattern` (the registered PATTERN, never the\n * concrete path), and no `status` label is added — a histogram split by status\n * is a different series shape than the one the docs tell operators to graph.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper — the latch is per object identity, and (per\n * #5122) a wrapper risks erasing the optional member this feature-detects.\n * @param metrics - The registry the histogram lands in. First caller wins.\n */\nexport function armHttpRequestDurationHistogram(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_DURATION_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_DURATION_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_DURATION_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.histogram(\n RUNTIME_METRICS.httpRequestDurationMs,\n observation.elapsedMs,\n { method: observation.method, route: observation.routePattern },\n );\n });\n return 'armed';\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { MetricsRegistry, MetricSample } from './contracts.js';\n\n// ─── Noop ─────────────────────────────────────────────────────────────\n\n/**\n * No-op metrics registry — the default. Discards every observation.\n * Production deployments should swap this for a real registry; tests\n * can use {@link InMemoryMetricsRegistry} to assert emissions.\n */\nexport class NoopMetricsRegistry implements MetricsRegistry {\n counter(): void { }\n histogram(): void { }\n gauge(): void { }\n}\n\n// ─── In-memory (tests + dev inspection) ───────────────────────────────\n\n/**\n * In-memory registry used for tests and local inspection. Stores\n * every observation in insertion order; query via the helpers below\n * or read {@link samples} directly.\n *\n * Not intended for production — unbounded growth.\n */\nexport class InMemoryMetricsRegistry implements MetricsRegistry {\n readonly samples: MetricSample[] = [];\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.samples.push({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n /** Sum of counter increments matching `name` and optionally a label subset. */\n totalCounter(name: string, labelMatch: Record<string, string> = {}): number {\n return this.samples\n .filter(s => s.kind === 'counter' && s.name === name && matchesLabels(s.labels, labelMatch))\n .reduce((acc, s) => acc + s.value, 0);\n }\n\n /** Raw histogram observations matching `name` and optionally a label subset. */\n histogramValues(name: string, labelMatch: Record<string, string> = {}): number[] {\n return this.samples\n .filter(s => s.kind === 'histogram' && s.name === name && matchesLabels(s.labels, labelMatch))\n .map(s => s.value);\n }\n\n /** Last gauge value matching `name` and optionally a label subset, or undefined. */\n lastGauge(name: string, labelMatch: Record<string, string> = {}): number | undefined {\n for (let i = this.samples.length - 1; i >= 0; i--) {\n const s = this.samples[i];\n if (s.kind === 'gauge' && s.name === name && matchesLabels(s.labels, labelMatch)) {\n return s.value;\n }\n }\n return undefined;\n }\n\n /** Clear all recorded samples. */\n reset(): void {\n this.samples.length = 0;\n }\n}\n\nfunction matchesLabels(actual: Record<string, string>, expected: Record<string, string>): boolean {\n for (const [k, v] of Object.entries(expected)) {\n if (actual[k] !== v) return false;\n }\n return true;\n}\n\n// ─── Console (development) ────────────────────────────────────────────\n\n/**\n * Console metrics registry — prints one line per observation. Useful\n * during local development to confirm that instrumentation is firing.\n *\n * Not intended for production: writing every observation to stdout\n * defeats Prometheus / OTLP pipelines and dominates request latency.\n */\nexport class ConsoleMetricsRegistry implements MetricsRegistry {\n constructor(private readonly opts: { sink?: (line: string) => void; prefix?: string } = {}) { }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.emit('counter', name, value, labels);\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('histogram', name, value, labels);\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('gauge', name, value, labels);\n }\n\n private emit(kind: string, name: string, value: number, labels: Record<string, string>): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.log(s); });\n const prefix = this.opts.prefix ?? '[metric]';\n const labelStr = Object.entries(labels)\n .map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n .join(' ');\n sink(`${prefix} ${kind} ${name} ${value}${labelStr ? ' ' + labelStr : ''}`);\n } catch {\n // Per contract: never throw from a metric call site.\n }\n }\n}\n\n// ─── OTLP / HTTP (Prometheus via OTel Collector, Grafana Cloud, …) ────\n\n/**\n * Configuration for {@link OtlpHttpMetricsRegistry}.\n */\nexport interface OtlpHttpExporterOptions {\n /**\n * OTLP/HTTP metrics endpoint, e.g. `http://otel-collector:4318/v1/metrics`.\n * The path is appended automatically if missing.\n */\n endpoint: string;\n\n /** Optional headers (Authorization, x-tenant, …). */\n headers?: Record<string, string>;\n\n /**\n * Resource attributes — `service.name`, `service.namespace`,\n * `deployment.environment`, etc. Merged into the OTLP `resource`\n * block on every export.\n */\n resource?: Record<string, string>;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`.\n * Allows Workers / undici / node-fetch substitution and test\n * doubles.\n */\n fetch?: typeof fetch;\n\n /**\n * Called when an export attempt fails. Default: silently swallow\n * (per the contract that metric emission must not throw / log\n * loudly on the hot path).\n */\n onError?: (error: unknown) => void;\n\n /**\n * Maximum number of samples buffered before {@link OtlpHttpMetricsRegistry.flush}\n * is called automatically. Defaults to 1024.\n */\n maxBufferSize?: number;\n}\n\n/**\n * OTLP/HTTP metrics exporter.\n *\n * Buffers samples in memory and serialises them to the OpenTelemetry\n * Protocol JSON encoding when {@link flush} is called (manually or\n * automatically once the buffer hits the configured size).\n *\n * Intentionally does **not** start an interval timer in the constructor:\n * (a) it makes the exporter usable on Cloudflare Workers where\n * `setInterval` is restricted, and (b) it keeps unit tests deterministic.\n * Long-running hosts should call `flush()` on a schedule\n * (e.g. `setInterval(() => reg.flush(), 10_000)` on Node, or\n * `ctx.waitUntil(reg.flush())` from a Workers fetch handler).\n *\n * Only counters, histograms, and gauges are emitted — no support for\n * exemplars or aggregation temporality switches (the Collector handles\n * those on the upstream side).\n */\nexport class OtlpHttpMetricsRegistry implements MetricsRegistry {\n private buffer: MetricSample[] = [];\n private readonly endpoint: string;\n private readonly headers: Record<string, string>;\n private readonly resource: Record<string, string>;\n private readonly maxBufferSize: number;\n private readonly fetchImpl: typeof fetch;\n private readonly onError: (error: unknown) => void;\n\n constructor(options: OtlpHttpExporterOptions) {\n this.endpoint = normaliseEndpoint(options.endpoint);\n this.headers = options.headers ?? {};\n this.resource = options.resource ?? {};\n this.maxBufferSize = options.maxBufferSize ?? 1024;\n this.fetchImpl = options.fetch ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : noFetch);\n this.onError = options.onError ?? (() => { });\n }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.record({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n private record(sample: MetricSample): void {\n this.buffer.push(sample);\n if (this.buffer.length >= this.maxBufferSize) {\n // Fire-and-forget: do not block the call site.\n void this.flush().catch(this.onError);\n }\n }\n\n /** Snapshot the current buffer (for tests). */\n peek(): readonly MetricSample[] {\n return this.buffer.slice();\n }\n\n /**\n * Send the buffered samples to the OTLP endpoint and clear the\n * buffer. Safe to call concurrently — each invocation takes a\n * snapshot before clearing.\n */\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n const samples = this.buffer;\n this.buffer = [];\n try {\n const body = JSON.stringify(serialiseToOtlp(samples, this.resource));\n const res = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...this.headers },\n body,\n });\n if (!res.ok) {\n this.onError(new Error(`OTLP export returned HTTP ${res.status}`));\n }\n } catch (err) {\n this.onError(err);\n }\n }\n}\n\nfunction normaliseEndpoint(raw: string): string {\n const trimmed = raw.replace(/\\/+$/, '');\n if (/\\/v1\\/metrics$/.test(trimmed)) return trimmed;\n return trimmed + '/v1/metrics';\n}\n\nfunction noFetch(): never {\n throw new Error('OtlpHttpMetricsRegistry: no global fetch available; pass options.fetch');\n}\n\n/**\n * Minimal OTLP/JSON metrics serialiser.\n *\n * Implements the subset of <https://opentelemetry.io/docs/specs/otlp/>\n * that we actually emit — sums for counters, gauges for gauges,\n * histograms encoded as bucketless \"summary\"-style histograms with\n * explicit per-sample data points. The Collector accepts this shape\n * and applies its own bucketing.\n *\n * Aggregation temporality is set to DELTA (2) because every flush\n * sends only the samples accumulated since the last flush.\n */\nfunction serialiseToOtlp(samples: MetricSample[], resource: Record<string, string>): unknown {\n const byName = new Map<string, { kind: MetricSample['kind']; points: MetricSample[] }>();\n for (const s of samples) {\n const existing = byName.get(s.name);\n if (existing) existing.points.push(s);\n else byName.set(s.name, { kind: s.kind, points: [s] });\n }\n\n const metrics = Array.from(byName.entries()).map(([name, { kind, points }]) => {\n if (kind === 'counter') {\n return {\n name,\n sum: {\n dataPoints: points.map(p => toNumberPoint(p)),\n aggregationTemporality: 2,\n isMonotonic: true,\n },\n };\n }\n if (kind === 'gauge') {\n return { name, gauge: { dataPoints: points.map(p => toNumberPoint(p)) } };\n }\n // histogram: emit a histogram with a single bucket boundary so the\n // Collector treats it as a recorded distribution.\n return {\n name,\n histogram: {\n aggregationTemporality: 2,\n dataPoints: points.map(p => ({\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n count: '1',\n sum: p.value,\n bucketCounts: ['0', '1'],\n explicitBounds: [p.value],\n })),\n },\n };\n });\n\n return {\n resourceMetrics: [{\n resource: { attributes: toAttributes(resource) },\n scopeMetrics: [{\n scope: { name: '@objectstack/observability', version: '0.1.0' },\n metrics,\n }],\n }],\n };\n}\n\nfunction toNumberPoint(p: MetricSample): unknown {\n return {\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n asDouble: p.value,\n };\n}\n\nfunction toAttributes(labels: Record<string, string>): unknown[] {\n return Object.entries(labels).map(([key, value]) => ({\n key,\n value: { stringValue: value },\n }));\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ErrorReporter, CapturedError } from './contracts.js';\n\n/** No-op reporter — the default. */\nexport class NoopErrorReporter implements ErrorReporter {\n captureException(): void { }\n}\n\n/** In-memory reporter for tests. */\nexport class InMemoryErrorReporter implements ErrorReporter {\n readonly captured: CapturedError[] = [];\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n this.captured.push({ error, context, at: Date.now() });\n }\n\n reset(): void {\n this.captured.length = 0;\n }\n}\n\n/**\n * Console error reporter — writes a structured JSON line to stderr per\n * captured exception. Convenient default for local development and for\n * any deployment that ships stderr to a log aggregator (e.g. Loki,\n * fluent-bit) but does not have a dedicated APM.\n *\n * Stack traces are included when the captured value is an `Error`.\n */\nexport class ConsoleErrorReporter implements ErrorReporter {\n constructor(private readonly opts: { sink?: (line: string) => void } = {}) { }\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.error(s); });\n const record: Record<string, unknown> = {\n ts: new Date().toISOString(),\n level: 'error',\n msg: error instanceof Error ? error.message : String(error),\n context,\n };\n if (error instanceof Error && error.stack) {\n record.stack = error.stack;\n }\n sink(JSON.stringify(record));\n } catch {\n // Per contract: error reporting must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Logger } from './contracts.js';\n\n/** Recognised log levels in increasing severity order. */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n fatal: 50,\n};\n\n/** No-op logger — discards every message. */\nexport class NoopLogger implements Logger {\n debug(): void { }\n info(): void { }\n warn(): void { }\n error(): void { }\n fatal(): void { }\n child(): Logger { return this; }\n}\n\n/**\n * Console logger — pretty-printed messages for local development.\n *\n * Not suitable for production where structured JSON is required;\n * use {@link JsonLogger} there instead.\n */\nexport class ConsoleLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n } = {},\n ) { }\n\n private get threshold(): number {\n return LEVEL_PRIORITY[this.opts.level ?? 'info'];\n }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> {\n return this.opts.context ?? {};\n }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new ConsoleLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const merged = { ...this.context, ...(meta ?? {}) };\n const tail = Object.keys(merged).length ? ' ' + JSON.stringify(merged) : '';\n const line = `[${level}] ${msg}${tail}`;\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n\n/**\n * JSON logger — one JSON object per line on stdout (errors on stderr).\n *\n * Matches the shape that Loki / fluent-bit / Cloudflare Logpush ingest\n * by default. Every record contains `ts`, `level`, `msg`, and any\n * accumulated child-context plus per-call `meta`.\n *\n * Use this in production. Use {@link ConsoleLogger} during development\n * for human-friendly output.\n */\nexport class JsonLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n /** Optional fields injected into every record (`service`, `env`, …). */\n base?: Record<string, unknown>;\n /** Wall clock for tests. */\n now?: () => Date;\n } = {},\n ) { }\n\n private get threshold(): number { return LEVEL_PRIORITY[this.opts.level ?? 'info']; }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> { return this.opts.context ?? {}; }\n private get base(): Record<string, unknown> { return this.opts.base ?? {}; }\n private get now(): () => Date { return this.opts.now ?? (() => new Date()); }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new JsonLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const record = {\n ts: this.now().toISOString(),\n level,\n msg,\n ...this.base,\n ...this.context,\n ...(meta ?? {}),\n };\n const line = JSON.stringify(record);\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Per-request performance timing - a tiny, dependency-free collector that\n * accumulates named phase durations during a single request and serializes\n * them into the W3C `Server-Timing` response header.\n *\n * @see <https://www.w3.org/TR/server-timing/>\n * @see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>\n *\n * Two ways to record:\n *\n * 1. **Explicit collector.** Hold a {@link PerfTiming} instance and call\n * `start()` / `record()` / `measure()` directly. The HTTP adapter owns\n * the instance for a request.\n *\n * 2. **Ambient collector.** Run a request inside {@link runWithPerfTiming}\n * and any framework code on that async call chain records phases via the\n * free functions ({@link measureServerTiming}, {@link startServerTiming},\n * {@link recordServerTiming}, {@link countServerTiming}) without threading\n * the request object through every layer. When no collector is active the\n * free functions are cheap no-ops, so call sites pay nothing when the\n * feature is off. High-frequency phases (per SQL query, per hook) use\n * {@link countServerTiming} to fold into one aggregate mark carrying a\n * total duration and an event count.\n *\n * `Server-Timing` exposes internal phase durations to any client, which is a\n * (mild) information-disclosure surface - it helps an attacker profile the\n * backend. Emission is therefore opt-in (\"perf-tuning mode\"); the collector\n * itself never decides whether to emit, it only measures.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * One recorded phase of a request's server-side processing, serialized as a\n * single member of the `Server-Timing` header.\n */\nexport interface ServerTimingMark {\n /** Metric name. Coerced to a Server-Timing token on record. */\n name: string;\n /** Duration in milliseconds. */\n dur: number;\n /** Optional human-readable description (rendered as the quoted `desc`). */\n desc?: string;\n}\n\n/**\n * One recorded sub-event when DETAIL capture is on (see\n * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds\n * every query into a single count+duration — a detail sample keeps the\n * individual event so an admin can see *which* queries ran and which was\n * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED\n * statement, bindings stripped — the query shape, never literal row values).\n */\nexport interface ServerTimingDetail {\n /** Event label — e.g. a parametrized SQL statement (no bindings). */\n label: string;\n /** Duration in milliseconds. */\n dur: number;\n}\n\n/**\n * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not\n * affected by wall-clock adjustments); falls back to `Date.now()` on the rare\n * runtime where `performance` is unavailable.\n */\nexport function perfNow(): number {\n try {\n return performance.now();\n } catch {\n return Date.now();\n }\n}\n\n/** Characters not allowed in a Server-Timing metric name (a token). */\nconst NAME_UNSAFE = /[^A-Za-z0-9_-]+/g;\n\nconst UNDERSCORE = 0x5f;\n\n/**\n * Coerce an arbitrary string into a Server-Timing token: non-token runs become\n * a single underscore, then leading/trailing underscores are trimmed.\n *\n * The trim is a linear scan rather than a `/^_+|_+$/g` regex on purpose - the\n * anchored `_+$` quantifier backtracks polynomially on underscore-heavy input\n * (CodeQL js/polynomial-redos), and this name comes from a public-API argument.\n */\nfunction sanitizeName(name: string): string {\n const collapsed = String(name).replace(NAME_UNSAFE, '_');\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed.charCodeAt(start) === UNDERSCORE) start++;\n while (end > start && collapsed.charCodeAt(end - 1) === UNDERSCORE) end--;\n return collapsed.slice(start, end);\n}\n\n/**\n * Make a description safe to embed in a quoted-string. Backslashes and double\n * quotes would terminate the quoting; control chars (incl. CR/LF) could forge\n * headers. Collapse anything outside a conservative printable set to a space.\n */\nfunction sanitizeDesc(desc: string): string {\n let out = '';\n for (const ch of String(desc)) {\n const code = ch.codePointAt(0)!;\n // Printable ASCII excluding `\"` (0x22) and `\\` (0x5C); drop the rest.\n if (code >= 0x20 && code < 0x7f && ch !== '\"' && ch !== '\\\\') {\n out += ch;\n } else {\n out += ' ';\n }\n }\n return out.replace(/ +/g, ' ').trim();\n}\n\n/** Round to at most 2 decimals without trailing-zero noise (`12.3`, not `12.30`). */\nfunction fmtDur(dur: number): string {\n if (!Number.isFinite(dur)) return '0';\n return String(Math.round(dur * 100) / 100);\n}\n\n/**\n * Serialize marks into a `Server-Timing` header value. Marks with an empty\n * name after sanitization are dropped (the grammar requires a token). Returns\n * `''` when there is nothing to emit so callers can skip the header.\n */\nexport function formatServerTiming(marks: readonly ServerTimingMark[]): string {\n const parts: string[] = [];\n for (const m of marks) {\n const name = sanitizeName(m.name);\n if (!name) continue;\n let part = `${name};dur=${fmtDur(m.dur)}`;\n if (m.desc) {\n const desc = sanitizeDesc(m.desc);\n if (desc) part += `;desc=\"${desc}\"`;\n }\n parts.push(part);\n }\n return parts.join(', ');\n}\n\n/**\n * Collector for one request's timing phases. Not thread-safe by design - one\n * instance belongs to one request. All methods are allocation-light and never\n * throw on the hot path.\n */\nexport class PerfTiming {\n private readonly _marks: ServerTimingMark[] = [];\n /**\n * Live aggregate marks by name (see {@link count}). Lazily created so a\n * request that never aggregates pays nothing. Each entry points at a mark\n * already inserted into {@link _marks}, mutated in place as events arrive.\n */\n private _aggregates?: Map<string, { mark: ServerTimingMark; count: number; unit?: string }>;\n /**\n * Per-event detail samples by category, populated only while detail capture\n * is on (see {@link enableDetail}). Lazily created so a request that never\n * enables detail pays nothing.\n */\n private _detail?: Map<string, ServerTimingDetail[]>;\n private _detailOn = false;\n /**\n * Hard cap on stored detail samples per category — detail is only ever on\n * for a deliberate debug request, but a pathological request must not pin\n * unbounded memory. The aggregate {@link count} still reflects the true\n * total; only the retained per-event list is bounded.\n */\n private static readonly DETAIL_CAP = 1000;\n\n /** Record an already-measured phase. */\n record(name: string, dur: number, desc?: string): void {\n this._marks.push({ name, dur, desc });\n }\n\n /**\n * Turn on per-event DETAIL capture for this request. Off by default so the\n * hot path never allocates a per-event list; the HTTP middleware enables it\n * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.\n */\n enableDetail(): void {\n this._detailOn = true;\n }\n\n /** Whether per-event detail capture is on. */\n get detailEnabled(): boolean {\n return this._detailOn;\n }\n\n /**\n * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op\n * unless {@link enableDetail} was called, so the hot-path call site (the SQL\n * driver's query listener) pays only a boolean check when detail is off.\n * Bounded by {@link DETAIL_CAP}; excess events still count toward the\n * aggregate via {@link count} but are not retained individually.\n */\n recordDetail(category: string, label: string, dur: number): void {\n if (!this._detailOn) return;\n const detail = (this._detail ??= new Map());\n let list = detail.get(category);\n if (!list) {\n list = [];\n detail.set(category, list);\n }\n if (list.length >= PerfTiming.DETAIL_CAP) return;\n list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });\n }\n\n /** Retained detail samples for `category`, in record order (empty when none). */\n details(category: string): readonly ServerTimingDetail[] {\n return this._detail?.get(category) ?? [];\n }\n\n /**\n * Begin timing a phase. Returns an idempotent `end()` - the first call\n * records the elapsed duration; later calls are ignored, so it is safe to\n * call from both a success and an error path.\n */\n start(name: string, desc?: string): () => void {\n const t0 = perfNow();\n let done = false;\n return () => {\n if (done) return;\n done = true;\n this.record(name, perfNow() - t0, desc);\n };\n }\n\n /** Time an async (or sync) function, recording its elapsed duration. */\n async measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T> {\n const end = this.start(name, desc);\n try {\n return await fn();\n } finally {\n end();\n }\n }\n\n /**\n * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call\n * adds `dur` to the running total for `name` and increments a counter; the\n * mark serializes as `name;dur=<sum>;desc=\"<count> <unit>\"` (or just the\n * bare count when no `unit` is given).\n *\n * Use this for high-frequency phases — one SQL query, one hook execution —\n * where recording a distinct mark per event would blow the header out to\n * hundreds of entries. The single `db;dur=210;desc=\"6 queries\"` member is\n * both the total DB time and the query count, which is the number most\n * useful for spotting N sequential round-trips.\n *\n * The aggregate mark is inserted into the record stream the first time its\n * name is seen, so it keeps its natural position relative to explicit marks\n * (e.g. before the outer `total`, which is recorded last).\n */\n count(name: string, dur: number, unit?: string): void {\n const add = Number.isFinite(dur) && dur > 0 ? dur : 0;\n const aggregates = (this._aggregates ??= new Map());\n let entry = aggregates.get(name);\n if (!entry) {\n const mark: ServerTimingMark = { name, dur: 0 };\n entry = { mark, count: 0, unit };\n aggregates.set(name, entry);\n this._marks.push(mark);\n }\n entry.count += 1;\n entry.mark.dur += add;\n if (unit) entry.unit = unit;\n entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);\n }\n\n /** Snapshot of recorded marks, in record order. */\n marks(): readonly ServerTimingMark[] {\n return this._marks;\n }\n\n /** Serialize to a `Server-Timing` header value (`''` when empty). */\n toHeader(): string {\n return formatServerTiming(this._marks);\n }\n}\n\n// --- Ambient (request-scoped) collector -------------------------------\n\n/**\n * The ambient collector lives in ONE process-wide `AsyncLocalStorage`, pinned\n * to a global-registry symbol rather than a plain module-level `const`.\n *\n * Why: this module is consumed from many packages and can legitimately be\n * loaded more than once in a single process — the ESM build (`dist/index.js`)\n * and the CJS build (`dist/index.cjs`) are distinct module instances, and a\n * bundler may inline yet another copy. A plain `const store` would give each\n * copy its OWN store, so a request scope opened through one copy (the HTTP\n * server's `runWithPerfTiming`) would be invisible to code reading the ambient\n * collector through another copy (the SQL driver, the ObjectQL engine) — the\n * cross-layer `db` / `auth` / `hooks` spans would silently never record.\n * `Symbol.for` resolves to the same symbol across every copy, so they all share\n * the one store.\n */\nconst STORE_KEY = Symbol.for('@objectstack/observability:perf-timing-store');\nconst globalStore = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfTiming> | undefined>;\nconst store: AsyncLocalStorage<PerfTiming> =\n globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage<PerfTiming>());\n\n/** Run `fn` with `timing` as the ambient collector for the async call chain. */\nexport function runWithPerfTiming<T>(timing: PerfTiming, fn: () => T): T {\n return store.run(timing, fn);\n}\n\n/** The collector for the current request, or `undefined` outside a request. */\nexport function currentPerfTiming(): PerfTiming | undefined {\n return store.getStore();\n}\n\n/** Record a phase on the ambient collector. No-op when none is active. */\nexport function recordServerTiming(name: string, dur: number, desc?: string): void {\n store.getStore()?.record(name, dur, desc);\n}\n\n/**\n * Begin timing a phase on the ambient collector. Returns an `end()` callback;\n * when no collector is active the returned callback is a no-op so call sites\n * stay branch-free.\n */\nexport function startServerTiming(name: string, desc?: string): () => void {\n const t = store.getStore();\n if (!t) return () => {};\n return t.start(name, desc);\n}\n\n/**\n * Time an async function on the ambient collector. When no collector is active\n * the function is awaited with zero timing overhead.\n */\nexport async function measureServerTiming<T>(\n name: string,\n fn: () => T | Promise<T>,\n desc?: string,\n): Promise<T> {\n const t = store.getStore();\n if (!t) return fn();\n return t.measure(name, fn, desc);\n}\n\n/**\n * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the\n * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is\n * active, so the hot-path call sites (the SQL driver's query listener, the hook\n * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.\n */\nexport function countServerTiming(name: string, dur: number, unit?: string): void {\n store.getStore()?.count(name, dur, unit);\n}\n\n/**\n * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto\n * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no\n * collector is active OR detail capture is off, so the hot-path call site pays\n * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.\n */\nexport function recordServerTimingDetail(category: string, label: string, dur: number): void {\n store.getStore()?.recordDetail(category, label, dur);\n}\n\n// --- Disclosure gate (WHO may see the timing) -------------------------\n\n/**\n * Per-request disclosure gate — the policy counterpart to the collector.\n *\n * The {@link PerfTiming} collector only MEASURES; whether the measured\n * `Server-Timing` header is returned to the client is a separate decision. When\n * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has\n * opted the whole environment in, so the gate opens up front and every response\n * carries the header. When it is turned on PER-REQUEST — the caller sends an\n * `X-OS-Debug-Timing` header — the header must stay withheld until the request\n * proves an admin/service identity: phase durations are a mild\n * backend-fingerprinting surface, so an ordinary user must never be able to pull\n * them just by sending a header. The request path flips the gate open with\n * {@link allowPerfDisclosure} once it has resolved a privileged principal.\n *\n * Keeping this out of {@link PerfTiming} preserves the collector's invariant\n * (\"it only measures, it never decides whether to emit\").\n *\n * Two levels, because global mode discloses the basic header to everyone but the\n * richer per-query detail must stay admin-only:\n * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by\n * global mode for everyone, or by a proven admin per-request).\n * - `privileged` — the principal is a proven admin/service. Gates the richer,\n * SQL-shape-bearing detail payload, which must NEVER reach an\n * ordinary caller even when global mode is on.\n */\nexport interface PerfDisclosureGate {\n /** Whether the basic collected timing may be disclosed to the client. */\n allowed: boolean;\n /**\n * Whether the principal is a proven admin/service — gates the richer detail\n * payload independently of `allowed`. Absent = not privileged.\n */\n privileged?: boolean;\n}\n\n/**\n * The disclosure gate lives in its OWN global-registry-pinned\n * `AsyncLocalStorage`, for the same cross-module-copy reason as the collector\n * store above: the middleware seeds the gate and the dispatcher (a different\n * package, possibly a different module copy) flips it open — both must see the\n * one store.\n */\nconst GATE_KEY = Symbol.for('@objectstack/observability:perf-disclosure-gate');\nconst globalGate = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfDisclosureGate> | undefined>;\nconst gateStore: AsyncLocalStorage<PerfDisclosureGate> =\n globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage<PerfDisclosureGate>());\n\n/**\n * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.\n * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`\n * settles to decide whether to emit the header.\n */\nexport function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T {\n return gateStore.run(gate, fn);\n}\n\n/**\n * Open the ambient disclosure gate — the request has proven an admin/service\n * identity, so it may see its own `Server-Timing` header AND the richer detail\n * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A\n * no-op when no gate is active (perf-tuning off), so the call site stays\n * branch-free.\n */\nexport function allowPerfDisclosure(): void {\n const g = gateStore.getStore();\n if (g) {\n g.allowed = true;\n g.privileged = true;\n }\n}\n\n/** Whether the ambient disclosure gate is open. `false` when none is active. */\nexport function isPerfDisclosureAllowed(): boolean {\n return gateStore.getStore()?.allowed ?? false;\n}\n\n/**\n * Whether the ambient principal is a proven admin/service — gates the richer\n * detail payload. `false` when no gate is active or only global-mode disclosure\n * (not a proven admin) opened it.\n */\nexport function isPerfDisclosurePrivileged(): boolean {\n return gateStore.getStore()?.privileged ?? false;\n}\n\n/**\n * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header\n * (#2408 perf-tuning gating). The header exposes internal phase durations — a\n * mild backend-fingerprinting surface — so when timing is opened per-request via\n * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:\n *\n * - `isSystem` — internal/engine self-calls,\n * - `principalKind` `service` / `system` — service tokens & the system seed,\n * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.\n *\n * Ordinary human/guest/agent callers get `false`, so sending the debug header\n * yields no header for them. Global (env/option) perf mode bypasses this — it\n * opened the disclosure gate up front for the whole environment.\n *\n * This is the ONE definition of \"who may pull per-request timings\", shared by\n * every HTTP entry point that resolves a principal — the runtime dispatcher\n * (`timedResolveExecutionContext`), the REST server, and the standalone Hono\n * CRUD surface — so a new admin-serving path can never silently under- or\n * over-disclose by hand-rolling its own rule (#3361).\n */\nexport function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean {\n if (!ec) return false;\n if (ec.isSystem === true) return true;\n if (ec.principalKind === 'service' || ec.principalKind === 'system') return true;\n return ec.posture === 'PLATFORM_ADMIN' || ec.posture === 'TENANT_ADMIN';\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -132,15 +132,6 @@ declare const SEMCONV: {
|
|
|
132
132
|
* body parse included — not the handler's share of it.
|
|
133
133
|
*/
|
|
134
134
|
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
135
|
-
/**
|
|
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).
|
|
142
|
-
*/
|
|
143
|
-
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
144
135
|
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
145
136
|
readonly storageOperationsTotal: "storage_operations_total";
|
|
146
137
|
/** Histogram (ms), labels: `adapter`, `op`. */
|
|
@@ -205,7 +196,6 @@ declare const SEMCONV: {
|
|
|
205
196
|
declare const RUNTIME_METRICS: {
|
|
206
197
|
readonly httpRequestsTotal: "http_requests_total";
|
|
207
198
|
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
208
|
-
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
209
199
|
};
|
|
210
200
|
|
|
211
201
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -132,15 +132,6 @@ declare const SEMCONV: {
|
|
|
132
132
|
* body parse included — not the handler's share of it.
|
|
133
133
|
*/
|
|
134
134
|
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
135
|
-
/**
|
|
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).
|
|
142
|
-
*/
|
|
143
|
-
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
144
135
|
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
145
136
|
readonly storageOperationsTotal: "storage_operations_total";
|
|
146
137
|
/** Histogram (ms), labels: `adapter`, `op`. */
|
|
@@ -205,7 +196,6 @@ declare const SEMCONV: {
|
|
|
205
196
|
declare const RUNTIME_METRICS: {
|
|
206
197
|
readonly httpRequestsTotal: "http_requests_total";
|
|
207
198
|
readonly httpRequestDurationMs: "http_request_duration_ms";
|
|
208
|
-
readonly httpRequestErrorsTotal: "http_request_errors_total";
|
|
209
199
|
};
|
|
210
200
|
|
|
211
201
|
/**
|
package/dist/index.js
CHANGED
|
@@ -19,15 +19,20 @@ var SEMCONV = {
|
|
|
19
19
|
* body parse included — not the handler's share of it.
|
|
20
20
|
*/
|
|
21
21
|
httpRequestDurationMs: "http_request_duration_ms",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
// ⛔ RETIRED — `http_request_errors_total` was removed in
|
|
23
|
+
// `@objectstack/observability` 17.2.0 (#9834, ADR-0049 enforce-or-remove).
|
|
24
|
+
// ⛔ Do not re-add the name. It was DECLARED here as a stable server-wide
|
|
25
|
+
// signal and EMITTED only from `@objectstack/runtime`'s per-route wrapper,
|
|
26
|
+
// on a THROWN handler — so it never saw auth's `getRawApp()` mount, the
|
|
27
|
+
// REST data API, or any error a handler answered politely through
|
|
28
|
+
// `errorResponseBase` (which sets a status and does not re-throw). No
|
|
29
|
+
// transport-side emitter could preserve that population either: the
|
|
30
|
+
// `IHttpServer.afterResponse` observation carries `{method, routePattern,
|
|
31
|
+
// status, elapsedMs}` and no throw signal at all.
|
|
32
|
+
// ⇒ Read the 5xx rate from `http_requests_total{status=~"5.."}` instead.
|
|
33
|
+
// The transport emits that family through the seam, so it covers every
|
|
34
|
+
// inbound surface (#9650 / #9835 / #10004) and carries the status label
|
|
35
|
+
// this counter only stood in for. Maintainer ruling 2026-08-20.
|
|
31
36
|
// ── Storage — emitted by `@objectstack/service-storage` adapters ──
|
|
32
37
|
/** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */
|
|
33
38
|
storageOperationsTotal: "storage_operations_total",
|
|
@@ -90,8 +95,11 @@ var SEMCONV = {
|
|
|
90
95
|
};
|
|
91
96
|
var RUNTIME_METRICS = {
|
|
92
97
|
httpRequestsTotal: SEMCONV.httpRequestsTotal,
|
|
93
|
-
httpRequestDurationMs: SEMCONV.httpRequestDurationMs
|
|
94
|
-
httpRequestErrorsTotal
|
|
98
|
+
httpRequestDurationMs: SEMCONV.httpRequestDurationMs
|
|
99
|
+
// `httpRequestErrorsTotal` retired with its SEMCONV declaration above
|
|
100
|
+
// (#9834). The alias is not a compatibility window of its own: there is no
|
|
101
|
+
// emitter left to read, so keeping the name here would hand callers a
|
|
102
|
+
// string nothing ever writes.
|
|
95
103
|
};
|
|
96
104
|
|
|
97
105
|
// src/http-transport-metrics.ts
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/service-names.ts","../src/semconv.ts","../src/http-transport-metrics.ts","../src/metrics-exporters.ts","../src/error-exporters.ts","../src/loggers.ts","../src/perf-timing.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical service-registry names for the host's observability\n * backends. Plugins look these up to discover the configured\n * {@link MetricsRegistry} / {@link ErrorReporter} without each host\n * having to thread observability config through every plugin\n * constructor.\n *\n * See `@objectstack/runtime` → `ObservabilityServicePlugin` for the\n * registration side.\n */\nexport const OBSERVABILITY_METRICS_SERVICE = 'observability:metrics';\nexport const OBSERVABILITY_ERRORS_SERVICE = 'observability:errors';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Semantic conventions — canonical metric names emitted by the\n * framework. Listed here so hosts can wire alerts/dashboards against\n * a stable namespace and so call sites don't sprinkle string\n * literals through the code base.\n *\n * Naming follows Prometheus conventions:\n *\n * - snake_case identifiers.\n * - `_total` suffix for monotonic counters.\n * - `_ms`, `_seconds`, `_bytes` suffixes for histograms / gauges\n * with units.\n *\n * Groups roughly mirror the framework subsystems that emit them.\n * Cloud-specific metrics (DO restarts, Workers Analytics Engine\n * writes, …) do NOT belong here — they are deployment-specific and\n * stay in the deployment repo.\n */\nexport const SEMCONV = {\n // ── HTTP — emitter differs per family, see each ────────────────────\n /**\n * Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT\n * through the `IHttpServer.afterResponse` seam (#9835), so it covers\n * every inbound request on the server rather than only the routes the\n * runtime dispatcher registers.\n */\n httpRequestsTotal: 'http_requests_total',\n /**\n * Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT\n * through the same seam (#9834). It measures the REQUEST as the transport\n * sees it — first sight to the response existing, middleware chain and\n * body parse included — not the handler's share of it.\n */\n httpRequestDurationMs: 'http_request_duration_ms',\n /**\n * Counter, labels: `method`, `route`. Incremented when an in-flight\n * handler throws after the response is sent. Emitted by\n * `@objectstack/runtime`'s `instrumentRouteHandler`, and NOT movable to\n * the seam above as-is: the observation carries a status but no throw\n * signal, so a transport-side emitter would count a different population\n * (#9834 records the fork).\n */\n httpRequestErrorsTotal: 'http_request_errors_total',\n\n // ── Storage — emitted by `@objectstack/service-storage` adapters ──\n /** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */\n storageOperationsTotal: 'storage_operations_total',\n /** Histogram (ms), labels: `adapter`, `op`. */\n storageOperationDurationMs: 'storage_operation_duration_ms',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n storageErrorsTotal: 'storage_errors_total',\n\n // ── Cache — emitted by `@objectstack/service-cache` adapters ──\n // Uniform emitter; what varies is who CONSULTS the service — see below.\n /**\n * Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).\n *\n * ⚠️ A flat zero means \"NO CONFIGURED CONSUMER\", not \"no cache activity\",\n * and — unlike the HTTP families above — it is NOT an instrumentation gap.\n * The adapters hold the host's registry and count every call they receive\n * (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails\n * to communicate is WHY.\n *\n * The why: nothing consults the `cache` service unconditionally. Every\n * production consumer is a rate-limit / budget counter store, and each is\n * gated on a declaration somebody has to write — better-auth's per-IP\n * counters (`rate_limit_max` / `rate_limit_window_seconds` in auth\n * settings), the dispatcher's inbound limiter and its declarative\n * per-endpoint buckets (an armed `rateLimit` budget; with none declared\n * the dispatcher registers no limiter at all), and the per-number OTP send\n * budget (an SMS send path). A default install declares none of them, so\n * this counter stays at 0 while the server handles traffic normally.\n *\n * ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%\n * hit rate or a broken adapter. Before trusting a cache hit-rate panel,\n * confirm at least one consumer above is actually armed.\n */\n cacheLookupsTotal: 'cache_lookups_total',\n /**\n * Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same\n * \"zero = no configured consumer\" reading as `cacheLookupsTotal` above.\n */\n cacheWritesTotal: 'cache_writes_total',\n /**\n * Counter, labels: `adapter`, `op`, `errorClass`. Same \"zero = no\n * configured consumer\" reading as `cacheLookupsTotal` above — a zero is\n * \"nothing was asked of the cache\", not \"every call succeeded\".\n */\n cacheErrorsTotal: 'cache_errors_total',\n\n // ── Background jobs — emitted by `@objectstack/runtime`'s AppPlugin ──\n /**\n * Counter, labels: `app`, `job`. Incremented when a DECLARED background\n * job could not be handed to the job service — i.e. the app booted green\n * but that job will never run (#4567). Any non-zero value is an outage of\n * the job, not a warning.\n */\n jobScheduleFailuresTotal: 'job_schedule_failures_total',\n\n // ── Package / registry-reader — emitted by `@objectstack/service-package` ──\n /** Counter, labels: `result` (`ok`|`miss`|`error`). */\n registryLookupsTotal: 'registry_lookups_total',\n /** Histogram (ms). */\n registryLookupDurationMs: 'registry_lookup_duration_ms',\n /** Counter, labels: `source` (`r2`|`http`|`local`), `result` (`hit`|`miss`|`error`). */\n registrySourceFetchesTotal: 'registry_source_fetches_total',\n} as const;\n\n/**\n * Backwards-compat alias. `RUNTIME_METRICS` was the original (HTTP-only)\n * constant name shipped from `@objectstack/runtime`; we keep it here so\n * existing code reading `RUNTIME_METRICS.httpRequestsTotal` continues\n * to work after the constants moved into this package.\n */\nexport const RUNTIME_METRICS = {\n httpRequestsTotal: SEMCONV.httpRequestsTotal,\n httpRequestDurationMs: SEMCONV.httpRequestDurationMs,\n httpRequestErrorsTotal: SEMCONV.httpRequestErrorsTotal,\n} as const;\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IHttpServer } from '@objectstack/spec/contracts';\nimport type { MetricsRegistry } from './contracts.js';\nimport { RUNTIME_METRICS } from './semconv.js';\n\n/**\n * What an `arm*` call in this module did:\n *\n * - `'armed'` — the emitting observer was registered on this call.\n * - `'already-armed'` — some earlier caller already armed this server for\n * THIS metric family; the seam emits it, and this call registered nothing\n * (first-wins).\n * - `'unsupported'` — the transport does not implement the\n * `IHttpServer.afterResponse` seam; it reports NO HTTP metrics (#9835:\n * zero there means \"not instrumented\", never \"no traffic\"), and the\n * caller must decide how to degrade — the runtime dispatcher falls back\n * to instrumenting its own routes.\n */\nexport type ArmHttpMetricResult = 'armed' | 'already-armed' | 'unsupported';\n\n/**\n * The name {@link armHttpRequestCounter} shipped with. Kept as an alias\n * rather than renamed: the export is already in the pending release, and the\n * two families arm through separate entry points anyway.\n */\nexport type ArmHttpRequestCounterResult = ArmHttpMetricResult;\n\n/**\n * The per-server latch behind the contract's ownership rule. A registered\n * global-registry symbol (`Symbol.for`), not a module-level WeakSet, so the\n * latch cannot fork if two copies of this module ever coexist in one\n * process — the whole point is that there is exactly ONE latch per server\n * object, whoever asks.\n */\nconst HTTP_REQUEST_COUNTER_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestCounterArmed',\n);\n\n/**\n * Arm `http_requests_total{method,route,status}` on a transport through the\n * `IHttpServer.afterResponse` observation seam (#9835) — AT MOST ONCE per\n * server, whoever calls first.\n *\n * ## Why arming is centralized here\n *\n * The contract's ownership rule says a request must never be double-counted,\n * and two composition layers legitimately hold both a server and a metrics\n * registry: the transport's own hosting plugin (`HonoServerPlugin`, which\n * the 2026-08-18 ruling on #9650 made the counter's home) and the runtime\n * dispatcher (whose `observability.metrics` config is the wiring the docs\n * demonstrate). When a host hands ONE registry to both — the ordinary case —\n * two independently-registered observers would land every request on the\n * same series twice: exactly the #9833 distortion, rebuilt one seam over.\n * Routing every arming through this function makes \"exactly one\n * counter-emitting observer per server\" structural: the first caller arms\n * (in the shipped composition that is the transport plugin, in Phase 1),\n * every later caller is told the seam already counts.\n *\n * The label shape is pinned by the contract: `route` is the transport's\n * `routePattern` — the registered PATTERN, never the concrete path — and\n * `status` is stringified for the label set. Emission goes through the\n * transport's observer-isolation guarantee, so a throwing registry cannot\n * break a response.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper: the latch is per object identity, and a wrapper\n * would both fork the latch and (per #5122) risk erasing the optional\n * member this function feature-detects.\n * @param metrics - The registry the counter lands in. First caller wins; a\n * second registry offered later is NOT added (the contract's one-owner\n * rule), and the result says so.\n */\nexport function armHttpRequestCounter(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_COUNTER_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_COUNTER_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_COUNTER_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {\n method: observation.method,\n route: observation.routePattern,\n status: String(observation.status),\n });\n });\n return 'armed';\n}\n\n/**\n * The duration family's own latch. A SEPARATE registered symbol from the\n * counter's, deliberately: the two families are armed through separate entry\n * points and gated separately on the dispatcher's per-route wrapper, so a\n * host that arms one must not silently latch the other away.\n */\nconst HTTP_REQUEST_DURATION_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestDurationArmed',\n);\n\n/**\n * Arm `http_request_duration_ms{method,route}` on a transport through the\n * `IHttpServer.afterResponse` observation seam — AT MOST ONCE per server,\n * whoever calls first. The duration half of #9834, built on the mechanism\n * #9835 proved out for `http_requests_total`.\n *\n * ## Why the histogram has to move too\n *\n * #9835 moved only the counter, which left the docs' two derived signals\n * inconsistent with each other: 5xx rate saw every inbound surface while p95\n * latency still saw the dispatcher's own routes. An operator reading one\n * dashboard got request volume for `/api/v1/*` beside a latency panel with no\n * series for it — and the worse reading is the p95 that IS drawn, computed\n * from dispatcher routes only and presented as the server's.\n *\n * ## ⚠️ The observation WINDOW changes with the emitter\n *\n * The dispatcher's per-route wrapper timed `await handler(req, res)` — handler\n * latency. This seam times the transport's own `use('*')` around\n * `await next()`, which is what {@link HttpResponseObservation.elapsedMs}\n * means: \"from the transport first seeing the request to the response\n * existing\". That includes the middleware chain and body parse, so the series\n * can only move UP, never down. It is the number an operator's latency panel\n * should have been showing — the request's latency rather than one layer's\n * share of it — but it is a visible change in an existing series, so it is\n * stated here, in the changeset, and in `docs/OBSERVABILITY.md` rather than\n * left for a dashboard to discover.\n *\n * The label set is unchanged and stays the SEMCONV-declared `{method,route}`:\n * `route` is the transport's `routePattern` (the registered PATTERN, never the\n * concrete path), and no `status` label is added — a histogram split by status\n * is a different series shape than the one the docs tell operators to graph.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper — the latch is per object identity, and (per\n * #5122) a wrapper risks erasing the optional member this feature-detects.\n * @param metrics - The registry the histogram lands in. First caller wins.\n */\nexport function armHttpRequestDurationHistogram(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_DURATION_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_DURATION_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_DURATION_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.histogram(\n RUNTIME_METRICS.httpRequestDurationMs,\n observation.elapsedMs,\n { method: observation.method, route: observation.routePattern },\n );\n });\n return 'armed';\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { MetricsRegistry, MetricSample } from './contracts.js';\n\n// ─── Noop ─────────────────────────────────────────────────────────────\n\n/**\n * No-op metrics registry — the default. Discards every observation.\n * Production deployments should swap this for a real registry; tests\n * can use {@link InMemoryMetricsRegistry} to assert emissions.\n */\nexport class NoopMetricsRegistry implements MetricsRegistry {\n counter(): void { }\n histogram(): void { }\n gauge(): void { }\n}\n\n// ─── In-memory (tests + dev inspection) ───────────────────────────────\n\n/**\n * In-memory registry used for tests and local inspection. Stores\n * every observation in insertion order; query via the helpers below\n * or read {@link samples} directly.\n *\n * Not intended for production — unbounded growth.\n */\nexport class InMemoryMetricsRegistry implements MetricsRegistry {\n readonly samples: MetricSample[] = [];\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.samples.push({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n /** Sum of counter increments matching `name` and optionally a label subset. */\n totalCounter(name: string, labelMatch: Record<string, string> = {}): number {\n return this.samples\n .filter(s => s.kind === 'counter' && s.name === name && matchesLabels(s.labels, labelMatch))\n .reduce((acc, s) => acc + s.value, 0);\n }\n\n /** Raw histogram observations matching `name` and optionally a label subset. */\n histogramValues(name: string, labelMatch: Record<string, string> = {}): number[] {\n return this.samples\n .filter(s => s.kind === 'histogram' && s.name === name && matchesLabels(s.labels, labelMatch))\n .map(s => s.value);\n }\n\n /** Last gauge value matching `name` and optionally a label subset, or undefined. */\n lastGauge(name: string, labelMatch: Record<string, string> = {}): number | undefined {\n for (let i = this.samples.length - 1; i >= 0; i--) {\n const s = this.samples[i];\n if (s.kind === 'gauge' && s.name === name && matchesLabels(s.labels, labelMatch)) {\n return s.value;\n }\n }\n return undefined;\n }\n\n /** Clear all recorded samples. */\n reset(): void {\n this.samples.length = 0;\n }\n}\n\nfunction matchesLabels(actual: Record<string, string>, expected: Record<string, string>): boolean {\n for (const [k, v] of Object.entries(expected)) {\n if (actual[k] !== v) return false;\n }\n return true;\n}\n\n// ─── Console (development) ────────────────────────────────────────────\n\n/**\n * Console metrics registry — prints one line per observation. Useful\n * during local development to confirm that instrumentation is firing.\n *\n * Not intended for production: writing every observation to stdout\n * defeats Prometheus / OTLP pipelines and dominates request latency.\n */\nexport class ConsoleMetricsRegistry implements MetricsRegistry {\n constructor(private readonly opts: { sink?: (line: string) => void; prefix?: string } = {}) { }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.emit('counter', name, value, labels);\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('histogram', name, value, labels);\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('gauge', name, value, labels);\n }\n\n private emit(kind: string, name: string, value: number, labels: Record<string, string>): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.log(s); });\n const prefix = this.opts.prefix ?? '[metric]';\n const labelStr = Object.entries(labels)\n .map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n .join(' ');\n sink(`${prefix} ${kind} ${name} ${value}${labelStr ? ' ' + labelStr : ''}`);\n } catch {\n // Per contract: never throw from a metric call site.\n }\n }\n}\n\n// ─── OTLP / HTTP (Prometheus via OTel Collector, Grafana Cloud, …) ────\n\n/**\n * Configuration for {@link OtlpHttpMetricsRegistry}.\n */\nexport interface OtlpHttpExporterOptions {\n /**\n * OTLP/HTTP metrics endpoint, e.g. `http://otel-collector:4318/v1/metrics`.\n * The path is appended automatically if missing.\n */\n endpoint: string;\n\n /** Optional headers (Authorization, x-tenant, …). */\n headers?: Record<string, string>;\n\n /**\n * Resource attributes — `service.name`, `service.namespace`,\n * `deployment.environment`, etc. Merged into the OTLP `resource`\n * block on every export.\n */\n resource?: Record<string, string>;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`.\n * Allows Workers / undici / node-fetch substitution and test\n * doubles.\n */\n fetch?: typeof fetch;\n\n /**\n * Called when an export attempt fails. Default: silently swallow\n * (per the contract that metric emission must not throw / log\n * loudly on the hot path).\n */\n onError?: (error: unknown) => void;\n\n /**\n * Maximum number of samples buffered before {@link OtlpHttpMetricsRegistry.flush}\n * is called automatically. Defaults to 1024.\n */\n maxBufferSize?: number;\n}\n\n/**\n * OTLP/HTTP metrics exporter.\n *\n * Buffers samples in memory and serialises them to the OpenTelemetry\n * Protocol JSON encoding when {@link flush} is called (manually or\n * automatically once the buffer hits the configured size).\n *\n * Intentionally does **not** start an interval timer in the constructor:\n * (a) it makes the exporter usable on Cloudflare Workers where\n * `setInterval` is restricted, and (b) it keeps unit tests deterministic.\n * Long-running hosts should call `flush()` on a schedule\n * (e.g. `setInterval(() => reg.flush(), 10_000)` on Node, or\n * `ctx.waitUntil(reg.flush())` from a Workers fetch handler).\n *\n * Only counters, histograms, and gauges are emitted — no support for\n * exemplars or aggregation temporality switches (the Collector handles\n * those on the upstream side).\n */\nexport class OtlpHttpMetricsRegistry implements MetricsRegistry {\n private buffer: MetricSample[] = [];\n private readonly endpoint: string;\n private readonly headers: Record<string, string>;\n private readonly resource: Record<string, string>;\n private readonly maxBufferSize: number;\n private readonly fetchImpl: typeof fetch;\n private readonly onError: (error: unknown) => void;\n\n constructor(options: OtlpHttpExporterOptions) {\n this.endpoint = normaliseEndpoint(options.endpoint);\n this.headers = options.headers ?? {};\n this.resource = options.resource ?? {};\n this.maxBufferSize = options.maxBufferSize ?? 1024;\n this.fetchImpl = options.fetch ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : noFetch);\n this.onError = options.onError ?? (() => { });\n }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.record({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n private record(sample: MetricSample): void {\n this.buffer.push(sample);\n if (this.buffer.length >= this.maxBufferSize) {\n // Fire-and-forget: do not block the call site.\n void this.flush().catch(this.onError);\n }\n }\n\n /** Snapshot the current buffer (for tests). */\n peek(): readonly MetricSample[] {\n return this.buffer.slice();\n }\n\n /**\n * Send the buffered samples to the OTLP endpoint and clear the\n * buffer. Safe to call concurrently — each invocation takes a\n * snapshot before clearing.\n */\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n const samples = this.buffer;\n this.buffer = [];\n try {\n const body = JSON.stringify(serialiseToOtlp(samples, this.resource));\n const res = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...this.headers },\n body,\n });\n if (!res.ok) {\n this.onError(new Error(`OTLP export returned HTTP ${res.status}`));\n }\n } catch (err) {\n this.onError(err);\n }\n }\n}\n\nfunction normaliseEndpoint(raw: string): string {\n const trimmed = raw.replace(/\\/+$/, '');\n if (/\\/v1\\/metrics$/.test(trimmed)) return trimmed;\n return trimmed + '/v1/metrics';\n}\n\nfunction noFetch(): never {\n throw new Error('OtlpHttpMetricsRegistry: no global fetch available; pass options.fetch');\n}\n\n/**\n * Minimal OTLP/JSON metrics serialiser.\n *\n * Implements the subset of <https://opentelemetry.io/docs/specs/otlp/>\n * that we actually emit — sums for counters, gauges for gauges,\n * histograms encoded as bucketless \"summary\"-style histograms with\n * explicit per-sample data points. The Collector accepts this shape\n * and applies its own bucketing.\n *\n * Aggregation temporality is set to DELTA (2) because every flush\n * sends only the samples accumulated since the last flush.\n */\nfunction serialiseToOtlp(samples: MetricSample[], resource: Record<string, string>): unknown {\n const byName = new Map<string, { kind: MetricSample['kind']; points: MetricSample[] }>();\n for (const s of samples) {\n const existing = byName.get(s.name);\n if (existing) existing.points.push(s);\n else byName.set(s.name, { kind: s.kind, points: [s] });\n }\n\n const metrics = Array.from(byName.entries()).map(([name, { kind, points }]) => {\n if (kind === 'counter') {\n return {\n name,\n sum: {\n dataPoints: points.map(p => toNumberPoint(p)),\n aggregationTemporality: 2,\n isMonotonic: true,\n },\n };\n }\n if (kind === 'gauge') {\n return { name, gauge: { dataPoints: points.map(p => toNumberPoint(p)) } };\n }\n // histogram: emit a histogram with a single bucket boundary so the\n // Collector treats it as a recorded distribution.\n return {\n name,\n histogram: {\n aggregationTemporality: 2,\n dataPoints: points.map(p => ({\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n count: '1',\n sum: p.value,\n bucketCounts: ['0', '1'],\n explicitBounds: [p.value],\n })),\n },\n };\n });\n\n return {\n resourceMetrics: [{\n resource: { attributes: toAttributes(resource) },\n scopeMetrics: [{\n scope: { name: '@objectstack/observability', version: '0.1.0' },\n metrics,\n }],\n }],\n };\n}\n\nfunction toNumberPoint(p: MetricSample): unknown {\n return {\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n asDouble: p.value,\n };\n}\n\nfunction toAttributes(labels: Record<string, string>): unknown[] {\n return Object.entries(labels).map(([key, value]) => ({\n key,\n value: { stringValue: value },\n }));\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ErrorReporter, CapturedError } from './contracts.js';\n\n/** No-op reporter — the default. */\nexport class NoopErrorReporter implements ErrorReporter {\n captureException(): void { }\n}\n\n/** In-memory reporter for tests. */\nexport class InMemoryErrorReporter implements ErrorReporter {\n readonly captured: CapturedError[] = [];\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n this.captured.push({ error, context, at: Date.now() });\n }\n\n reset(): void {\n this.captured.length = 0;\n }\n}\n\n/**\n * Console error reporter — writes a structured JSON line to stderr per\n * captured exception. Convenient default for local development and for\n * any deployment that ships stderr to a log aggregator (e.g. Loki,\n * fluent-bit) but does not have a dedicated APM.\n *\n * Stack traces are included when the captured value is an `Error`.\n */\nexport class ConsoleErrorReporter implements ErrorReporter {\n constructor(private readonly opts: { sink?: (line: string) => void } = {}) { }\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.error(s); });\n const record: Record<string, unknown> = {\n ts: new Date().toISOString(),\n level: 'error',\n msg: error instanceof Error ? error.message : String(error),\n context,\n };\n if (error instanceof Error && error.stack) {\n record.stack = error.stack;\n }\n sink(JSON.stringify(record));\n } catch {\n // Per contract: error reporting must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Logger } from './contracts.js';\n\n/** Recognised log levels in increasing severity order. */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n fatal: 50,\n};\n\n/** No-op logger — discards every message. */\nexport class NoopLogger implements Logger {\n debug(): void { }\n info(): void { }\n warn(): void { }\n error(): void { }\n fatal(): void { }\n child(): Logger { return this; }\n}\n\n/**\n * Console logger — pretty-printed messages for local development.\n *\n * Not suitable for production where structured JSON is required;\n * use {@link JsonLogger} there instead.\n */\nexport class ConsoleLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n } = {},\n ) { }\n\n private get threshold(): number {\n return LEVEL_PRIORITY[this.opts.level ?? 'info'];\n }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> {\n return this.opts.context ?? {};\n }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new ConsoleLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const merged = { ...this.context, ...(meta ?? {}) };\n const tail = Object.keys(merged).length ? ' ' + JSON.stringify(merged) : '';\n const line = `[${level}] ${msg}${tail}`;\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n\n/**\n * JSON logger — one JSON object per line on stdout (errors on stderr).\n *\n * Matches the shape that Loki / fluent-bit / Cloudflare Logpush ingest\n * by default. Every record contains `ts`, `level`, `msg`, and any\n * accumulated child-context plus per-call `meta`.\n *\n * Use this in production. Use {@link ConsoleLogger} during development\n * for human-friendly output.\n */\nexport class JsonLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n /** Optional fields injected into every record (`service`, `env`, …). */\n base?: Record<string, unknown>;\n /** Wall clock for tests. */\n now?: () => Date;\n } = {},\n ) { }\n\n private get threshold(): number { return LEVEL_PRIORITY[this.opts.level ?? 'info']; }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> { return this.opts.context ?? {}; }\n private get base(): Record<string, unknown> { return this.opts.base ?? {}; }\n private get now(): () => Date { return this.opts.now ?? (() => new Date()); }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new JsonLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const record = {\n ts: this.now().toISOString(),\n level,\n msg,\n ...this.base,\n ...this.context,\n ...(meta ?? {}),\n };\n const line = JSON.stringify(record);\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Per-request performance timing - a tiny, dependency-free collector that\n * accumulates named phase durations during a single request and serializes\n * them into the W3C `Server-Timing` response header.\n *\n * @see <https://www.w3.org/TR/server-timing/>\n * @see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>\n *\n * Two ways to record:\n *\n * 1. **Explicit collector.** Hold a {@link PerfTiming} instance and call\n * `start()` / `record()` / `measure()` directly. The HTTP adapter owns\n * the instance for a request.\n *\n * 2. **Ambient collector.** Run a request inside {@link runWithPerfTiming}\n * and any framework code on that async call chain records phases via the\n * free functions ({@link measureServerTiming}, {@link startServerTiming},\n * {@link recordServerTiming}, {@link countServerTiming}) without threading\n * the request object through every layer. When no collector is active the\n * free functions are cheap no-ops, so call sites pay nothing when the\n * feature is off. High-frequency phases (per SQL query, per hook) use\n * {@link countServerTiming} to fold into one aggregate mark carrying a\n * total duration and an event count.\n *\n * `Server-Timing` exposes internal phase durations to any client, which is a\n * (mild) information-disclosure surface - it helps an attacker profile the\n * backend. Emission is therefore opt-in (\"perf-tuning mode\"); the collector\n * itself never decides whether to emit, it only measures.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * One recorded phase of a request's server-side processing, serialized as a\n * single member of the `Server-Timing` header.\n */\nexport interface ServerTimingMark {\n /** Metric name. Coerced to a Server-Timing token on record. */\n name: string;\n /** Duration in milliseconds. */\n dur: number;\n /** Optional human-readable description (rendered as the quoted `desc`). */\n desc?: string;\n}\n\n/**\n * One recorded sub-event when DETAIL capture is on (see\n * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds\n * every query into a single count+duration — a detail sample keeps the\n * individual event so an admin can see *which* queries ran and which was\n * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED\n * statement, bindings stripped — the query shape, never literal row values).\n */\nexport interface ServerTimingDetail {\n /** Event label — e.g. a parametrized SQL statement (no bindings). */\n label: string;\n /** Duration in milliseconds. */\n dur: number;\n}\n\n/**\n * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not\n * affected by wall-clock adjustments); falls back to `Date.now()` on the rare\n * runtime where `performance` is unavailable.\n */\nexport function perfNow(): number {\n try {\n return performance.now();\n } catch {\n return Date.now();\n }\n}\n\n/** Characters not allowed in a Server-Timing metric name (a token). */\nconst NAME_UNSAFE = /[^A-Za-z0-9_-]+/g;\n\nconst UNDERSCORE = 0x5f;\n\n/**\n * Coerce an arbitrary string into a Server-Timing token: non-token runs become\n * a single underscore, then leading/trailing underscores are trimmed.\n *\n * The trim is a linear scan rather than a `/^_+|_+$/g` regex on purpose - the\n * anchored `_+$` quantifier backtracks polynomially on underscore-heavy input\n * (CodeQL js/polynomial-redos), and this name comes from a public-API argument.\n */\nfunction sanitizeName(name: string): string {\n const collapsed = String(name).replace(NAME_UNSAFE, '_');\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed.charCodeAt(start) === UNDERSCORE) start++;\n while (end > start && collapsed.charCodeAt(end - 1) === UNDERSCORE) end--;\n return collapsed.slice(start, end);\n}\n\n/**\n * Make a description safe to embed in a quoted-string. Backslashes and double\n * quotes would terminate the quoting; control chars (incl. CR/LF) could forge\n * headers. Collapse anything outside a conservative printable set to a space.\n */\nfunction sanitizeDesc(desc: string): string {\n let out = '';\n for (const ch of String(desc)) {\n const code = ch.codePointAt(0)!;\n // Printable ASCII excluding `\"` (0x22) and `\\` (0x5C); drop the rest.\n if (code >= 0x20 && code < 0x7f && ch !== '\"' && ch !== '\\\\') {\n out += ch;\n } else {\n out += ' ';\n }\n }\n return out.replace(/ +/g, ' ').trim();\n}\n\n/** Round to at most 2 decimals without trailing-zero noise (`12.3`, not `12.30`). */\nfunction fmtDur(dur: number): string {\n if (!Number.isFinite(dur)) return '0';\n return String(Math.round(dur * 100) / 100);\n}\n\n/**\n * Serialize marks into a `Server-Timing` header value. Marks with an empty\n * name after sanitization are dropped (the grammar requires a token). Returns\n * `''` when there is nothing to emit so callers can skip the header.\n */\nexport function formatServerTiming(marks: readonly ServerTimingMark[]): string {\n const parts: string[] = [];\n for (const m of marks) {\n const name = sanitizeName(m.name);\n if (!name) continue;\n let part = `${name};dur=${fmtDur(m.dur)}`;\n if (m.desc) {\n const desc = sanitizeDesc(m.desc);\n if (desc) part += `;desc=\"${desc}\"`;\n }\n parts.push(part);\n }\n return parts.join(', ');\n}\n\n/**\n * Collector for one request's timing phases. Not thread-safe by design - one\n * instance belongs to one request. All methods are allocation-light and never\n * throw on the hot path.\n */\nexport class PerfTiming {\n private readonly _marks: ServerTimingMark[] = [];\n /**\n * Live aggregate marks by name (see {@link count}). Lazily created so a\n * request that never aggregates pays nothing. Each entry points at a mark\n * already inserted into {@link _marks}, mutated in place as events arrive.\n */\n private _aggregates?: Map<string, { mark: ServerTimingMark; count: number; unit?: string }>;\n /**\n * Per-event detail samples by category, populated only while detail capture\n * is on (see {@link enableDetail}). Lazily created so a request that never\n * enables detail pays nothing.\n */\n private _detail?: Map<string, ServerTimingDetail[]>;\n private _detailOn = false;\n /**\n * Hard cap on stored detail samples per category — detail is only ever on\n * for a deliberate debug request, but a pathological request must not pin\n * unbounded memory. The aggregate {@link count} still reflects the true\n * total; only the retained per-event list is bounded.\n */\n private static readonly DETAIL_CAP = 1000;\n\n /** Record an already-measured phase. */\n record(name: string, dur: number, desc?: string): void {\n this._marks.push({ name, dur, desc });\n }\n\n /**\n * Turn on per-event DETAIL capture for this request. Off by default so the\n * hot path never allocates a per-event list; the HTTP middleware enables it\n * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.\n */\n enableDetail(): void {\n this._detailOn = true;\n }\n\n /** Whether per-event detail capture is on. */\n get detailEnabled(): boolean {\n return this._detailOn;\n }\n\n /**\n * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op\n * unless {@link enableDetail} was called, so the hot-path call site (the SQL\n * driver's query listener) pays only a boolean check when detail is off.\n * Bounded by {@link DETAIL_CAP}; excess events still count toward the\n * aggregate via {@link count} but are not retained individually.\n */\n recordDetail(category: string, label: string, dur: number): void {\n if (!this._detailOn) return;\n const detail = (this._detail ??= new Map());\n let list = detail.get(category);\n if (!list) {\n list = [];\n detail.set(category, list);\n }\n if (list.length >= PerfTiming.DETAIL_CAP) return;\n list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });\n }\n\n /** Retained detail samples for `category`, in record order (empty when none). */\n details(category: string): readonly ServerTimingDetail[] {\n return this._detail?.get(category) ?? [];\n }\n\n /**\n * Begin timing a phase. Returns an idempotent `end()` - the first call\n * records the elapsed duration; later calls are ignored, so it is safe to\n * call from both a success and an error path.\n */\n start(name: string, desc?: string): () => void {\n const t0 = perfNow();\n let done = false;\n return () => {\n if (done) return;\n done = true;\n this.record(name, perfNow() - t0, desc);\n };\n }\n\n /** Time an async (or sync) function, recording its elapsed duration. */\n async measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T> {\n const end = this.start(name, desc);\n try {\n return await fn();\n } finally {\n end();\n }\n }\n\n /**\n * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call\n * adds `dur` to the running total for `name` and increments a counter; the\n * mark serializes as `name;dur=<sum>;desc=\"<count> <unit>\"` (or just the\n * bare count when no `unit` is given).\n *\n * Use this for high-frequency phases — one SQL query, one hook execution —\n * where recording a distinct mark per event would blow the header out to\n * hundreds of entries. The single `db;dur=210;desc=\"6 queries\"` member is\n * both the total DB time and the query count, which is the number most\n * useful for spotting N sequential round-trips.\n *\n * The aggregate mark is inserted into the record stream the first time its\n * name is seen, so it keeps its natural position relative to explicit marks\n * (e.g. before the outer `total`, which is recorded last).\n */\n count(name: string, dur: number, unit?: string): void {\n const add = Number.isFinite(dur) && dur > 0 ? dur : 0;\n const aggregates = (this._aggregates ??= new Map());\n let entry = aggregates.get(name);\n if (!entry) {\n const mark: ServerTimingMark = { name, dur: 0 };\n entry = { mark, count: 0, unit };\n aggregates.set(name, entry);\n this._marks.push(mark);\n }\n entry.count += 1;\n entry.mark.dur += add;\n if (unit) entry.unit = unit;\n entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);\n }\n\n /** Snapshot of recorded marks, in record order. */\n marks(): readonly ServerTimingMark[] {\n return this._marks;\n }\n\n /** Serialize to a `Server-Timing` header value (`''` when empty). */\n toHeader(): string {\n return formatServerTiming(this._marks);\n }\n}\n\n// --- Ambient (request-scoped) collector -------------------------------\n\n/**\n * The ambient collector lives in ONE process-wide `AsyncLocalStorage`, pinned\n * to a global-registry symbol rather than a plain module-level `const`.\n *\n * Why: this module is consumed from many packages and can legitimately be\n * loaded more than once in a single process — the ESM build (`dist/index.js`)\n * and the CJS build (`dist/index.cjs`) are distinct module instances, and a\n * bundler may inline yet another copy. A plain `const store` would give each\n * copy its OWN store, so a request scope opened through one copy (the HTTP\n * server's `runWithPerfTiming`) would be invisible to code reading the ambient\n * collector through another copy (the SQL driver, the ObjectQL engine) — the\n * cross-layer `db` / `auth` / `hooks` spans would silently never record.\n * `Symbol.for` resolves to the same symbol across every copy, so they all share\n * the one store.\n */\nconst STORE_KEY = Symbol.for('@objectstack/observability:perf-timing-store');\nconst globalStore = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfTiming> | undefined>;\nconst store: AsyncLocalStorage<PerfTiming> =\n globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage<PerfTiming>());\n\n/** Run `fn` with `timing` as the ambient collector for the async call chain. */\nexport function runWithPerfTiming<T>(timing: PerfTiming, fn: () => T): T {\n return store.run(timing, fn);\n}\n\n/** The collector for the current request, or `undefined` outside a request. */\nexport function currentPerfTiming(): PerfTiming | undefined {\n return store.getStore();\n}\n\n/** Record a phase on the ambient collector. No-op when none is active. */\nexport function recordServerTiming(name: string, dur: number, desc?: string): void {\n store.getStore()?.record(name, dur, desc);\n}\n\n/**\n * Begin timing a phase on the ambient collector. Returns an `end()` callback;\n * when no collector is active the returned callback is a no-op so call sites\n * stay branch-free.\n */\nexport function startServerTiming(name: string, desc?: string): () => void {\n const t = store.getStore();\n if (!t) return () => {};\n return t.start(name, desc);\n}\n\n/**\n * Time an async function on the ambient collector. When no collector is active\n * the function is awaited with zero timing overhead.\n */\nexport async function measureServerTiming<T>(\n name: string,\n fn: () => T | Promise<T>,\n desc?: string,\n): Promise<T> {\n const t = store.getStore();\n if (!t) return fn();\n return t.measure(name, fn, desc);\n}\n\n/**\n * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the\n * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is\n * active, so the hot-path call sites (the SQL driver's query listener, the hook\n * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.\n */\nexport function countServerTiming(name: string, dur: number, unit?: string): void {\n store.getStore()?.count(name, dur, unit);\n}\n\n/**\n * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto\n * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no\n * collector is active OR detail capture is off, so the hot-path call site pays\n * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.\n */\nexport function recordServerTimingDetail(category: string, label: string, dur: number): void {\n store.getStore()?.recordDetail(category, label, dur);\n}\n\n// --- Disclosure gate (WHO may see the timing) -------------------------\n\n/**\n * Per-request disclosure gate — the policy counterpart to the collector.\n *\n * The {@link PerfTiming} collector only MEASURES; whether the measured\n * `Server-Timing` header is returned to the client is a separate decision. When\n * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has\n * opted the whole environment in, so the gate opens up front and every response\n * carries the header. When it is turned on PER-REQUEST — the caller sends an\n * `X-OS-Debug-Timing` header — the header must stay withheld until the request\n * proves an admin/service identity: phase durations are a mild\n * backend-fingerprinting surface, so an ordinary user must never be able to pull\n * them just by sending a header. The request path flips the gate open with\n * {@link allowPerfDisclosure} once it has resolved a privileged principal.\n *\n * Keeping this out of {@link PerfTiming} preserves the collector's invariant\n * (\"it only measures, it never decides whether to emit\").\n *\n * Two levels, because global mode discloses the basic header to everyone but the\n * richer per-query detail must stay admin-only:\n * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by\n * global mode for everyone, or by a proven admin per-request).\n * - `privileged` — the principal is a proven admin/service. Gates the richer,\n * SQL-shape-bearing detail payload, which must NEVER reach an\n * ordinary caller even when global mode is on.\n */\nexport interface PerfDisclosureGate {\n /** Whether the basic collected timing may be disclosed to the client. */\n allowed: boolean;\n /**\n * Whether the principal is a proven admin/service — gates the richer detail\n * payload independently of `allowed`. Absent = not privileged.\n */\n privileged?: boolean;\n}\n\n/**\n * The disclosure gate lives in its OWN global-registry-pinned\n * `AsyncLocalStorage`, for the same cross-module-copy reason as the collector\n * store above: the middleware seeds the gate and the dispatcher (a different\n * package, possibly a different module copy) flips it open — both must see the\n * one store.\n */\nconst GATE_KEY = Symbol.for('@objectstack/observability:perf-disclosure-gate');\nconst globalGate = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfDisclosureGate> | undefined>;\nconst gateStore: AsyncLocalStorage<PerfDisclosureGate> =\n globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage<PerfDisclosureGate>());\n\n/**\n * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.\n * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`\n * settles to decide whether to emit the header.\n */\nexport function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T {\n return gateStore.run(gate, fn);\n}\n\n/**\n * Open the ambient disclosure gate — the request has proven an admin/service\n * identity, so it may see its own `Server-Timing` header AND the richer detail\n * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A\n * no-op when no gate is active (perf-tuning off), so the call site stays\n * branch-free.\n */\nexport function allowPerfDisclosure(): void {\n const g = gateStore.getStore();\n if (g) {\n g.allowed = true;\n g.privileged = true;\n }\n}\n\n/** Whether the ambient disclosure gate is open. `false` when none is active. */\nexport function isPerfDisclosureAllowed(): boolean {\n return gateStore.getStore()?.allowed ?? false;\n}\n\n/**\n * Whether the ambient principal is a proven admin/service — gates the richer\n * detail payload. `false` when no gate is active or only global-mode disclosure\n * (not a proven admin) opened it.\n */\nexport function isPerfDisclosurePrivileged(): boolean {\n return gateStore.getStore()?.privileged ?? false;\n}\n\n/**\n * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header\n * (#2408 perf-tuning gating). The header exposes internal phase durations — a\n * mild backend-fingerprinting surface — so when timing is opened per-request via\n * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:\n *\n * - `isSystem` — internal/engine self-calls,\n * - `principalKind` `service` / `system` — service tokens & the system seed,\n * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.\n *\n * Ordinary human/guest/agent callers get `false`, so sending the debug header\n * yields no header for them. Global (env/option) perf mode bypasses this — it\n * opened the disclosure gate up front for the whole environment.\n *\n * This is the ONE definition of \"who may pull per-request timings\", shared by\n * every HTTP entry point that resolves a principal — the runtime dispatcher\n * (`timedResolveExecutionContext`), the REST server, and the standalone Hono\n * CRUD surface — so a new admin-serving path can never silently under- or\n * over-disclose by hand-rolling its own rule (#3361).\n */\nexport function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean {\n if (!ec) return false;\n if (ec.isSystem === true) return true;\n if (ec.principalKind === 'service' || ec.principalKind === 'system') return true;\n return ec.posture === 'PLATFORM_ADMIN' || ec.posture === 'TENANT_ADMIN';\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/service-names.ts","../src/semconv.ts","../src/http-transport-metrics.ts","../src/metrics-exporters.ts","../src/error-exporters.ts","../src/loggers.ts","../src/perf-timing.ts"],"sourcesContent":["// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical service-registry names for the host's observability\n * backends. Plugins look these up to discover the configured\n * {@link MetricsRegistry} / {@link ErrorReporter} without each host\n * having to thread observability config through every plugin\n * constructor.\n *\n * See `@objectstack/runtime` → `ObservabilityServicePlugin` for the\n * registration side.\n */\nexport const OBSERVABILITY_METRICS_SERVICE = 'observability:metrics';\nexport const OBSERVABILITY_ERRORS_SERVICE = 'observability:errors';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Semantic conventions — canonical metric names emitted by the\n * framework. Listed here so hosts can wire alerts/dashboards against\n * a stable namespace and so call sites don't sprinkle string\n * literals through the code base.\n *\n * Naming follows Prometheus conventions:\n *\n * - snake_case identifiers.\n * - `_total` suffix for monotonic counters.\n * - `_ms`, `_seconds`, `_bytes` suffixes for histograms / gauges\n * with units.\n *\n * Groups roughly mirror the framework subsystems that emit them.\n * Cloud-specific metrics (DO restarts, Workers Analytics Engine\n * writes, …) do NOT belong here — they are deployment-specific and\n * stay in the deployment repo.\n */\nexport const SEMCONV = {\n // ── HTTP — emitter differs per family, see each ────────────────────\n /**\n * Counter, labels: `method`, `route`, `status`. Emitted by the TRANSPORT\n * through the `IHttpServer.afterResponse` seam (#9835), so it covers\n * every inbound request on the server rather than only the routes the\n * runtime dispatcher registers.\n */\n httpRequestsTotal: 'http_requests_total',\n /**\n * Histogram (ms), labels: `method`, `route`. Emitted by the TRANSPORT\n * through the same seam (#9834). It measures the REQUEST as the transport\n * sees it — first sight to the response existing, middleware chain and\n * body parse included — not the handler's share of it.\n */\n httpRequestDurationMs: 'http_request_duration_ms',\n // ⛔ RETIRED — `http_request_errors_total` was removed in\n // `@objectstack/observability` 17.2.0 (#9834, ADR-0049 enforce-or-remove).\n // ⛔ Do not re-add the name. It was DECLARED here as a stable server-wide\n // signal and EMITTED only from `@objectstack/runtime`'s per-route wrapper,\n // on a THROWN handler — so it never saw auth's `getRawApp()` mount, the\n // REST data API, or any error a handler answered politely through\n // `errorResponseBase` (which sets a status and does not re-throw). No\n // transport-side emitter could preserve that population either: the\n // `IHttpServer.afterResponse` observation carries `{method, routePattern,\n // status, elapsedMs}` and no throw signal at all.\n // ⇒ Read the 5xx rate from `http_requests_total{status=~\"5..\"}` instead.\n // The transport emits that family through the seam, so it covers every\n // inbound surface (#9650 / #9835 / #10004) and carries the status label\n // this counter only stood in for. Maintainer ruling 2026-08-20.\n\n // ── Storage — emitted by `@objectstack/service-storage` adapters ──\n /** Counter, labels: `adapter` (`local`|`s3`|…), `op` (`get`|`put`|`delete`|`head`), `result` (`ok`|`error`). */\n storageOperationsTotal: 'storage_operations_total',\n /** Histogram (ms), labels: `adapter`, `op`. */\n storageOperationDurationMs: 'storage_operation_duration_ms',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n storageErrorsTotal: 'storage_errors_total',\n\n // ── Cache — emitted by `@objectstack/service-cache` adapters ──\n // Uniform emitter; what varies is who CONSULTS the service — see below.\n /**\n * Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`).\n *\n * ⚠️ A flat zero means \"NO CONFIGURED CONSUMER\", not \"no cache activity\",\n * and — unlike the HTTP families above — it is NOT an instrumentation gap.\n * The adapters hold the host's registry and count every call they receive\n * (#9832 wired that; #9951 pins it), so a zero here is TRUE. What it fails\n * to communicate is WHY.\n *\n * The why: nothing consults the `cache` service unconditionally. Every\n * production consumer is a rate-limit / budget counter store, and each is\n * gated on a declaration somebody has to write — better-auth's per-IP\n * counters (`rate_limit_max` / `rate_limit_window_seconds` in auth\n * settings), the dispatcher's inbound limiter and its declarative\n * per-endpoint buckets (an armed `rateLimit` budget; with none declared\n * the dispatcher registers no limiter at all), and the per-number OTP send\n * budget (an SMS send path). A default install declares none of them, so\n * this counter stays at 0 while the server handles traffic normally.\n *\n * ⇒ Read a flat `cache_*` as a question about CONFIGURATION, never as a 0%\n * hit rate or a broken adapter. Before trusting a cache hit-rate panel,\n * confirm at least one consumer above is actually armed.\n */\n cacheLookupsTotal: 'cache_lookups_total',\n /**\n * Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). Same\n * \"zero = no configured consumer\" reading as `cacheLookupsTotal` above.\n */\n cacheWritesTotal: 'cache_writes_total',\n /**\n * Counter, labels: `adapter`, `op`, `errorClass`. Same \"zero = no\n * configured consumer\" reading as `cacheLookupsTotal` above — a zero is\n * \"nothing was asked of the cache\", not \"every call succeeded\".\n */\n cacheErrorsTotal: 'cache_errors_total',\n\n // ── Background jobs — emitted by `@objectstack/runtime`'s AppPlugin ──\n /**\n * Counter, labels: `app`, `job`. Incremented when a DECLARED background\n * job could not be handed to the job service — i.e. the app booted green\n * but that job will never run (#4567). Any non-zero value is an outage of\n * the job, not a warning.\n */\n jobScheduleFailuresTotal: 'job_schedule_failures_total',\n\n // ── Package / registry-reader — emitted by `@objectstack/service-package` ──\n /** Counter, labels: `result` (`ok`|`miss`|`error`). */\n registryLookupsTotal: 'registry_lookups_total',\n /** Histogram (ms). */\n registryLookupDurationMs: 'registry_lookup_duration_ms',\n /** Counter, labels: `source` (`r2`|`http`|`local`), `result` (`hit`|`miss`|`error`). */\n registrySourceFetchesTotal: 'registry_source_fetches_total',\n} as const;\n\n/**\n * Backwards-compat alias. `RUNTIME_METRICS` was the original (HTTP-only)\n * constant name shipped from `@objectstack/runtime`; we keep it here so\n * existing code reading `RUNTIME_METRICS.httpRequestsTotal` continues\n * to work after the constants moved into this package.\n */\nexport const RUNTIME_METRICS = {\n httpRequestsTotal: SEMCONV.httpRequestsTotal,\n httpRequestDurationMs: SEMCONV.httpRequestDurationMs,\n // `httpRequestErrorsTotal` retired with its SEMCONV declaration above\n // (#9834). The alias is not a compatibility window of its own: there is no\n // emitter left to read, so keeping the name here would hand callers a\n // string nothing ever writes.\n} as const;\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { IHttpServer } from '@objectstack/spec/contracts';\nimport type { MetricsRegistry } from './contracts.js';\nimport { RUNTIME_METRICS } from './semconv.js';\n\n/**\n * What an `arm*` call in this module did:\n *\n * - `'armed'` — the emitting observer was registered on this call.\n * - `'already-armed'` — some earlier caller already armed this server for\n * THIS metric family; the seam emits it, and this call registered nothing\n * (first-wins).\n * - `'unsupported'` — the transport does not implement the\n * `IHttpServer.afterResponse` seam; it reports NO HTTP metrics (#9835:\n * zero there means \"not instrumented\", never \"no traffic\"), and the\n * caller must decide how to degrade — the runtime dispatcher falls back\n * to instrumenting its own routes.\n */\nexport type ArmHttpMetricResult = 'armed' | 'already-armed' | 'unsupported';\n\n/**\n * The name {@link armHttpRequestCounter} shipped with. Kept as an alias\n * rather than renamed: the export is already in the pending release, and the\n * two families arm through separate entry points anyway.\n */\nexport type ArmHttpRequestCounterResult = ArmHttpMetricResult;\n\n/**\n * The per-server latch behind the contract's ownership rule. A registered\n * global-registry symbol (`Symbol.for`), not a module-level WeakSet, so the\n * latch cannot fork if two copies of this module ever coexist in one\n * process — the whole point is that there is exactly ONE latch per server\n * object, whoever asks.\n */\nconst HTTP_REQUEST_COUNTER_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestCounterArmed',\n);\n\n/**\n * Arm `http_requests_total{method,route,status}` on a transport through the\n * `IHttpServer.afterResponse` observation seam (#9835) — AT MOST ONCE per\n * server, whoever calls first.\n *\n * ## Why arming is centralized here\n *\n * The contract's ownership rule says a request must never be double-counted,\n * and two composition layers legitimately hold both a server and a metrics\n * registry: the transport's own hosting plugin (`HonoServerPlugin`, which\n * the 2026-08-18 ruling on #9650 made the counter's home) and the runtime\n * dispatcher (whose `observability.metrics` config is the wiring the docs\n * demonstrate). When a host hands ONE registry to both — the ordinary case —\n * two independently-registered observers would land every request on the\n * same series twice: exactly the #9833 distortion, rebuilt one seam over.\n * Routing every arming through this function makes \"exactly one\n * counter-emitting observer per server\" structural: the first caller arms\n * (in the shipped composition that is the transport plugin, in Phase 1),\n * every later caller is told the seam already counts.\n *\n * The label shape is pinned by the contract: `route` is the transport's\n * `routePattern` — the registered PATTERN, never the concrete path — and\n * `status` is stringified for the label set. Emission goes through the\n * transport's observer-isolation guarantee, so a throwing registry cannot\n * break a response.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper: the latch is per object identity, and a wrapper\n * would both fork the latch and (per #5122) risk erasing the optional\n * member this function feature-detects.\n * @param metrics - The registry the counter lands in. First caller wins; a\n * second registry offered later is NOT added (the contract's one-owner\n * rule), and the result says so.\n */\nexport function armHttpRequestCounter(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_COUNTER_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_COUNTER_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_COUNTER_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.counter(RUNTIME_METRICS.httpRequestsTotal, {\n method: observation.method,\n route: observation.routePattern,\n status: String(observation.status),\n });\n });\n return 'armed';\n}\n\n/**\n * The duration family's own latch. A SEPARATE registered symbol from the\n * counter's, deliberately: the two families are armed through separate entry\n * points and gated separately on the dispatcher's per-route wrapper, so a\n * host that arms one must not silently latch the other away.\n */\nconst HTTP_REQUEST_DURATION_ARMED = Symbol.for(\n 'objectstack.observability.httpRequestDurationArmed',\n);\n\n/**\n * Arm `http_request_duration_ms{method,route}` on a transport through the\n * `IHttpServer.afterResponse` observation seam — AT MOST ONCE per server,\n * whoever calls first. The duration half of #9834, built on the mechanism\n * #9835 proved out for `http_requests_total`.\n *\n * ## Why the histogram has to move too\n *\n * #9835 moved only the counter, which left the docs' two derived signals\n * inconsistent with each other: 5xx rate saw every inbound surface while p95\n * latency still saw the dispatcher's own routes. An operator reading one\n * dashboard got request volume for `/api/v1/*` beside a latency panel with no\n * series for it — and the worse reading is the p95 that IS drawn, computed\n * from dispatcher routes only and presented as the server's.\n *\n * ## ⚠️ The observation WINDOW changes with the emitter\n *\n * The dispatcher's per-route wrapper timed `await handler(req, res)` — handler\n * latency. This seam times the transport's own `use('*')` around\n * `await next()`, which is what {@link HttpResponseObservation.elapsedMs}\n * means: \"from the transport first seeing the request to the response\n * existing\". That includes the middleware chain and body parse, so the series\n * can only move UP, never down. It is the number an operator's latency panel\n * should have been showing — the request's latency rather than one layer's\n * share of it — but it is a visible change in an existing series, so it is\n * stated here, in the changeset, and in `docs/OBSERVABILITY.md` rather than\n * left for a dashboard to discover.\n *\n * The label set is unchanged and stays the SEMCONV-declared `{method,route}`:\n * `route` is the transport's `routePattern` (the registered PATTERN, never the\n * concrete path), and no `status` label is added — a histogram split by status\n * is a different series shape than the one the docs tell operators to graph.\n *\n * @param server - The transport. Pass the RAW registered `http.server`\n * instance, not a wrapper — the latch is per object identity, and (per\n * #5122) a wrapper risks erasing the optional member this feature-detects.\n * @param metrics - The registry the histogram lands in. First caller wins.\n */\nexport function armHttpRequestDurationHistogram(\n server: IHttpServer,\n metrics: MetricsRegistry,\n): ArmHttpMetricResult {\n if (typeof server.afterResponse !== 'function') return 'unsupported';\n const latched = server as IHttpServer & { [HTTP_REQUEST_DURATION_ARMED]?: boolean };\n if (latched[HTTP_REQUEST_DURATION_ARMED]) return 'already-armed';\n latched[HTTP_REQUEST_DURATION_ARMED] = true;\n server.afterResponse((observation) => {\n metrics.histogram(\n RUNTIME_METRICS.httpRequestDurationMs,\n observation.elapsedMs,\n { method: observation.method, route: observation.routePattern },\n );\n });\n return 'armed';\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { MetricsRegistry, MetricSample } from './contracts.js';\n\n// ─── Noop ─────────────────────────────────────────────────────────────\n\n/**\n * No-op metrics registry — the default. Discards every observation.\n * Production deployments should swap this for a real registry; tests\n * can use {@link InMemoryMetricsRegistry} to assert emissions.\n */\nexport class NoopMetricsRegistry implements MetricsRegistry {\n counter(): void { }\n histogram(): void { }\n gauge(): void { }\n}\n\n// ─── In-memory (tests + dev inspection) ───────────────────────────────\n\n/**\n * In-memory registry used for tests and local inspection. Stores\n * every observation in insertion order; query via the helpers below\n * or read {@link samples} directly.\n *\n * Not intended for production — unbounded growth.\n */\nexport class InMemoryMetricsRegistry implements MetricsRegistry {\n readonly samples: MetricSample[] = [];\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.samples.push({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.samples.push({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n /** Sum of counter increments matching `name` and optionally a label subset. */\n totalCounter(name: string, labelMatch: Record<string, string> = {}): number {\n return this.samples\n .filter(s => s.kind === 'counter' && s.name === name && matchesLabels(s.labels, labelMatch))\n .reduce((acc, s) => acc + s.value, 0);\n }\n\n /** Raw histogram observations matching `name` and optionally a label subset. */\n histogramValues(name: string, labelMatch: Record<string, string> = {}): number[] {\n return this.samples\n .filter(s => s.kind === 'histogram' && s.name === name && matchesLabels(s.labels, labelMatch))\n .map(s => s.value);\n }\n\n /** Last gauge value matching `name` and optionally a label subset, or undefined. */\n lastGauge(name: string, labelMatch: Record<string, string> = {}): number | undefined {\n for (let i = this.samples.length - 1; i >= 0; i--) {\n const s = this.samples[i];\n if (s.kind === 'gauge' && s.name === name && matchesLabels(s.labels, labelMatch)) {\n return s.value;\n }\n }\n return undefined;\n }\n\n /** Clear all recorded samples. */\n reset(): void {\n this.samples.length = 0;\n }\n}\n\nfunction matchesLabels(actual: Record<string, string>, expected: Record<string, string>): boolean {\n for (const [k, v] of Object.entries(expected)) {\n if (actual[k] !== v) return false;\n }\n return true;\n}\n\n// ─── Console (development) ────────────────────────────────────────────\n\n/**\n * Console metrics registry — prints one line per observation. Useful\n * during local development to confirm that instrumentation is firing.\n *\n * Not intended for production: writing every observation to stdout\n * defeats Prometheus / OTLP pipelines and dominates request latency.\n */\nexport class ConsoleMetricsRegistry implements MetricsRegistry {\n constructor(private readonly opts: { sink?: (line: string) => void; prefix?: string } = {}) { }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.emit('counter', name, value, labels);\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('histogram', name, value, labels);\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.emit('gauge', name, value, labels);\n }\n\n private emit(kind: string, name: string, value: number, labels: Record<string, string>): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.log(s); });\n const prefix = this.opts.prefix ?? '[metric]';\n const labelStr = Object.entries(labels)\n .map(([k, v]) => `${k}=${JSON.stringify(v)}`)\n .join(' ');\n sink(`${prefix} ${kind} ${name} ${value}${labelStr ? ' ' + labelStr : ''}`);\n } catch {\n // Per contract: never throw from a metric call site.\n }\n }\n}\n\n// ─── OTLP / HTTP (Prometheus via OTel Collector, Grafana Cloud, …) ────\n\n/**\n * Configuration for {@link OtlpHttpMetricsRegistry}.\n */\nexport interface OtlpHttpExporterOptions {\n /**\n * OTLP/HTTP metrics endpoint, e.g. `http://otel-collector:4318/v1/metrics`.\n * The path is appended automatically if missing.\n */\n endpoint: string;\n\n /** Optional headers (Authorization, x-tenant, …). */\n headers?: Record<string, string>;\n\n /**\n * Resource attributes — `service.name`, `service.namespace`,\n * `deployment.environment`, etc. Merged into the OTLP `resource`\n * block on every export.\n */\n resource?: Record<string, string>;\n\n /**\n * Custom fetch implementation. Defaults to the global `fetch`.\n * Allows Workers / undici / node-fetch substitution and test\n * doubles.\n */\n fetch?: typeof fetch;\n\n /**\n * Called when an export attempt fails. Default: silently swallow\n * (per the contract that metric emission must not throw / log\n * loudly on the hot path).\n */\n onError?: (error: unknown) => void;\n\n /**\n * Maximum number of samples buffered before {@link OtlpHttpMetricsRegistry.flush}\n * is called automatically. Defaults to 1024.\n */\n maxBufferSize?: number;\n}\n\n/**\n * OTLP/HTTP metrics exporter.\n *\n * Buffers samples in memory and serialises them to the OpenTelemetry\n * Protocol JSON encoding when {@link flush} is called (manually or\n * automatically once the buffer hits the configured size).\n *\n * Intentionally does **not** start an interval timer in the constructor:\n * (a) it makes the exporter usable on Cloudflare Workers where\n * `setInterval` is restricted, and (b) it keeps unit tests deterministic.\n * Long-running hosts should call `flush()` on a schedule\n * (e.g. `setInterval(() => reg.flush(), 10_000)` on Node, or\n * `ctx.waitUntil(reg.flush())` from a Workers fetch handler).\n *\n * Only counters, histograms, and gauges are emitted — no support for\n * exemplars or aggregation temporality switches (the Collector handles\n * those on the upstream side).\n */\nexport class OtlpHttpMetricsRegistry implements MetricsRegistry {\n private buffer: MetricSample[] = [];\n private readonly endpoint: string;\n private readonly headers: Record<string, string>;\n private readonly resource: Record<string, string>;\n private readonly maxBufferSize: number;\n private readonly fetchImpl: typeof fetch;\n private readonly onError: (error: unknown) => void;\n\n constructor(options: OtlpHttpExporterOptions) {\n this.endpoint = normaliseEndpoint(options.endpoint);\n this.headers = options.headers ?? {};\n this.resource = options.resource ?? {};\n this.maxBufferSize = options.maxBufferSize ?? 1024;\n this.fetchImpl = options.fetch ?? (typeof fetch !== 'undefined' ? fetch.bind(globalThis) : noFetch);\n this.onError = options.onError ?? (() => { });\n }\n\n counter(name: string, labels: Record<string, string> = {}, value: number = 1): void {\n this.record({ name, kind: 'counter', value, labels, at: Date.now() });\n }\n histogram(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'histogram', value, labels, at: Date.now() });\n }\n gauge(name: string, value: number, labels: Record<string, string> = {}): void {\n this.record({ name, kind: 'gauge', value, labels, at: Date.now() });\n }\n\n private record(sample: MetricSample): void {\n this.buffer.push(sample);\n if (this.buffer.length >= this.maxBufferSize) {\n // Fire-and-forget: do not block the call site.\n void this.flush().catch(this.onError);\n }\n }\n\n /** Snapshot the current buffer (for tests). */\n peek(): readonly MetricSample[] {\n return this.buffer.slice();\n }\n\n /**\n * Send the buffered samples to the OTLP endpoint and clear the\n * buffer. Safe to call concurrently — each invocation takes a\n * snapshot before clearing.\n */\n async flush(): Promise<void> {\n if (this.buffer.length === 0) return;\n const samples = this.buffer;\n this.buffer = [];\n try {\n const body = JSON.stringify(serialiseToOtlp(samples, this.resource));\n const res = await this.fetchImpl(this.endpoint, {\n method: 'POST',\n headers: { 'content-type': 'application/json', ...this.headers },\n body,\n });\n if (!res.ok) {\n this.onError(new Error(`OTLP export returned HTTP ${res.status}`));\n }\n } catch (err) {\n this.onError(err);\n }\n }\n}\n\nfunction normaliseEndpoint(raw: string): string {\n const trimmed = raw.replace(/\\/+$/, '');\n if (/\\/v1\\/metrics$/.test(trimmed)) return trimmed;\n return trimmed + '/v1/metrics';\n}\n\nfunction noFetch(): never {\n throw new Error('OtlpHttpMetricsRegistry: no global fetch available; pass options.fetch');\n}\n\n/**\n * Minimal OTLP/JSON metrics serialiser.\n *\n * Implements the subset of <https://opentelemetry.io/docs/specs/otlp/>\n * that we actually emit — sums for counters, gauges for gauges,\n * histograms encoded as bucketless \"summary\"-style histograms with\n * explicit per-sample data points. The Collector accepts this shape\n * and applies its own bucketing.\n *\n * Aggregation temporality is set to DELTA (2) because every flush\n * sends only the samples accumulated since the last flush.\n */\nfunction serialiseToOtlp(samples: MetricSample[], resource: Record<string, string>): unknown {\n const byName = new Map<string, { kind: MetricSample['kind']; points: MetricSample[] }>();\n for (const s of samples) {\n const existing = byName.get(s.name);\n if (existing) existing.points.push(s);\n else byName.set(s.name, { kind: s.kind, points: [s] });\n }\n\n const metrics = Array.from(byName.entries()).map(([name, { kind, points }]) => {\n if (kind === 'counter') {\n return {\n name,\n sum: {\n dataPoints: points.map(p => toNumberPoint(p)),\n aggregationTemporality: 2,\n isMonotonic: true,\n },\n };\n }\n if (kind === 'gauge') {\n return { name, gauge: { dataPoints: points.map(p => toNumberPoint(p)) } };\n }\n // histogram: emit a histogram with a single bucket boundary so the\n // Collector treats it as a recorded distribution.\n return {\n name,\n histogram: {\n aggregationTemporality: 2,\n dataPoints: points.map(p => ({\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n count: '1',\n sum: p.value,\n bucketCounts: ['0', '1'],\n explicitBounds: [p.value],\n })),\n },\n };\n });\n\n return {\n resourceMetrics: [{\n resource: { attributes: toAttributes(resource) },\n scopeMetrics: [{\n scope: { name: '@objectstack/observability', version: '0.1.0' },\n metrics,\n }],\n }],\n };\n}\n\nfunction toNumberPoint(p: MetricSample): unknown {\n return {\n attributes: toAttributes(p.labels),\n timeUnixNano: String(p.at) + '000000',\n startTimeUnixNano: String(p.at) + '000000',\n asDouble: p.value,\n };\n}\n\nfunction toAttributes(labels: Record<string, string>): unknown[] {\n return Object.entries(labels).map(([key, value]) => ({\n key,\n value: { stringValue: value },\n }));\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { ErrorReporter, CapturedError } from './contracts.js';\n\n/** No-op reporter — the default. */\nexport class NoopErrorReporter implements ErrorReporter {\n captureException(): void { }\n}\n\n/** In-memory reporter for tests. */\nexport class InMemoryErrorReporter implements ErrorReporter {\n readonly captured: CapturedError[] = [];\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n this.captured.push({ error, context, at: Date.now() });\n }\n\n reset(): void {\n this.captured.length = 0;\n }\n}\n\n/**\n * Console error reporter — writes a structured JSON line to stderr per\n * captured exception. Convenient default for local development and for\n * any deployment that ships stderr to a log aggregator (e.g. Loki,\n * fluent-bit) but does not have a dedicated APM.\n *\n * Stack traces are included when the captured value is an `Error`.\n */\nexport class ConsoleErrorReporter implements ErrorReporter {\n constructor(private readonly opts: { sink?: (line: string) => void } = {}) { }\n\n captureException(error: unknown, context: Record<string, unknown> = {}): void {\n try {\n const sink = this.opts.sink ?? ((s) => { console.error(s); });\n const record: Record<string, unknown> = {\n ts: new Date().toISOString(),\n level: 'error',\n msg: error instanceof Error ? error.message : String(error),\n context,\n };\n if (error instanceof Error && error.stack) {\n record.stack = error.stack;\n }\n sink(JSON.stringify(record));\n } catch {\n // Per contract: error reporting must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\nimport type { Logger } from './contracts.js';\n\n/** Recognised log levels in increasing severity order. */\nexport const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'] as const;\nexport type LogLevel = (typeof LOG_LEVELS)[number];\n\nconst LEVEL_PRIORITY: Record<LogLevel, number> = {\n debug: 10,\n info: 20,\n warn: 30,\n error: 40,\n fatal: 50,\n};\n\n/** No-op logger — discards every message. */\nexport class NoopLogger implements Logger {\n debug(): void { }\n info(): void { }\n warn(): void { }\n error(): void { }\n fatal(): void { }\n child(): Logger { return this; }\n}\n\n/**\n * Console logger — pretty-printed messages for local development.\n *\n * Not suitable for production where structured JSON is required;\n * use {@link JsonLogger} there instead.\n */\nexport class ConsoleLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n } = {},\n ) { }\n\n private get threshold(): number {\n return LEVEL_PRIORITY[this.opts.level ?? 'info'];\n }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> {\n return this.opts.context ?? {};\n }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new ConsoleLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const merged = { ...this.context, ...(meta ?? {}) };\n const tail = Object.keys(merged).length ? ' ' + JSON.stringify(merged) : '';\n const line = `[${level}] ${msg}${tail}`;\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n\n/**\n * JSON logger — one JSON object per line on stdout (errors on stderr).\n *\n * Matches the shape that Loki / fluent-bit / Cloudflare Logpush ingest\n * by default. Every record contains `ts`, `level`, `msg`, and any\n * accumulated child-context plus per-call `meta`.\n *\n * Use this in production. Use {@link ConsoleLogger} during development\n * for human-friendly output.\n */\nexport class JsonLogger implements Logger {\n constructor(\n private readonly opts: {\n level?: LogLevel;\n context?: Record<string, unknown>;\n sink?: { log: (s: string) => void; error: (s: string) => void };\n /** Optional fields injected into every record (`service`, `env`, …). */\n base?: Record<string, unknown>;\n /** Wall clock for tests. */\n now?: () => Date;\n } = {},\n ) { }\n\n private get threshold(): number { return LEVEL_PRIORITY[this.opts.level ?? 'info']; }\n private get sink() {\n return this.opts.sink ?? { log: (s: string) => console.log(s), error: (s: string) => console.error(s) };\n }\n private get context(): Record<string, unknown> { return this.opts.context ?? {}; }\n private get base(): Record<string, unknown> { return this.opts.base ?? {}; }\n private get now(): () => Date { return this.opts.now ?? (() => new Date()); }\n\n debug(message: string, meta?: Record<string, unknown>): void { this.emit('debug', message, meta); }\n info(message: string, meta?: Record<string, unknown>): void { this.emit('info', message, meta); }\n warn(message: string, meta?: Record<string, unknown>): void { this.emit('warn', message, meta); }\n error(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('error', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n fatal(message: string, error?: Error, meta?: Record<string, unknown>): void {\n this.emit('fatal', message, { ...(meta ?? {}), ...(error ? { error: error.message, stack: error.stack } : {}) });\n }\n child(context: Record<string, unknown>): Logger {\n return new JsonLogger({ ...this.opts, context: { ...this.context, ...context } });\n }\n\n private emit(level: LogLevel, msg: string, meta?: Record<string, unknown>): void {\n if (LEVEL_PRIORITY[level] < this.threshold) return;\n try {\n const record = {\n ts: this.now().toISOString(),\n level,\n msg,\n ...this.base,\n ...this.context,\n ...(meta ?? {}),\n };\n const line = JSON.stringify(record);\n if (level === 'error' || level === 'fatal') this.sink.error(line);\n else this.sink.log(line);\n } catch {\n // Log emission must never throw.\n }\n }\n}\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Per-request performance timing - a tiny, dependency-free collector that\n * accumulates named phase durations during a single request and serializes\n * them into the W3C `Server-Timing` response header.\n *\n * @see <https://www.w3.org/TR/server-timing/>\n * @see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>\n *\n * Two ways to record:\n *\n * 1. **Explicit collector.** Hold a {@link PerfTiming} instance and call\n * `start()` / `record()` / `measure()` directly. The HTTP adapter owns\n * the instance for a request.\n *\n * 2. **Ambient collector.** Run a request inside {@link runWithPerfTiming}\n * and any framework code on that async call chain records phases via the\n * free functions ({@link measureServerTiming}, {@link startServerTiming},\n * {@link recordServerTiming}, {@link countServerTiming}) without threading\n * the request object through every layer. When no collector is active the\n * free functions are cheap no-ops, so call sites pay nothing when the\n * feature is off. High-frequency phases (per SQL query, per hook) use\n * {@link countServerTiming} to fold into one aggregate mark carrying a\n * total duration and an event count.\n *\n * `Server-Timing` exposes internal phase durations to any client, which is a\n * (mild) information-disclosure surface - it helps an attacker profile the\n * backend. Emission is therefore opt-in (\"perf-tuning mode\"); the collector\n * itself never decides whether to emit, it only measures.\n */\n\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * One recorded phase of a request's server-side processing, serialized as a\n * single member of the `Server-Timing` header.\n */\nexport interface ServerTimingMark {\n /** Metric name. Coerced to a Server-Timing token on record. */\n name: string;\n /** Duration in milliseconds. */\n dur: number;\n /** Optional human-readable description (rendered as the quoted `desc`). */\n desc?: string;\n}\n\n/**\n * One recorded sub-event when DETAIL capture is on (see\n * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds\n * every query into a single count+duration — a detail sample keeps the\n * individual event so an admin can see *which* queries ran and which was\n * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED\n * statement, bindings stripped — the query shape, never literal row values).\n */\nexport interface ServerTimingDetail {\n /** Event label — e.g. a parametrized SQL statement (no bindings). */\n label: string;\n /** Duration in milliseconds. */\n dur: number;\n}\n\n/**\n * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not\n * affected by wall-clock adjustments); falls back to `Date.now()` on the rare\n * runtime where `performance` is unavailable.\n */\nexport function perfNow(): number {\n try {\n return performance.now();\n } catch {\n return Date.now();\n }\n}\n\n/** Characters not allowed in a Server-Timing metric name (a token). */\nconst NAME_UNSAFE = /[^A-Za-z0-9_-]+/g;\n\nconst UNDERSCORE = 0x5f;\n\n/**\n * Coerce an arbitrary string into a Server-Timing token: non-token runs become\n * a single underscore, then leading/trailing underscores are trimmed.\n *\n * The trim is a linear scan rather than a `/^_+|_+$/g` regex on purpose - the\n * anchored `_+$` quantifier backtracks polynomially on underscore-heavy input\n * (CodeQL js/polynomial-redos), and this name comes from a public-API argument.\n */\nfunction sanitizeName(name: string): string {\n const collapsed = String(name).replace(NAME_UNSAFE, '_');\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed.charCodeAt(start) === UNDERSCORE) start++;\n while (end > start && collapsed.charCodeAt(end - 1) === UNDERSCORE) end--;\n return collapsed.slice(start, end);\n}\n\n/**\n * Make a description safe to embed in a quoted-string. Backslashes and double\n * quotes would terminate the quoting; control chars (incl. CR/LF) could forge\n * headers. Collapse anything outside a conservative printable set to a space.\n */\nfunction sanitizeDesc(desc: string): string {\n let out = '';\n for (const ch of String(desc)) {\n const code = ch.codePointAt(0)!;\n // Printable ASCII excluding `\"` (0x22) and `\\` (0x5C); drop the rest.\n if (code >= 0x20 && code < 0x7f && ch !== '\"' && ch !== '\\\\') {\n out += ch;\n } else {\n out += ' ';\n }\n }\n return out.replace(/ +/g, ' ').trim();\n}\n\n/** Round to at most 2 decimals without trailing-zero noise (`12.3`, not `12.30`). */\nfunction fmtDur(dur: number): string {\n if (!Number.isFinite(dur)) return '0';\n return String(Math.round(dur * 100) / 100);\n}\n\n/**\n * Serialize marks into a `Server-Timing` header value. Marks with an empty\n * name after sanitization are dropped (the grammar requires a token). Returns\n * `''` when there is nothing to emit so callers can skip the header.\n */\nexport function formatServerTiming(marks: readonly ServerTimingMark[]): string {\n const parts: string[] = [];\n for (const m of marks) {\n const name = sanitizeName(m.name);\n if (!name) continue;\n let part = `${name};dur=${fmtDur(m.dur)}`;\n if (m.desc) {\n const desc = sanitizeDesc(m.desc);\n if (desc) part += `;desc=\"${desc}\"`;\n }\n parts.push(part);\n }\n return parts.join(', ');\n}\n\n/**\n * Collector for one request's timing phases. Not thread-safe by design - one\n * instance belongs to one request. All methods are allocation-light and never\n * throw on the hot path.\n */\nexport class PerfTiming {\n private readonly _marks: ServerTimingMark[] = [];\n /**\n * Live aggregate marks by name (see {@link count}). Lazily created so a\n * request that never aggregates pays nothing. Each entry points at a mark\n * already inserted into {@link _marks}, mutated in place as events arrive.\n */\n private _aggregates?: Map<string, { mark: ServerTimingMark; count: number; unit?: string }>;\n /**\n * Per-event detail samples by category, populated only while detail capture\n * is on (see {@link enableDetail}). Lazily created so a request that never\n * enables detail pays nothing.\n */\n private _detail?: Map<string, ServerTimingDetail[]>;\n private _detailOn = false;\n /**\n * Hard cap on stored detail samples per category — detail is only ever on\n * for a deliberate debug request, but a pathological request must not pin\n * unbounded memory. The aggregate {@link count} still reflects the true\n * total; only the retained per-event list is bounded.\n */\n private static readonly DETAIL_CAP = 1000;\n\n /** Record an already-measured phase. */\n record(name: string, dur: number, desc?: string): void {\n this._marks.push({ name, dur, desc });\n }\n\n /**\n * Turn on per-event DETAIL capture for this request. Off by default so the\n * hot path never allocates a per-event list; the HTTP middleware enables it\n * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.\n */\n enableDetail(): void {\n this._detailOn = true;\n }\n\n /** Whether per-event detail capture is on. */\n get detailEnabled(): boolean {\n return this._detailOn;\n }\n\n /**\n * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op\n * unless {@link enableDetail} was called, so the hot-path call site (the SQL\n * driver's query listener) pays only a boolean check when detail is off.\n * Bounded by {@link DETAIL_CAP}; excess events still count toward the\n * aggregate via {@link count} but are not retained individually.\n */\n recordDetail(category: string, label: string, dur: number): void {\n if (!this._detailOn) return;\n const detail = (this._detail ??= new Map());\n let list = detail.get(category);\n if (!list) {\n list = [];\n detail.set(category, list);\n }\n if (list.length >= PerfTiming.DETAIL_CAP) return;\n list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });\n }\n\n /** Retained detail samples for `category`, in record order (empty when none). */\n details(category: string): readonly ServerTimingDetail[] {\n return this._detail?.get(category) ?? [];\n }\n\n /**\n * Begin timing a phase. Returns an idempotent `end()` - the first call\n * records the elapsed duration; later calls are ignored, so it is safe to\n * call from both a success and an error path.\n */\n start(name: string, desc?: string): () => void {\n const t0 = perfNow();\n let done = false;\n return () => {\n if (done) return;\n done = true;\n this.record(name, perfNow() - t0, desc);\n };\n }\n\n /** Time an async (or sync) function, recording its elapsed duration. */\n async measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T> {\n const end = this.start(name, desc);\n try {\n return await fn();\n } finally {\n end();\n }\n }\n\n /**\n * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call\n * adds `dur` to the running total for `name` and increments a counter; the\n * mark serializes as `name;dur=<sum>;desc=\"<count> <unit>\"` (or just the\n * bare count when no `unit` is given).\n *\n * Use this for high-frequency phases — one SQL query, one hook execution —\n * where recording a distinct mark per event would blow the header out to\n * hundreds of entries. The single `db;dur=210;desc=\"6 queries\"` member is\n * both the total DB time and the query count, which is the number most\n * useful for spotting N sequential round-trips.\n *\n * The aggregate mark is inserted into the record stream the first time its\n * name is seen, so it keeps its natural position relative to explicit marks\n * (e.g. before the outer `total`, which is recorded last).\n */\n count(name: string, dur: number, unit?: string): void {\n const add = Number.isFinite(dur) && dur > 0 ? dur : 0;\n const aggregates = (this._aggregates ??= new Map());\n let entry = aggregates.get(name);\n if (!entry) {\n const mark: ServerTimingMark = { name, dur: 0 };\n entry = { mark, count: 0, unit };\n aggregates.set(name, entry);\n this._marks.push(mark);\n }\n entry.count += 1;\n entry.mark.dur += add;\n if (unit) entry.unit = unit;\n entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);\n }\n\n /** Snapshot of recorded marks, in record order. */\n marks(): readonly ServerTimingMark[] {\n return this._marks;\n }\n\n /** Serialize to a `Server-Timing` header value (`''` when empty). */\n toHeader(): string {\n return formatServerTiming(this._marks);\n }\n}\n\n// --- Ambient (request-scoped) collector -------------------------------\n\n/**\n * The ambient collector lives in ONE process-wide `AsyncLocalStorage`, pinned\n * to a global-registry symbol rather than a plain module-level `const`.\n *\n * Why: this module is consumed from many packages and can legitimately be\n * loaded more than once in a single process — the ESM build (`dist/index.js`)\n * and the CJS build (`dist/index.cjs`) are distinct module instances, and a\n * bundler may inline yet another copy. A plain `const store` would give each\n * copy its OWN store, so a request scope opened through one copy (the HTTP\n * server's `runWithPerfTiming`) would be invisible to code reading the ambient\n * collector through another copy (the SQL driver, the ObjectQL engine) — the\n * cross-layer `db` / `auth` / `hooks` spans would silently never record.\n * `Symbol.for` resolves to the same symbol across every copy, so they all share\n * the one store.\n */\nconst STORE_KEY = Symbol.for('@objectstack/observability:perf-timing-store');\nconst globalStore = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfTiming> | undefined>;\nconst store: AsyncLocalStorage<PerfTiming> =\n globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage<PerfTiming>());\n\n/** Run `fn` with `timing` as the ambient collector for the async call chain. */\nexport function runWithPerfTiming<T>(timing: PerfTiming, fn: () => T): T {\n return store.run(timing, fn);\n}\n\n/** The collector for the current request, or `undefined` outside a request. */\nexport function currentPerfTiming(): PerfTiming | undefined {\n return store.getStore();\n}\n\n/** Record a phase on the ambient collector. No-op when none is active. */\nexport function recordServerTiming(name: string, dur: number, desc?: string): void {\n store.getStore()?.record(name, dur, desc);\n}\n\n/**\n * Begin timing a phase on the ambient collector. Returns an `end()` callback;\n * when no collector is active the returned callback is a no-op so call sites\n * stay branch-free.\n */\nexport function startServerTiming(name: string, desc?: string): () => void {\n const t = store.getStore();\n if (!t) return () => {};\n return t.start(name, desc);\n}\n\n/**\n * Time an async function on the ambient collector. When no collector is active\n * the function is awaited with zero timing overhead.\n */\nexport async function measureServerTiming<T>(\n name: string,\n fn: () => T | Promise<T>,\n desc?: string,\n): Promise<T> {\n const t = store.getStore();\n if (!t) return fn();\n return t.measure(name, fn, desc);\n}\n\n/**\n * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the\n * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is\n * active, so the hot-path call sites (the SQL driver's query listener, the hook\n * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.\n */\nexport function countServerTiming(name: string, dur: number, unit?: string): void {\n store.getStore()?.count(name, dur, unit);\n}\n\n/**\n * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto\n * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no\n * collector is active OR detail capture is off, so the hot-path call site pays\n * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.\n */\nexport function recordServerTimingDetail(category: string, label: string, dur: number): void {\n store.getStore()?.recordDetail(category, label, dur);\n}\n\n// --- Disclosure gate (WHO may see the timing) -------------------------\n\n/**\n * Per-request disclosure gate — the policy counterpart to the collector.\n *\n * The {@link PerfTiming} collector only MEASURES; whether the measured\n * `Server-Timing` header is returned to the client is a separate decision. When\n * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has\n * opted the whole environment in, so the gate opens up front and every response\n * carries the header. When it is turned on PER-REQUEST — the caller sends an\n * `X-OS-Debug-Timing` header — the header must stay withheld until the request\n * proves an admin/service identity: phase durations are a mild\n * backend-fingerprinting surface, so an ordinary user must never be able to pull\n * them just by sending a header. The request path flips the gate open with\n * {@link allowPerfDisclosure} once it has resolved a privileged principal.\n *\n * Keeping this out of {@link PerfTiming} preserves the collector's invariant\n * (\"it only measures, it never decides whether to emit\").\n *\n * Two levels, because global mode discloses the basic header to everyone but the\n * richer per-query detail must stay admin-only:\n * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by\n * global mode for everyone, or by a proven admin per-request).\n * - `privileged` — the principal is a proven admin/service. Gates the richer,\n * SQL-shape-bearing detail payload, which must NEVER reach an\n * ordinary caller even when global mode is on.\n */\nexport interface PerfDisclosureGate {\n /** Whether the basic collected timing may be disclosed to the client. */\n allowed: boolean;\n /**\n * Whether the principal is a proven admin/service — gates the richer detail\n * payload independently of `allowed`. Absent = not privileged.\n */\n privileged?: boolean;\n}\n\n/**\n * The disclosure gate lives in its OWN global-registry-pinned\n * `AsyncLocalStorage`, for the same cross-module-copy reason as the collector\n * store above: the middleware seeds the gate and the dispatcher (a different\n * package, possibly a different module copy) flips it open — both must see the\n * one store.\n */\nconst GATE_KEY = Symbol.for('@objectstack/observability:perf-disclosure-gate');\nconst globalGate = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfDisclosureGate> | undefined>;\nconst gateStore: AsyncLocalStorage<PerfDisclosureGate> =\n globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage<PerfDisclosureGate>());\n\n/**\n * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.\n * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`\n * settles to decide whether to emit the header.\n */\nexport function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T {\n return gateStore.run(gate, fn);\n}\n\n/**\n * Open the ambient disclosure gate — the request has proven an admin/service\n * identity, so it may see its own `Server-Timing` header AND the richer detail\n * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A\n * no-op when no gate is active (perf-tuning off), so the call site stays\n * branch-free.\n */\nexport function allowPerfDisclosure(): void {\n const g = gateStore.getStore();\n if (g) {\n g.allowed = true;\n g.privileged = true;\n }\n}\n\n/** Whether the ambient disclosure gate is open. `false` when none is active. */\nexport function isPerfDisclosureAllowed(): boolean {\n return gateStore.getStore()?.allowed ?? false;\n}\n\n/**\n * Whether the ambient principal is a proven admin/service — gates the richer\n * detail payload. `false` when no gate is active or only global-mode disclosure\n * (not a proven admin) opened it.\n */\nexport function isPerfDisclosurePrivileged(): boolean {\n return gateStore.getStore()?.privileged ?? false;\n}\n\n/**\n * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header\n * (#2408 perf-tuning gating). The header exposes internal phase durations — a\n * mild backend-fingerprinting surface — so when timing is opened per-request via\n * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:\n *\n * - `isSystem` — internal/engine self-calls,\n * - `principalKind` `service` / `system` — service tokens & the system seed,\n * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.\n *\n * Ordinary human/guest/agent callers get `false`, so sending the debug header\n * yields no header for them. Global (env/option) perf mode bypasses this — it\n * opened the disclosure gate up front for the whole environment.\n *\n * This is the ONE definition of \"who may pull per-request timings\", shared by\n * every HTTP entry point that resolves a principal — the runtime dispatcher\n * (`timedResolveExecutionContext`), the REST server, and the standalone Hono\n * CRUD surface — so a new admin-serving path can never silently under- or\n * over-disclose by hand-rolling its own rule (#3361).\n */\nexport function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean {\n if (!ec) return false;\n if (ec.isSystem === true) return true;\n if (ec.principalKind === 'service' || ec.principalKind === 'system') return true;\n return ec.posture === 'PLATFORM_ADMIN' || ec.posture === 'TENANT_ADMIN';\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectstack/observability",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.2.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Observability contracts and exporters for ObjectStack — MetricsRegistry, ErrorReporter, Logger plus noop/console/OTLP-HTTP exporters. Deployment-target neutral; runtime and services depend on this so the same instrumentation works on Cloudflare Workers, Node, and self-hosted Kubernetes.",
|
|
6
6
|
"type": "module",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
}
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@objectstack/spec": "17.
|
|
17
|
+
"@objectstack/spec": "17.2.0"
|
|
18
18
|
},
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^26.2.0",
|