@objectstack/observability 15.1.0 → 16.0.0-rc.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/dist/index.cjs CHANGED
@@ -36,11 +36,17 @@ __export(index_exports, {
36
36
  PerfTiming: () => PerfTiming,
37
37
  RUNTIME_METRICS: () => RUNTIME_METRICS,
38
38
  SEMCONV: () => SEMCONV,
39
+ allowPerfDisclosure: () => allowPerfDisclosure,
40
+ countServerTiming: () => countServerTiming,
39
41
  currentPerfTiming: () => currentPerfTiming,
40
42
  formatServerTiming: () => formatServerTiming,
43
+ isPerfDisclosureAllowed: () => isPerfDisclosureAllowed,
44
+ isPerfDisclosurePrivileged: () => isPerfDisclosurePrivileged,
41
45
  measureServerTiming: () => measureServerTiming,
42
46
  perfNow: () => perfNow,
43
47
  recordServerTiming: () => recordServerTiming,
48
+ recordServerTimingDetail: () => recordServerTimingDetail,
49
+ runWithPerfDisclosure: () => runWithPerfDisclosure,
44
50
  runWithPerfTiming: () => runWithPerfTiming,
45
51
  startServerTiming: () => startServerTiming
46
52
  });
@@ -502,14 +508,49 @@ function formatServerTiming(marks) {
502
508
  }
503
509
  return parts.join(", ");
504
510
  }
505
- var PerfTiming = class {
511
+ var _PerfTiming = class _PerfTiming {
506
512
  constructor() {
507
513
  this._marks = [];
514
+ this._detailOn = false;
508
515
  }
509
516
  /** Record an already-measured phase. */
510
517
  record(name, dur, desc) {
511
518
  this._marks.push({ name, dur, desc });
512
519
  }
520
+ /**
521
+ * Turn on per-event DETAIL capture for this request. Off by default so the
522
+ * hot path never allocates a per-event list; the HTTP middleware enables it
523
+ * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.
524
+ */
525
+ enableDetail() {
526
+ this._detailOn = true;
527
+ }
528
+ /** Whether per-event detail capture is on. */
529
+ get detailEnabled() {
530
+ return this._detailOn;
531
+ }
532
+ /**
533
+ * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op
534
+ * unless {@link enableDetail} was called, so the hot-path call site (the SQL
535
+ * driver's query listener) pays only a boolean check when detail is off.
536
+ * Bounded by {@link DETAIL_CAP}; excess events still count toward the
537
+ * aggregate via {@link count} but are not retained individually.
538
+ */
539
+ recordDetail(category, label, dur) {
540
+ if (!this._detailOn) return;
541
+ const detail = this._detail ?? (this._detail = /* @__PURE__ */ new Map());
542
+ let list = detail.get(category);
543
+ if (!list) {
544
+ list = [];
545
+ detail.set(category, list);
546
+ }
547
+ if (list.length >= _PerfTiming.DETAIL_CAP) return;
548
+ list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });
549
+ }
550
+ /** Retained detail samples for `category`, in record order (empty when none). */
551
+ details(category) {
552
+ return this._detail?.get(category) ?? [];
553
+ }
513
554
  /**
514
555
  * Begin timing a phase. Returns an idempotent `end()` - the first call
515
556
  * records the elapsed duration; later calls are ignored, so it is safe to
@@ -533,6 +574,37 @@ var PerfTiming = class {
533
574
  end();
534
575
  }
535
576
  }
577
+ /**
578
+ * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call
579
+ * adds `dur` to the running total for `name` and increments a counter; the
580
+ * mark serializes as `name;dur=<sum>;desc="<count> <unit>"` (or just the
581
+ * bare count when no `unit` is given).
582
+ *
583
+ * Use this for high-frequency phases — one SQL query, one hook execution —
584
+ * where recording a distinct mark per event would blow the header out to
585
+ * hundreds of entries. The single `db;dur=210;desc="6 queries"` member is
586
+ * both the total DB time and the query count, which is the number most
587
+ * useful for spotting N sequential round-trips.
588
+ *
589
+ * The aggregate mark is inserted into the record stream the first time its
590
+ * name is seen, so it keeps its natural position relative to explicit marks
591
+ * (e.g. before the outer `total`, which is recorded last).
592
+ */
593
+ count(name, dur, unit) {
594
+ const add = Number.isFinite(dur) && dur > 0 ? dur : 0;
595
+ const aggregates = this._aggregates ?? (this._aggregates = /* @__PURE__ */ new Map());
596
+ let entry = aggregates.get(name);
597
+ if (!entry) {
598
+ const mark = { name, dur: 0 };
599
+ entry = { mark, count: 0, unit };
600
+ aggregates.set(name, entry);
601
+ this._marks.push(mark);
602
+ }
603
+ entry.count += 1;
604
+ entry.mark.dur += add;
605
+ if (unit) entry.unit = unit;
606
+ entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);
607
+ }
536
608
  /** Snapshot of recorded marks, in record order. */
537
609
  marks() {
538
610
  return this._marks;
@@ -542,7 +614,17 @@ var PerfTiming = class {
542
614
  return formatServerTiming(this._marks);
543
615
  }
544
616
  };
545
- var store = new import_node_async_hooks.AsyncLocalStorage();
617
+ /**
618
+ * Hard cap on stored detail samples per category — detail is only ever on
619
+ * for a deliberate debug request, but a pathological request must not pin
620
+ * unbounded memory. The aggregate {@link count} still reflects the true
621
+ * total; only the retained per-event list is bounded.
622
+ */
623
+ _PerfTiming.DETAIL_CAP = 1e3;
624
+ var PerfTiming = _PerfTiming;
625
+ var STORE_KEY = /* @__PURE__ */ Symbol.for("@objectstack/observability:perf-timing-store");
626
+ var globalStore = globalThis;
627
+ var store = globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new import_node_async_hooks.AsyncLocalStorage());
546
628
  function runWithPerfTiming(timing, fn) {
547
629
  return store.run(timing, fn);
548
630
  }
@@ -563,6 +645,31 @@ async function measureServerTiming(name, fn, desc) {
563
645
  if (!t) return fn();
564
646
  return t.measure(name, fn, desc);
565
647
  }
648
+ function countServerTiming(name, dur, unit) {
649
+ store.getStore()?.count(name, dur, unit);
650
+ }
651
+ function recordServerTimingDetail(category, label, dur) {
652
+ store.getStore()?.recordDetail(category, label, dur);
653
+ }
654
+ var GATE_KEY = /* @__PURE__ */ Symbol.for("@objectstack/observability:perf-disclosure-gate");
655
+ var globalGate = globalThis;
656
+ var gateStore = globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new import_node_async_hooks.AsyncLocalStorage());
657
+ function runWithPerfDisclosure(gate, fn) {
658
+ return gateStore.run(gate, fn);
659
+ }
660
+ function allowPerfDisclosure() {
661
+ const g = gateStore.getStore();
662
+ if (g) {
663
+ g.allowed = true;
664
+ g.privileged = true;
665
+ }
666
+ }
667
+ function isPerfDisclosureAllowed() {
668
+ return gateStore.getStore()?.allowed ?? false;
669
+ }
670
+ function isPerfDisclosurePrivileged() {
671
+ return gateStore.getStore()?.privileged ?? false;
672
+ }
566
673
  // Annotate the CommonJS export names for ESM import in node:
567
674
  0 && (module.exports = {
568
675
  ConsoleErrorReporter,
@@ -581,11 +688,17 @@ async function measureServerTiming(name, fn, desc) {
581
688
  PerfTiming,
582
689
  RUNTIME_METRICS,
583
690
  SEMCONV,
691
+ allowPerfDisclosure,
692
+ countServerTiming,
584
693
  currentPerfTiming,
585
694
  formatServerTiming,
695
+ isPerfDisclosureAllowed,
696
+ isPerfDisclosurePrivileged,
586
697
  measureServerTiming,
587
698
  perfNow,
588
699
  recordServerTiming,
700
+ recordServerTimingDetail,
701
+ runWithPerfDisclosure,
589
702
  runWithPerfTiming,
590
703
  startServerTiming
591
704
  });
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/service-names.ts","../src/semconv.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// 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 type ServerTimingMark,\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 — emitted by `@objectstack/runtime`'s instrumentRouteHandler ──\n /** Counter, labels: `method`, `route`, `status`. */\n httpRequestsTotal: 'http_requests_total',\n /** Histogram (ms), labels: `method`, `route`. */\n httpRequestDurationMs: 'http_request_duration_ms',\n /**\n * Counter, labels: `method`, `route`. Incremented when an\n * in-flight handler throws after the response is sent.\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 /** Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`). */\n cacheLookupsTotal: 'cache_lookups_total',\n /** Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). */\n cacheWritesTotal: 'cache_writes_total',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n cacheErrorsTotal: 'cache_errors_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) 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}) without threading the request object\n * through every layer. When no collector is active the free functions are\n * cheap no-ops, so call sites pay nothing when the feature is off.\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';\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 * 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 /** 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 * 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 /** 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\nconst store = 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts","../src/service-names.ts","../src/semconv.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// 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 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 — emitted by `@objectstack/runtime`'s instrumentRouteHandler ──\n /** Counter, labels: `method`, `route`, `status`. */\n httpRequestsTotal: 'http_requests_total',\n /** Histogram (ms), labels: `method`, `route`. */\n httpRequestDurationMs: 'http_request_duration_ms',\n /**\n * Counter, labels: `method`, `route`. Incremented when an\n * in-flight handler throws after the response is sent.\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 /** Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`). */\n cacheLookupsTotal: 'cache_lookups_total',\n /** Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). */\n cacheWritesTotal: 'cache_writes_total',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n cacheErrorsTotal: 'cache_errors_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) 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';\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"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -397,6 +397,20 @@ interface ServerTimingMark {
397
397
  /** Optional human-readable description (rendered as the quoted `desc`). */
398
398
  desc?: string;
399
399
  }
400
+ /**
401
+ * One recorded sub-event when DETAIL capture is on (see
402
+ * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds
403
+ * every query into a single count+duration — a detail sample keeps the
404
+ * individual event so an admin can see *which* queries ran and which was
405
+ * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED
406
+ * statement, bindings stripped — the query shape, never literal row values).
407
+ */
408
+ interface ServerTimingDetail {
409
+ /** Event label — e.g. a parametrized SQL statement (no bindings). */
410
+ label: string;
411
+ /** Duration in milliseconds. */
412
+ dur: number;
413
+ }
400
414
  /**
401
415
  * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not
402
416
  * affected by wall-clock adjustments); falls back to `Date.now()` on the rare
@@ -416,8 +430,46 @@ declare function formatServerTiming(marks: readonly ServerTimingMark[]): string;
416
430
  */
417
431
  declare class PerfTiming {
418
432
  private readonly _marks;
433
+ /**
434
+ * Live aggregate marks by name (see {@link count}). Lazily created so a
435
+ * request that never aggregates pays nothing. Each entry points at a mark
436
+ * already inserted into {@link _marks}, mutated in place as events arrive.
437
+ */
438
+ private _aggregates?;
439
+ /**
440
+ * Per-event detail samples by category, populated only while detail capture
441
+ * is on (see {@link enableDetail}). Lazily created so a request that never
442
+ * enables detail pays nothing.
443
+ */
444
+ private _detail?;
445
+ private _detailOn;
446
+ /**
447
+ * Hard cap on stored detail samples per category — detail is only ever on
448
+ * for a deliberate debug request, but a pathological request must not pin
449
+ * unbounded memory. The aggregate {@link count} still reflects the true
450
+ * total; only the retained per-event list is bounded.
451
+ */
452
+ private static readonly DETAIL_CAP;
419
453
  /** Record an already-measured phase. */
420
454
  record(name: string, dur: number, desc?: string): void;
455
+ /**
456
+ * Turn on per-event DETAIL capture for this request. Off by default so the
457
+ * hot path never allocates a per-event list; the HTTP middleware enables it
458
+ * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.
459
+ */
460
+ enableDetail(): void;
461
+ /** Whether per-event detail capture is on. */
462
+ get detailEnabled(): boolean;
463
+ /**
464
+ * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op
465
+ * unless {@link enableDetail} was called, so the hot-path call site (the SQL
466
+ * driver's query listener) pays only a boolean check when detail is off.
467
+ * Bounded by {@link DETAIL_CAP}; excess events still count toward the
468
+ * aggregate via {@link count} but are not retained individually.
469
+ */
470
+ recordDetail(category: string, label: string, dur: number): void;
471
+ /** Retained detail samples for `category`, in record order (empty when none). */
472
+ details(category: string): readonly ServerTimingDetail[];
421
473
  /**
422
474
  * Begin timing a phase. Returns an idempotent `end()` - the first call
423
475
  * records the elapsed duration; later calls are ignored, so it is safe to
@@ -426,6 +478,23 @@ declare class PerfTiming {
426
478
  start(name: string, desc?: string): () => void;
427
479
  /** Time an async (or sync) function, recording its elapsed duration. */
428
480
  measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T>;
481
+ /**
482
+ * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call
483
+ * adds `dur` to the running total for `name` and increments a counter; the
484
+ * mark serializes as `name;dur=<sum>;desc="<count> <unit>"` (or just the
485
+ * bare count when no `unit` is given).
486
+ *
487
+ * Use this for high-frequency phases — one SQL query, one hook execution —
488
+ * where recording a distinct mark per event would blow the header out to
489
+ * hundreds of entries. The single `db;dur=210;desc="6 queries"` member is
490
+ * both the total DB time and the query count, which is the number most
491
+ * useful for spotting N sequential round-trips.
492
+ *
493
+ * The aggregate mark is inserted into the record stream the first time its
494
+ * name is seen, so it keeps its natural position relative to explicit marks
495
+ * (e.g. before the outer `total`, which is recorded last).
496
+ */
497
+ count(name: string, dur: number, unit?: string): void;
429
498
  /** Snapshot of recorded marks, in record order. */
430
499
  marks(): readonly ServerTimingMark[];
431
500
  /** Serialize to a `Server-Timing` header value (`''` when empty). */
@@ -448,5 +517,75 @@ declare function startServerTiming(name: string, desc?: string): () => void;
448
517
  * the function is awaited with zero timing overhead.
449
518
  */
450
519
  declare function measureServerTiming<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T>;
520
+ /**
521
+ * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the
522
+ * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is
523
+ * active, so the hot-path call sites (the SQL driver's query listener, the hook
524
+ * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.
525
+ */
526
+ declare function countServerTiming(name: string, dur: number, unit?: string): void;
527
+ /**
528
+ * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto
529
+ * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no
530
+ * collector is active OR detail capture is off, so the hot-path call site pays
531
+ * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.
532
+ */
533
+ declare function recordServerTimingDetail(category: string, label: string, dur: number): void;
534
+ /**
535
+ * Per-request disclosure gate — the policy counterpart to the collector.
536
+ *
537
+ * The {@link PerfTiming} collector only MEASURES; whether the measured
538
+ * `Server-Timing` header is returned to the client is a separate decision. When
539
+ * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has
540
+ * opted the whole environment in, so the gate opens up front and every response
541
+ * carries the header. When it is turned on PER-REQUEST — the caller sends an
542
+ * `X-OS-Debug-Timing` header — the header must stay withheld until the request
543
+ * proves an admin/service identity: phase durations are a mild
544
+ * backend-fingerprinting surface, so an ordinary user must never be able to pull
545
+ * them just by sending a header. The request path flips the gate open with
546
+ * {@link allowPerfDisclosure} once it has resolved a privileged principal.
547
+ *
548
+ * Keeping this out of {@link PerfTiming} preserves the collector's invariant
549
+ * ("it only measures, it never decides whether to emit").
550
+ *
551
+ * Two levels, because global mode discloses the basic header to everyone but the
552
+ * richer per-query detail must stay admin-only:
553
+ * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by
554
+ * global mode for everyone, or by a proven admin per-request).
555
+ * - `privileged` — the principal is a proven admin/service. Gates the richer,
556
+ * SQL-shape-bearing detail payload, which must NEVER reach an
557
+ * ordinary caller even when global mode is on.
558
+ */
559
+ interface PerfDisclosureGate {
560
+ /** Whether the basic collected timing may be disclosed to the client. */
561
+ allowed: boolean;
562
+ /**
563
+ * Whether the principal is a proven admin/service — gates the richer detail
564
+ * payload independently of `allowed`. Absent = not privileged.
565
+ */
566
+ privileged?: boolean;
567
+ }
568
+ /**
569
+ * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.
570
+ * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`
571
+ * settles to decide whether to emit the header.
572
+ */
573
+ declare function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T;
574
+ /**
575
+ * Open the ambient disclosure gate — the request has proven an admin/service
576
+ * identity, so it may see its own `Server-Timing` header AND the richer detail
577
+ * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A
578
+ * no-op when no gate is active (perf-tuning off), so the call site stays
579
+ * branch-free.
580
+ */
581
+ declare function allowPerfDisclosure(): void;
582
+ /** Whether the ambient disclosure gate is open. `false` when none is active. */
583
+ declare function isPerfDisclosureAllowed(): boolean;
584
+ /**
585
+ * Whether the ambient principal is a proven admin/service — gates the richer
586
+ * detail payload. `false` when no gate is active or only global-mode disclosure
587
+ * (not a proven admin) opened it.
588
+ */
589
+ declare function isPerfDisclosurePrivileged(): boolean;
451
590
 
452
- export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingMark, currentPerfTiming, formatServerTiming, measureServerTiming, perfNow, recordServerTiming, runWithPerfTiming, startServerTiming };
591
+ export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
package/dist/index.d.ts CHANGED
@@ -397,6 +397,20 @@ interface ServerTimingMark {
397
397
  /** Optional human-readable description (rendered as the quoted `desc`). */
398
398
  desc?: string;
399
399
  }
400
+ /**
401
+ * One recorded sub-event when DETAIL capture is on (see
402
+ * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds
403
+ * every query into a single count+duration — a detail sample keeps the
404
+ * individual event so an admin can see *which* queries ran and which was
405
+ * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED
406
+ * statement, bindings stripped — the query shape, never literal row values).
407
+ */
408
+ interface ServerTimingDetail {
409
+ /** Event label — e.g. a parametrized SQL statement (no bindings). */
410
+ label: string;
411
+ /** Duration in milliseconds. */
412
+ dur: number;
413
+ }
400
414
  /**
401
415
  * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not
402
416
  * affected by wall-clock adjustments); falls back to `Date.now()` on the rare
@@ -416,8 +430,46 @@ declare function formatServerTiming(marks: readonly ServerTimingMark[]): string;
416
430
  */
417
431
  declare class PerfTiming {
418
432
  private readonly _marks;
433
+ /**
434
+ * Live aggregate marks by name (see {@link count}). Lazily created so a
435
+ * request that never aggregates pays nothing. Each entry points at a mark
436
+ * already inserted into {@link _marks}, mutated in place as events arrive.
437
+ */
438
+ private _aggregates?;
439
+ /**
440
+ * Per-event detail samples by category, populated only while detail capture
441
+ * is on (see {@link enableDetail}). Lazily created so a request that never
442
+ * enables detail pays nothing.
443
+ */
444
+ private _detail?;
445
+ private _detailOn;
446
+ /**
447
+ * Hard cap on stored detail samples per category — detail is only ever on
448
+ * for a deliberate debug request, but a pathological request must not pin
449
+ * unbounded memory. The aggregate {@link count} still reflects the true
450
+ * total; only the retained per-event list is bounded.
451
+ */
452
+ private static readonly DETAIL_CAP;
419
453
  /** Record an already-measured phase. */
420
454
  record(name: string, dur: number, desc?: string): void;
455
+ /**
456
+ * Turn on per-event DETAIL capture for this request. Off by default so the
457
+ * hot path never allocates a per-event list; the HTTP middleware enables it
458
+ * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.
459
+ */
460
+ enableDetail(): void;
461
+ /** Whether per-event detail capture is on. */
462
+ get detailEnabled(): boolean;
463
+ /**
464
+ * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op
465
+ * unless {@link enableDetail} was called, so the hot-path call site (the SQL
466
+ * driver's query listener) pays only a boolean check when detail is off.
467
+ * Bounded by {@link DETAIL_CAP}; excess events still count toward the
468
+ * aggregate via {@link count} but are not retained individually.
469
+ */
470
+ recordDetail(category: string, label: string, dur: number): void;
471
+ /** Retained detail samples for `category`, in record order (empty when none). */
472
+ details(category: string): readonly ServerTimingDetail[];
421
473
  /**
422
474
  * Begin timing a phase. Returns an idempotent `end()` - the first call
423
475
  * records the elapsed duration; later calls are ignored, so it is safe to
@@ -426,6 +478,23 @@ declare class PerfTiming {
426
478
  start(name: string, desc?: string): () => void;
427
479
  /** Time an async (or sync) function, recording its elapsed duration. */
428
480
  measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T>;
481
+ /**
482
+ * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call
483
+ * adds `dur` to the running total for `name` and increments a counter; the
484
+ * mark serializes as `name;dur=<sum>;desc="<count> <unit>"` (or just the
485
+ * bare count when no `unit` is given).
486
+ *
487
+ * Use this for high-frequency phases — one SQL query, one hook execution —
488
+ * where recording a distinct mark per event would blow the header out to
489
+ * hundreds of entries. The single `db;dur=210;desc="6 queries"` member is
490
+ * both the total DB time and the query count, which is the number most
491
+ * useful for spotting N sequential round-trips.
492
+ *
493
+ * The aggregate mark is inserted into the record stream the first time its
494
+ * name is seen, so it keeps its natural position relative to explicit marks
495
+ * (e.g. before the outer `total`, which is recorded last).
496
+ */
497
+ count(name: string, dur: number, unit?: string): void;
429
498
  /** Snapshot of recorded marks, in record order. */
430
499
  marks(): readonly ServerTimingMark[];
431
500
  /** Serialize to a `Server-Timing` header value (`''` when empty). */
@@ -448,5 +517,75 @@ declare function startServerTiming(name: string, desc?: string): () => void;
448
517
  * the function is awaited with zero timing overhead.
449
518
  */
450
519
  declare function measureServerTiming<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T>;
520
+ /**
521
+ * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the
522
+ * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is
523
+ * active, so the hot-path call sites (the SQL driver's query listener, the hook
524
+ * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.
525
+ */
526
+ declare function countServerTiming(name: string, dur: number, unit?: string): void;
527
+ /**
528
+ * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto
529
+ * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no
530
+ * collector is active OR detail capture is off, so the hot-path call site pays
531
+ * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.
532
+ */
533
+ declare function recordServerTimingDetail(category: string, label: string, dur: number): void;
534
+ /**
535
+ * Per-request disclosure gate — the policy counterpart to the collector.
536
+ *
537
+ * The {@link PerfTiming} collector only MEASURES; whether the measured
538
+ * `Server-Timing` header is returned to the client is a separate decision. When
539
+ * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has
540
+ * opted the whole environment in, so the gate opens up front and every response
541
+ * carries the header. When it is turned on PER-REQUEST — the caller sends an
542
+ * `X-OS-Debug-Timing` header — the header must stay withheld until the request
543
+ * proves an admin/service identity: phase durations are a mild
544
+ * backend-fingerprinting surface, so an ordinary user must never be able to pull
545
+ * them just by sending a header. The request path flips the gate open with
546
+ * {@link allowPerfDisclosure} once it has resolved a privileged principal.
547
+ *
548
+ * Keeping this out of {@link PerfTiming} preserves the collector's invariant
549
+ * ("it only measures, it never decides whether to emit").
550
+ *
551
+ * Two levels, because global mode discloses the basic header to everyone but the
552
+ * richer per-query detail must stay admin-only:
553
+ * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by
554
+ * global mode for everyone, or by a proven admin per-request).
555
+ * - `privileged` — the principal is a proven admin/service. Gates the richer,
556
+ * SQL-shape-bearing detail payload, which must NEVER reach an
557
+ * ordinary caller even when global mode is on.
558
+ */
559
+ interface PerfDisclosureGate {
560
+ /** Whether the basic collected timing may be disclosed to the client. */
561
+ allowed: boolean;
562
+ /**
563
+ * Whether the principal is a proven admin/service — gates the richer detail
564
+ * payload independently of `allowed`. Absent = not privileged.
565
+ */
566
+ privileged?: boolean;
567
+ }
568
+ /**
569
+ * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.
570
+ * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`
571
+ * settles to decide whether to emit the header.
572
+ */
573
+ declare function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T;
574
+ /**
575
+ * Open the ambient disclosure gate — the request has proven an admin/service
576
+ * identity, so it may see its own `Server-Timing` header AND the richer detail
577
+ * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A
578
+ * no-op when no gate is active (perf-tuning off), so the call site stays
579
+ * branch-free.
580
+ */
581
+ declare function allowPerfDisclosure(): void;
582
+ /** Whether the ambient disclosure gate is open. `false` when none is active. */
583
+ declare function isPerfDisclosureAllowed(): boolean;
584
+ /**
585
+ * Whether the ambient principal is a proven admin/service — gates the richer
586
+ * detail payload. `false` when no gate is active or only global-mode disclosure
587
+ * (not a proven admin) opened it.
588
+ */
589
+ declare function isPerfDisclosurePrivileged(): boolean;
451
590
 
452
- export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingMark, currentPerfTiming, formatServerTiming, measureServerTiming, perfNow, recordServerTiming, runWithPerfTiming, startServerTiming };
591
+ export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
package/dist/index.js CHANGED
@@ -454,14 +454,49 @@ function formatServerTiming(marks) {
454
454
  }
455
455
  return parts.join(", ");
456
456
  }
457
- var PerfTiming = class {
457
+ var _PerfTiming = class _PerfTiming {
458
458
  constructor() {
459
459
  this._marks = [];
460
+ this._detailOn = false;
460
461
  }
461
462
  /** Record an already-measured phase. */
462
463
  record(name, dur, desc) {
463
464
  this._marks.push({ name, dur, desc });
464
465
  }
466
+ /**
467
+ * Turn on per-event DETAIL capture for this request. Off by default so the
468
+ * hot path never allocates a per-event list; the HTTP middleware enables it
469
+ * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.
470
+ */
471
+ enableDetail() {
472
+ this._detailOn = true;
473
+ }
474
+ /** Whether per-event detail capture is on. */
475
+ get detailEnabled() {
476
+ return this._detailOn;
477
+ }
478
+ /**
479
+ * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op
480
+ * unless {@link enableDetail} was called, so the hot-path call site (the SQL
481
+ * driver's query listener) pays only a boolean check when detail is off.
482
+ * Bounded by {@link DETAIL_CAP}; excess events still count toward the
483
+ * aggregate via {@link count} but are not retained individually.
484
+ */
485
+ recordDetail(category, label, dur) {
486
+ if (!this._detailOn) return;
487
+ const detail = this._detail ?? (this._detail = /* @__PURE__ */ new Map());
488
+ let list = detail.get(category);
489
+ if (!list) {
490
+ list = [];
491
+ detail.set(category, list);
492
+ }
493
+ if (list.length >= _PerfTiming.DETAIL_CAP) return;
494
+ list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });
495
+ }
496
+ /** Retained detail samples for `category`, in record order (empty when none). */
497
+ details(category) {
498
+ return this._detail?.get(category) ?? [];
499
+ }
465
500
  /**
466
501
  * Begin timing a phase. Returns an idempotent `end()` - the first call
467
502
  * records the elapsed duration; later calls are ignored, so it is safe to
@@ -485,6 +520,37 @@ var PerfTiming = class {
485
520
  end();
486
521
  }
487
522
  }
523
+ /**
524
+ * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call
525
+ * adds `dur` to the running total for `name` and increments a counter; the
526
+ * mark serializes as `name;dur=<sum>;desc="<count> <unit>"` (or just the
527
+ * bare count when no `unit` is given).
528
+ *
529
+ * Use this for high-frequency phases — one SQL query, one hook execution —
530
+ * where recording a distinct mark per event would blow the header out to
531
+ * hundreds of entries. The single `db;dur=210;desc="6 queries"` member is
532
+ * both the total DB time and the query count, which is the number most
533
+ * useful for spotting N sequential round-trips.
534
+ *
535
+ * The aggregate mark is inserted into the record stream the first time its
536
+ * name is seen, so it keeps its natural position relative to explicit marks
537
+ * (e.g. before the outer `total`, which is recorded last).
538
+ */
539
+ count(name, dur, unit) {
540
+ const add = Number.isFinite(dur) && dur > 0 ? dur : 0;
541
+ const aggregates = this._aggregates ?? (this._aggregates = /* @__PURE__ */ new Map());
542
+ let entry = aggregates.get(name);
543
+ if (!entry) {
544
+ const mark = { name, dur: 0 };
545
+ entry = { mark, count: 0, unit };
546
+ aggregates.set(name, entry);
547
+ this._marks.push(mark);
548
+ }
549
+ entry.count += 1;
550
+ entry.mark.dur += add;
551
+ if (unit) entry.unit = unit;
552
+ entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);
553
+ }
488
554
  /** Snapshot of recorded marks, in record order. */
489
555
  marks() {
490
556
  return this._marks;
@@ -494,7 +560,17 @@ var PerfTiming = class {
494
560
  return formatServerTiming(this._marks);
495
561
  }
496
562
  };
497
- var store = new AsyncLocalStorage();
563
+ /**
564
+ * Hard cap on stored detail samples per category — detail is only ever on
565
+ * for a deliberate debug request, but a pathological request must not pin
566
+ * unbounded memory. The aggregate {@link count} still reflects the true
567
+ * total; only the retained per-event list is bounded.
568
+ */
569
+ _PerfTiming.DETAIL_CAP = 1e3;
570
+ var PerfTiming = _PerfTiming;
571
+ var STORE_KEY = /* @__PURE__ */ Symbol.for("@objectstack/observability:perf-timing-store");
572
+ var globalStore = globalThis;
573
+ var store = globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage());
498
574
  function runWithPerfTiming(timing, fn) {
499
575
  return store.run(timing, fn);
500
576
  }
@@ -515,6 +591,31 @@ async function measureServerTiming(name, fn, desc) {
515
591
  if (!t) return fn();
516
592
  return t.measure(name, fn, desc);
517
593
  }
594
+ function countServerTiming(name, dur, unit) {
595
+ store.getStore()?.count(name, dur, unit);
596
+ }
597
+ function recordServerTimingDetail(category, label, dur) {
598
+ store.getStore()?.recordDetail(category, label, dur);
599
+ }
600
+ var GATE_KEY = /* @__PURE__ */ Symbol.for("@objectstack/observability:perf-disclosure-gate");
601
+ var globalGate = globalThis;
602
+ var gateStore = globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage());
603
+ function runWithPerfDisclosure(gate, fn) {
604
+ return gateStore.run(gate, fn);
605
+ }
606
+ function allowPerfDisclosure() {
607
+ const g = gateStore.getStore();
608
+ if (g) {
609
+ g.allowed = true;
610
+ g.privileged = true;
611
+ }
612
+ }
613
+ function isPerfDisclosureAllowed() {
614
+ return gateStore.getStore()?.allowed ?? false;
615
+ }
616
+ function isPerfDisclosurePrivileged() {
617
+ return gateStore.getStore()?.privileged ?? false;
618
+ }
518
619
  export {
519
620
  ConsoleErrorReporter,
520
621
  ConsoleLogger,
@@ -532,11 +633,17 @@ export {
532
633
  PerfTiming,
533
634
  RUNTIME_METRICS,
534
635
  SEMCONV,
636
+ allowPerfDisclosure,
637
+ countServerTiming,
535
638
  currentPerfTiming,
536
639
  formatServerTiming,
640
+ isPerfDisclosureAllowed,
641
+ isPerfDisclosurePrivileged,
537
642
  measureServerTiming,
538
643
  perfNow,
539
644
  recordServerTiming,
645
+ recordServerTimingDetail,
646
+ runWithPerfDisclosure,
540
647
  runWithPerfTiming,
541
648
  startServerTiming
542
649
  };
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/service-names.ts","../src/semconv.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 — emitted by `@objectstack/runtime`'s instrumentRouteHandler ──\n /** Counter, labels: `method`, `route`, `status`. */\n httpRequestsTotal: 'http_requests_total',\n /** Histogram (ms), labels: `method`, `route`. */\n httpRequestDurationMs: 'http_request_duration_ms',\n /**\n * Counter, labels: `method`, `route`. Incremented when an\n * in-flight handler throws after the response is sent.\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 /** Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`). */\n cacheLookupsTotal: 'cache_lookups_total',\n /** Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). */\n cacheWritesTotal: 'cache_writes_total',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n cacheErrorsTotal: 'cache_errors_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) 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}) without threading the request object\n * through every layer. When no collector is active the free functions are\n * cheap no-ops, so call sites pay nothing when the feature is off.\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';\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 * 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 /** 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 * 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 /** 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\nconst store = 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/service-names.ts","../src/semconv.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 — emitted by `@objectstack/runtime`'s instrumentRouteHandler ──\n /** Counter, labels: `method`, `route`, `status`. */\n httpRequestsTotal: 'http_requests_total',\n /** Histogram (ms), labels: `method`, `route`. */\n httpRequestDurationMs: 'http_request_duration_ms',\n /**\n * Counter, labels: `method`, `route`. Incremented when an\n * in-flight handler throws after the response is sent.\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 /** Counter, labels: `adapter` (`memory`|`redis`), `result` (`hit`|`miss`). */\n cacheLookupsTotal: 'cache_lookups_total',\n /** Counter, labels: `adapter`, `op` (`set`|`delete`|`clear`). */\n cacheWritesTotal: 'cache_writes_total',\n /** Counter, labels: `adapter`, `op`, `errorClass`. */\n cacheErrorsTotal: 'cache_errors_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) 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';\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"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/observability",
3
- "version": "15.1.0",
3
+ "version": "16.0.0-rc.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": "15.1.0"
17
+ "@objectstack/spec": "16.0.0-rc.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^26.1.1",