@objectstack/observability 16.0.0-rc.1 → 16.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,6 +41,7 @@ __export(index_exports, {
41
41
  currentPerfTiming: () => currentPerfTiming,
42
42
  formatServerTiming: () => formatServerTiming,
43
43
  isPerfDisclosureAllowed: () => isPerfDisclosureAllowed,
44
+ isPerfDisclosurePrincipal: () => isPerfDisclosurePrincipal,
44
45
  isPerfDisclosurePrivileged: () => isPerfDisclosurePrivileged,
45
46
  measureServerTiming: () => measureServerTiming,
46
47
  perfNow: () => perfNow,
@@ -670,6 +671,12 @@ function isPerfDisclosureAllowed() {
670
671
  function isPerfDisclosurePrivileged() {
671
672
  return gateStore.getStore()?.privileged ?? false;
672
673
  }
674
+ function isPerfDisclosurePrincipal(ec) {
675
+ if (!ec) return false;
676
+ if (ec.isSystem === true) return true;
677
+ if (ec.principalKind === "service" || ec.principalKind === "system") return true;
678
+ return ec.posture === "PLATFORM_ADMIN" || ec.posture === "TENANT_ADMIN";
679
+ }
673
680
  // Annotate the CommonJS export names for ESM import in node:
674
681
  0 && (module.exports = {
675
682
  ConsoleErrorReporter,
@@ -693,6 +700,7 @@ function isPerfDisclosurePrivileged() {
693
700
  currentPerfTiming,
694
701
  formatServerTiming,
695
702
  isPerfDisclosureAllowed,
703
+ isPerfDisclosurePrincipal,
696
704
  isPerfDisclosurePrivileged,
697
705
  measureServerTiming,
698
706
  perfNow,
@@ -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 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":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYO,IAAM,gCAAgC;AACtC,IAAM,+BAA+B;;;ACOrC,IAAM,UAAU;AAAA;AAAA;AAAA,EAGnB,mBAAmB;AAAA;AAAA,EAEnB,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKvB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,wBAAwB;AAAA;AAAA,EAExB,4BAA4B;AAAA;AAAA,EAE5B,oBAAoB;AAAA;AAAA;AAAA,EAIpB,mBAAmB;AAAA;AAAA,EAEnB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA,EAIlB,sBAAsB;AAAA;AAAA,EAEtB,0BAA0B;AAAA;AAAA,EAE1B,4BAA4B;AAChC;AAQO,IAAM,kBAAkB;AAAA,EAC3B,mBAAmB,QAAQ;AAAA,EAC3B,uBAAuB,QAAQ;AAAA,EAC/B,wBAAwB,QAAQ;AACpC;;;ACxDO,IAAM,sBAAN,MAAqD;AAAA,EACxD,UAAgB;AAAA,EAAE;AAAA,EAClB,YAAkB;AAAA,EAAE;AAAA,EACpB,QAAc;AAAA,EAAE;AACpB;AAWO,IAAM,0BAAN,MAAyD;AAAA,EAAzD;AACH,SAAS,UAA0B,CAAC;AAAA;AAAA,EAEpC,QAAQ,MAAc,SAAiC,CAAC,GAAG,QAAgB,GAAS;AAChF,SAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,WAAW,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAC9E;AAAA,EAEA,UAAU,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC9E,SAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,aAAa,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAChF;AAAA,EAEA,MAAM,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC1E,SAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,SAAS,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAC5E;AAAA;AAAA,EAGA,aAAa,MAAc,aAAqC,CAAC,GAAW;AACxE,WAAO,KAAK,QACP,OAAO,OAAK,EAAE,SAAS,aAAa,EAAE,SAAS,QAAQ,cAAc,EAAE,QAAQ,UAAU,CAAC,EAC1F,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAAA,EAC5C;AAAA;AAAA,EAGA,gBAAgB,MAAc,aAAqC,CAAC,GAAa;AAC7E,WAAO,KAAK,QACP,OAAO,OAAK,EAAE,SAAS,eAAe,EAAE,SAAS,QAAQ,cAAc,EAAE,QAAQ,UAAU,CAAC,EAC5F,IAAI,OAAK,EAAE,KAAK;AAAA,EACzB;AAAA;AAAA,EAGA,UAAU,MAAc,aAAqC,CAAC,GAAuB;AACjF,aAAS,IAAI,KAAK,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;AAC/C,YAAM,IAAI,KAAK,QAAQ,CAAC;AACxB,UAAI,EAAE,SAAS,WAAW,EAAE,SAAS,QAAQ,cAAc,EAAE,QAAQ,UAAU,GAAG;AAC9E,eAAO,EAAE;AAAA,MACb;AAAA,IACJ;AACA,WAAO;AAAA,EACX;AAAA;AAAA,EAGA,QAAc;AACV,SAAK,QAAQ,SAAS;AAAA,EAC1B;AACJ;AAEA,SAAS,cAAc,QAAgC,UAA2C;AAC9F,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAC3C,QAAI,OAAO,CAAC,MAAM,EAAG,QAAO;AAAA,EAChC;AACA,SAAO;AACX;AAWO,IAAM,yBAAN,MAAwD;AAAA,EAC3D,YAA6B,OAA2D,CAAC,GAAG;AAA/D;AAAA,EAAiE;AAAA,EAE9F,QAAQ,MAAc,SAAiC,CAAC,GAAG,QAAgB,GAAS;AAChF,SAAK,KAAK,WAAW,MAAM,OAAO,MAAM;AAAA,EAC5C;AAAA,EACA,UAAU,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC9E,SAAK,KAAK,aAAa,MAAM,OAAO,MAAM;AAAA,EAC9C;AAAA,EACA,MAAM,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC1E,SAAK,KAAK,SAAS,MAAM,OAAO,MAAM;AAAA,EAC1C;AAAA,EAEQ,KAAK,MAAc,MAAc,OAAe,QAAsC;AAC1F,QAAI;AACA,YAAM,OAAO,KAAK,KAAK,SAAS,CAAC,MAAM;AAAE,gBAAQ,IAAI,CAAC;AAAA,MAAG;AACzD,YAAM,SAAS,KAAK,KAAK,UAAU;AACnC,YAAM,WAAW,OAAO,QAAQ,MAAM,EACjC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,EAAE,EAC3C,KAAK,GAAG;AACb,WAAK,GAAG,MAAM,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,GAAG,WAAW,MAAM,WAAW,EAAE,EAAE;AAAA,IAC9E,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AA+DO,IAAM,0BAAN,MAAyD;AAAA,EAS5D,YAAY,SAAkC;AAR9C,SAAQ,SAAyB,CAAC;AAS9B,SAAK,WAAW,kBAAkB,QAAQ,QAAQ;AAClD,SAAK,UAAU,QAAQ,WAAW,CAAC;AACnC,SAAK,WAAW,QAAQ,YAAY,CAAC;AACrC,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,YAAY,QAAQ,UAAU,OAAO,UAAU,cAAc,MAAM,KAAK,UAAU,IAAI;AAC3F,SAAK,UAAU,QAAQ,YAAY,MAAM;AAAA,IAAE;AAAA,EAC/C;AAAA,EAEA,QAAQ,MAAc,SAAiC,CAAC,GAAG,QAAgB,GAAS;AAChF,SAAK,OAAO,EAAE,MAAM,MAAM,WAAW,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACxE;AAAA,EACA,UAAU,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC9E,SAAK,OAAO,EAAE,MAAM,MAAM,aAAa,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAC1E;AAAA,EACA,MAAM,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC1E,SAAK,OAAO,EAAE,MAAM,MAAM,SAAS,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACtE;AAAA,EAEQ,OAAO,QAA4B;AACvC,SAAK,OAAO,KAAK,MAAM;AACvB,QAAI,KAAK,OAAO,UAAU,KAAK,eAAe;AAE1C,WAAK,KAAK,MAAM,EAAE,MAAM,KAAK,OAAO;AAAA,IACxC;AAAA,EACJ;AAAA;AAAA,EAGA,OAAgC;AAC5B,WAAO,KAAK,OAAO,MAAM;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAuB;AACzB,QAAI,KAAK,OAAO,WAAW,EAAG;AAC9B,UAAM,UAAU,KAAK;AACrB,SAAK,SAAS,CAAC;AACf,QAAI;AACA,YAAM,OAAO,KAAK,UAAU,gBAAgB,SAAS,KAAK,QAAQ,CAAC;AACnE,YAAM,MAAM,MAAM,KAAK,UAAU,KAAK,UAAU;AAAA,QAC5C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,KAAK,QAAQ;AAAA,QAC/D;AAAA,MACJ,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACT,aAAK,QAAQ,IAAI,MAAM,6BAA6B,IAAI,MAAM,EAAE,CAAC;AAAA,MACrE;AAAA,IACJ,SAAS,KAAK;AACV,WAAK,QAAQ,GAAG;AAAA,IACpB;AAAA,EACJ;AACJ;AAEA,SAAS,kBAAkB,KAAqB;AAC5C,QAAM,UAAU,IAAI,QAAQ,QAAQ,EAAE;AACtC,MAAI,iBAAiB,KAAK,OAAO,EAAG,QAAO;AAC3C,SAAO,UAAU;AACrB;AAEA,SAAS,UAAiB;AACtB,QAAM,IAAI,MAAM,wEAAwE;AAC5F;AAcA,SAAS,gBAAgB,SAAyB,UAA2C;AACzF,QAAM,SAAS,oBAAI,IAAoE;AACvF,aAAW,KAAK,SAAS;AACrB,UAAM,WAAW,OAAO,IAAI,EAAE,IAAI;AAClC,QAAI,SAAU,UAAS,OAAO,KAAK,CAAC;AAAA,QAC/B,QAAO,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC;AAAA,EACzD;AAEA,QAAM,UAAU,MAAM,KAAK,OAAO,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM;AAC3E,QAAI,SAAS,WAAW;AACpB,aAAO;AAAA,QACH;AAAA,QACA,KAAK;AAAA,UACD,YAAY,OAAO,IAAI,OAAK,cAAc,CAAC,CAAC;AAAA,UAC5C,wBAAwB;AAAA,UACxB,aAAa;AAAA,QACjB;AAAA,MACJ;AAAA,IACJ;AACA,QAAI,SAAS,SAAS;AAClB,aAAO,EAAE,MAAM,OAAO,EAAE,YAAY,OAAO,IAAI,OAAK,cAAc,CAAC,CAAC,EAAE,EAAE;AAAA,IAC5E;AAGA,WAAO;AAAA,MACH;AAAA,MACA,WAAW;AAAA,QACP,wBAAwB;AAAA,QACxB,YAAY,OAAO,IAAI,QAAM;AAAA,UACzB,YAAY,aAAa,EAAE,MAAM;AAAA,UACjC,cAAc,OAAO,EAAE,EAAE,IAAI;AAAA,UAC7B,mBAAmB,OAAO,EAAE,EAAE,IAAI;AAAA,UAClC,OAAO;AAAA,UACP,KAAK,EAAE;AAAA,UACP,cAAc,CAAC,KAAK,GAAG;AAAA,UACvB,gBAAgB,CAAC,EAAE,KAAK;AAAA,QAC5B,EAAE;AAAA,MACN;AAAA,IACJ;AAAA,EACJ,CAAC;AAED,SAAO;AAAA,IACH,iBAAiB,CAAC;AAAA,MACd,UAAU,EAAE,YAAY,aAAa,QAAQ,EAAE;AAAA,MAC/C,cAAc,CAAC;AAAA,QACX,OAAO,EAAE,MAAM,8BAA8B,SAAS,QAAQ;AAAA,QAC9D;AAAA,MACJ,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;AAEA,SAAS,cAAc,GAA0B;AAC7C,SAAO;AAAA,IACH,YAAY,aAAa,EAAE,MAAM;AAAA,IACjC,cAAc,OAAO,EAAE,EAAE,IAAI;AAAA,IAC7B,mBAAmB,OAAO,EAAE,EAAE,IAAI;AAAA,IAClC,UAAU,EAAE;AAAA,EAChB;AACJ;AAEA,SAAS,aAAa,QAA2C;AAC7D,SAAO,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO;AAAA,IACjD;AAAA,IACA,OAAO,EAAE,aAAa,MAAM;AAAA,EAChC,EAAE;AACN;;;ACrUO,IAAM,oBAAN,MAAiD;AAAA,EACpD,mBAAyB;AAAA,EAAE;AAC/B;AAGO,IAAM,wBAAN,MAAqD;AAAA,EAArD;AACH,SAAS,WAA4B,CAAC;AAAA;AAAA,EAEtC,iBAAiB,OAAgB,UAAmC,CAAC,GAAS;AAC1E,SAAK,SAAS,KAAK,EAAE,OAAO,SAAS,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACzD;AAAA,EAEA,QAAc;AACV,SAAK,SAAS,SAAS;AAAA,EAC3B;AACJ;AAUO,IAAM,uBAAN,MAAoD;AAAA,EACvD,YAA6B,OAA0C,CAAC,GAAG;AAA9C;AAAA,EAAgD;AAAA,EAE7E,iBAAiB,OAAgB,UAAmC,CAAC,GAAS;AAC1E,QAAI;AACA,YAAM,OAAO,KAAK,KAAK,SAAS,CAAC,MAAM;AAAE,gBAAQ,MAAM,CAAC;AAAA,MAAG;AAC3D,YAAM,SAAkC;AAAA,QACpC,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC3B,OAAO;AAAA,QACP,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC1D;AAAA,MACJ;AACA,UAAI,iBAAiB,SAAS,MAAM,OAAO;AACvC,eAAO,QAAQ,MAAM;AAAA,MACzB;AACA,WAAK,KAAK,UAAU,MAAM,CAAC;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;;;AC7CO,IAAM,aAAa,CAAC,SAAS,QAAQ,QAAQ,SAAS,OAAO;AAGpE,IAAM,iBAA2C;AAAA,EAC7C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AACX;AAGO,IAAM,aAAN,MAAmC;AAAA,EACtC,QAAc;AAAA,EAAE;AAAA,EAChB,OAAa;AAAA,EAAE;AAAA,EACf,OAAa;AAAA,EAAE;AAAA,EACf,QAAc;AAAA,EAAE;AAAA,EAChB,QAAc;AAAA,EAAE;AAAA,EAChB,QAAgB;AAAE,WAAO;AAAA,EAAM;AACnC;AAQO,IAAM,gBAAN,MAAM,eAAgC;AAAA,EACzC,YACqB,OAIb,CAAC,GACP;AALmB;AAAA,EAKjB;AAAA,EAEJ,IAAY,YAAoB;AAC5B,WAAO,eAAe,KAAK,KAAK,SAAS,MAAM;AAAA,EACnD;AAAA,EACA,IAAY,OAAO;AACf,WAAO,KAAK,KAAK,QAAQ,EAAE,KAAK,CAAC,MAAc,QAAQ,IAAI,CAAC,GAAG,OAAO,CAAC,MAAc,QAAQ,MAAM,CAAC,EAAE;AAAA,EAC1G;AAAA,EACA,IAAY,UAAmC;AAC3C,WAAO,KAAK,KAAK,WAAW,CAAC;AAAA,EACjC;AAAA,EAEA,MAAM,SAAiB,MAAsC;AAAE,SAAK,KAAK,SAAS,SAAS,IAAI;AAAA,EAAG;AAAA,EAClG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAA0C;AAC5C,WAAO,IAAI,eAAc,EAAE,GAAG,KAAK,MAAM,SAAS,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,EAAE,CAAC;AAAA,EACvF;AAAA,EAEQ,KAAK,OAAiB,KAAa,MAAsC;AAC7E,QAAI,eAAe,KAAK,IAAI,KAAK,UAAW;AAC5C,QAAI;AACA,YAAM,SAAS,EAAE,GAAG,KAAK,SAAS,GAAI,QAAQ,CAAC,EAAG;AAClD,YAAM,OAAO,OAAO,KAAK,MAAM,EAAE,SAAS,MAAM,KAAK,UAAU,MAAM,IAAI;AACzE,YAAM,OAAO,IAAI,KAAK,KAAK,GAAG,GAAG,IAAI;AACrC,UAAI,UAAU,WAAW,UAAU,QAAS,MAAK,KAAK,MAAM,IAAI;AAAA,UAC3D,MAAK,KAAK,IAAI,IAAI;AAAA,IAC3B,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AAYO,IAAM,aAAN,MAAM,YAA6B;AAAA,EACtC,YACqB,OAQb,CAAC,GACP;AATmB;AAAA,EASjB;AAAA,EAEJ,IAAY,YAAoB;AAAE,WAAO,eAAe,KAAK,KAAK,SAAS,MAAM;AAAA,EAAG;AAAA,EACpF,IAAY,OAAO;AACf,WAAO,KAAK,KAAK,QAAQ,EAAE,KAAK,CAAC,MAAc,QAAQ,IAAI,CAAC,GAAG,OAAO,CAAC,MAAc,QAAQ,MAAM,CAAC,EAAE;AAAA,EAC1G;AAAA,EACA,IAAY,UAAmC;AAAE,WAAO,KAAK,KAAK,WAAW,CAAC;AAAA,EAAG;AAAA,EACjF,IAAY,OAAgC;AAAE,WAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,EAAG;AAAA,EAC3E,IAAY,MAAkB;AAAE,WAAO,KAAK,KAAK,QAAQ,MAAM,oBAAI,KAAK;AAAA,EAAI;AAAA,EAE5E,MAAM,SAAiB,MAAsC;AAAE,SAAK,KAAK,SAAS,SAAS,IAAI;AAAA,EAAG;AAAA,EAClG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAA0C;AAC5C,WAAO,IAAI,YAAW,EAAE,GAAG,KAAK,MAAM,SAAS,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,EAAE,CAAC;AAAA,EACpF;AAAA,EAEQ,KAAK,OAAiB,KAAa,MAAsC;AAC7E,QAAI,eAAe,KAAK,IAAI,KAAK,UAAW;AAC5C,QAAI;AACA,YAAM,SAAS;AAAA,QACX,IAAI,KAAK,IAAI,EAAE,YAAY;AAAA,QAC3B;AAAA,QACA;AAAA,QACA,GAAG,KAAK;AAAA,QACR,GAAG,KAAK;AAAA,QACR,GAAI,QAAQ,CAAC;AAAA,MACjB;AACA,YAAM,OAAO,KAAK,UAAU,MAAM;AAClC,UAAI,UAAU,WAAW,UAAU,QAAS,MAAK,KAAK,MAAM,IAAI;AAAA,UAC3D,MAAK,KAAK,IAAI,IAAI;AAAA,IAC3B,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;;;AC5GA,8BAAkC;AAmC3B,SAAS,UAAkB;AAC9B,MAAI;AACA,WAAO,YAAY,IAAI;AAAA,EAC3B,QAAQ;AACJ,WAAO,KAAK,IAAI;AAAA,EACpB;AACJ;AAGA,IAAM,cAAc;AAEpB,IAAM,aAAa;AAUnB,SAAS,aAAa,MAAsB;AACxC,QAAM,YAAY,OAAO,IAAI,EAAE,QAAQ,aAAa,GAAG;AACvD,MAAI,QAAQ;AACZ,MAAI,MAAM,UAAU;AACpB,SAAO,QAAQ,OAAO,UAAU,WAAW,KAAK,MAAM,WAAY;AAClE,SAAO,MAAM,SAAS,UAAU,WAAW,MAAM,CAAC,MAAM,WAAY;AACpE,SAAO,UAAU,MAAM,OAAO,GAAG;AACrC;AAOA,SAAS,aAAa,MAAsB;AACxC,MAAI,MAAM;AACV,aAAW,MAAM,OAAO,IAAI,GAAG;AAC3B,UAAM,OAAO,GAAG,YAAY,CAAC;AAE7B,QAAI,QAAQ,MAAQ,OAAO,OAAQ,OAAO,OAAO,OAAO,MAAM;AAC1D,aAAO;AAAA,IACX,OAAO;AACH,aAAO;AAAA,IACX;AAAA,EACJ;AACA,SAAO,IAAI,QAAQ,OAAO,GAAG,EAAE,KAAK;AACxC;AAGA,SAAS,OAAO,KAAqB;AACjC,MAAI,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AAClC,SAAO,OAAO,KAAK,MAAM,MAAM,GAAG,IAAI,GAAG;AAC7C;AAOO,SAAS,mBAAmB,OAA4C;AAC3E,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,OAAO;AACnB,UAAM,OAAO,aAAa,EAAE,IAAI;AAChC,QAAI,CAAC,KAAM;AACX,QAAI,OAAO,GAAG,IAAI,QAAQ,OAAO,EAAE,GAAG,CAAC;AACvC,QAAI,EAAE,MAAM;AACR,YAAM,OAAO,aAAa,EAAE,IAAI;AAChC,UAAI,KAAM,SAAQ,UAAU,IAAI;AAAA,IACpC;AACA,UAAM,KAAK,IAAI;AAAA,EACnB;AACA,SAAO,MAAM,KAAK,IAAI;AAC1B;AAOO,IAAM,cAAN,MAAM,YAAW;AAAA,EAAjB;AACH,SAAiB,SAA6B,CAAC;AAa/C,SAAQ,YAAY;AAAA;AAAA;AAAA,EAUpB,OAAO,MAAc,KAAa,MAAqB;AACnD,SAAK,OAAO,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAqB;AACjB,SAAK,YAAY;AAAA,EACrB;AAAA;AAAA,EAGA,IAAI,gBAAyB;AACzB,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAa,UAAkB,OAAe,KAAmB;AAC7D,QAAI,CAAC,KAAK,UAAW;AACrB,UAAM,SAAU,KAAK,YAAL,KAAK,UAAY,oBAAI,IAAI;AACzC,QAAI,OAAO,OAAO,IAAI,QAAQ;AAC9B,QAAI,CAAC,MAAM;AACP,aAAO,CAAC;AACR,aAAO,IAAI,UAAU,IAAI;AAAA,IAC7B;AACA,QAAI,KAAK,UAAU,YAAW,WAAY;AAC1C,SAAK,KAAK,EAAE,OAAO,OAAO,KAAK,GAAG,KAAK,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,EAAE,CAAC;AAAA,EACtF;AAAA;AAAA,EAGA,QAAQ,UAAiD;AACrD,WAAO,KAAK,SAAS,IAAI,QAAQ,KAAK,CAAC;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAc,MAA2B;AAC3C,UAAM,KAAK,QAAQ;AACnB,QAAI,OAAO;AACX,WAAO,MAAM;AACT,UAAI,KAAM;AACV,aAAO;AACP,WAAK,OAAO,MAAM,QAAQ,IAAI,IAAI,IAAI;AAAA,IAC1C;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,QAAW,MAAc,IAA0B,MAA2B;AAChF,UAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AACjC,QAAI;AACA,aAAO,MAAM,GAAG;AAAA,IACpB,UAAE;AACE,UAAI;AAAA,IACR;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAM,MAAc,KAAa,MAAqB;AAClD,UAAM,MAAM,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACpD,UAAM,aAAc,KAAK,gBAAL,KAAK,cAAgB,oBAAI,IAAI;AACjD,QAAI,QAAQ,WAAW,IAAI,IAAI;AAC/B,QAAI,CAAC,OAAO;AACR,YAAM,OAAyB,EAAE,MAAM,KAAK,EAAE;AAC9C,cAAQ,EAAE,MAAM,OAAO,GAAG,KAAK;AAC/B,iBAAW,IAAI,MAAM,KAAK;AAC1B,WAAK,OAAO,KAAK,IAAI;AAAA,IACzB;AACA,UAAM,SAAS;AACf,UAAM,KAAK,OAAO;AAClB,QAAI,KAAM,OAAM,OAAO;AACvB,UAAM,KAAK,OAAO,MAAM,OAAO,GAAG,MAAM,KAAK,IAAI,MAAM,IAAI,KAAK,OAAO,MAAM,KAAK;AAAA,EACtF;AAAA;AAAA,EAGA,QAAqC;AACjC,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA,EAGA,WAAmB;AACf,WAAO,mBAAmB,KAAK,MAAM;AAAA,EACzC;AACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AApIa,YAqBe,aAAa;AArBlC,IAAM,aAAN;AAuJP,IAAM,YAAY,uBAAO,IAAI,8CAA8C;AAC3E,IAAM,cAAc;AACpB,IAAM,QACF,YAAY,SAAS,MAAM,YAAY,SAAS,IAAI,IAAI,0CAA8B;AAGnF,SAAS,kBAAqB,QAAoB,IAAgB;AACrE,SAAO,MAAM,IAAI,QAAQ,EAAE;AAC/B;AAGO,SAAS,oBAA4C;AACxD,SAAO,MAAM,SAAS;AAC1B;AAGO,SAAS,mBAAmB,MAAc,KAAa,MAAqB;AAC/E,QAAM,SAAS,GAAG,OAAO,MAAM,KAAK,IAAI;AAC5C;AAOO,SAAS,kBAAkB,MAAc,MAA2B;AACvE,QAAM,IAAI,MAAM,SAAS;AACzB,MAAI,CAAC,EAAG,QAAO,MAAM;AAAA,EAAC;AACtB,SAAO,EAAE,MAAM,MAAM,IAAI;AAC7B;AAMA,eAAsB,oBAClB,MACA,IACA,MACU;AACV,QAAM,IAAI,MAAM,SAAS;AACzB,MAAI,CAAC,EAAG,QAAO,GAAG;AAClB,SAAO,EAAE,QAAQ,MAAM,IAAI,IAAI;AACnC;AAQO,SAAS,kBAAkB,MAAc,KAAa,MAAqB;AAC9E,QAAM,SAAS,GAAG,MAAM,MAAM,KAAK,IAAI;AAC3C;AAQO,SAAS,yBAAyB,UAAkB,OAAe,KAAmB;AACzF,QAAM,SAAS,GAAG,aAAa,UAAU,OAAO,GAAG;AACvD;AA8CA,IAAM,WAAW,uBAAO,IAAI,iDAAiD;AAC7E,IAAM,aAAa;AACnB,IAAM,YACF,WAAW,QAAQ,MAAM,WAAW,QAAQ,IAAI,IAAI,0CAAsC;AAOvF,SAAS,sBAAyB,MAA0B,IAAgB;AAC/E,SAAO,UAAU,IAAI,MAAM,EAAE;AACjC;AASO,SAAS,sBAA4B;AACxC,QAAM,IAAI,UAAU,SAAS;AAC7B,MAAI,GAAG;AACH,MAAE,UAAU;AACZ,MAAE,aAAa;AAAA,EACnB;AACJ;AAGO,SAAS,0BAAmC;AAC/C,SAAO,UAAU,SAAS,GAAG,WAAW;AAC5C;AAOO,SAAS,6BAAsC;AAClD,SAAO,UAAU,SAAS,GAAG,cAAc;AAC/C;","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 isPerfDisclosurePrincipal,\n type ServerTimingMark,\n type ServerTimingDetail,\n type PerfDisclosureGate,\n} from './perf-timing.js';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Canonical service-registry names for the host's observability\n * backends. Plugins look these up to discover the configured\n * {@link MetricsRegistry} / {@link ErrorReporter} without each host\n * having to thread observability config through every plugin\n * constructor.\n *\n * See `@objectstack/runtime` → `ObservabilityServicePlugin` for the\n * registration side.\n */\nexport const OBSERVABILITY_METRICS_SERVICE = 'observability:metrics';\nexport const OBSERVABILITY_ERRORS_SERVICE = 'observability:errors';\n","// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Semantic conventions — canonical metric names emitted by the\n * framework. Listed here so hosts can wire alerts/dashboards against\n * a stable namespace and so call sites don't sprinkle string\n * literals through the code base.\n *\n * Naming follows Prometheus conventions:\n *\n * - snake_case identifiers.\n * - `_total` suffix for monotonic counters.\n * - `_ms`, `_seconds`, `_bytes` suffixes for histograms / gauges\n * with units.\n *\n * Groups roughly mirror the framework subsystems that emit them.\n * Cloud-specific metrics (DO restarts, Workers Analytics Engine\n * writes, …) do NOT belong here — they are deployment-specific and\n * stay in the deployment repo.\n */\nexport const SEMCONV = {\n // ── HTTP — 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';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * One recorded phase of a request's server-side processing, serialized as a\n * single member of the `Server-Timing` header.\n */\nexport interface ServerTimingMark {\n /** Metric name. Coerced to a Server-Timing token on record. */\n name: string;\n /** Duration in milliseconds. */\n dur: number;\n /** Optional human-readable description (rendered as the quoted `desc`). */\n desc?: string;\n}\n\n/**\n * One recorded sub-event when DETAIL capture is on (see\n * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds\n * every query into a single count+duration — a detail sample keeps the\n * individual event so an admin can see *which* queries ran and which was\n * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED\n * statement, bindings stripped — the query shape, never literal row values).\n */\nexport interface ServerTimingDetail {\n /** Event label — e.g. a parametrized SQL statement (no bindings). */\n label: string;\n /** Duration in milliseconds. */\n dur: number;\n}\n\n/**\n * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not\n * affected by wall-clock adjustments); falls back to `Date.now()` on the rare\n * runtime where `performance` is unavailable.\n */\nexport function perfNow(): number {\n try {\n return performance.now();\n } catch {\n return Date.now();\n }\n}\n\n/** Characters not allowed in a Server-Timing metric name (a token). */\nconst NAME_UNSAFE = /[^A-Za-z0-9_-]+/g;\n\nconst UNDERSCORE = 0x5f;\n\n/**\n * Coerce an arbitrary string into a Server-Timing token: non-token runs become\n * a single underscore, then leading/trailing underscores are trimmed.\n *\n * The trim is a linear scan rather than a `/^_+|_+$/g` regex on purpose - the\n * anchored `_+$` quantifier backtracks polynomially on underscore-heavy input\n * (CodeQL js/polynomial-redos), and this name comes from a public-API argument.\n */\nfunction sanitizeName(name: string): string {\n const collapsed = String(name).replace(NAME_UNSAFE, '_');\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed.charCodeAt(start) === UNDERSCORE) start++;\n while (end > start && collapsed.charCodeAt(end - 1) === UNDERSCORE) end--;\n return collapsed.slice(start, end);\n}\n\n/**\n * Make a description safe to embed in a quoted-string. Backslashes and double\n * quotes would terminate the quoting; control chars (incl. CR/LF) could forge\n * headers. Collapse anything outside a conservative printable set to a space.\n */\nfunction sanitizeDesc(desc: string): string {\n let out = '';\n for (const ch of String(desc)) {\n const code = ch.codePointAt(0)!;\n // Printable ASCII excluding `\"` (0x22) and `\\` (0x5C); drop the rest.\n if (code >= 0x20 && code < 0x7f && ch !== '\"' && ch !== '\\\\') {\n out += ch;\n } else {\n out += ' ';\n }\n }\n return out.replace(/ +/g, ' ').trim();\n}\n\n/** Round to at most 2 decimals without trailing-zero noise (`12.3`, not `12.30`). */\nfunction fmtDur(dur: number): string {\n if (!Number.isFinite(dur)) return '0';\n return String(Math.round(dur * 100) / 100);\n}\n\n/**\n * Serialize marks into a `Server-Timing` header value. Marks with an empty\n * name after sanitization are dropped (the grammar requires a token). Returns\n * `''` when there is nothing to emit so callers can skip the header.\n */\nexport function formatServerTiming(marks: readonly ServerTimingMark[]): string {\n const parts: string[] = [];\n for (const m of marks) {\n const name = sanitizeName(m.name);\n if (!name) continue;\n let part = `${name};dur=${fmtDur(m.dur)}`;\n if (m.desc) {\n const desc = sanitizeDesc(m.desc);\n if (desc) part += `;desc=\"${desc}\"`;\n }\n parts.push(part);\n }\n return parts.join(', ');\n}\n\n/**\n * Collector for one request's timing phases. Not thread-safe by design - one\n * instance belongs to one request. All methods are allocation-light and never\n * throw on the hot path.\n */\nexport class PerfTiming {\n private readonly _marks: ServerTimingMark[] = [];\n /**\n * Live aggregate marks by name (see {@link count}). Lazily created so a\n * request that never aggregates pays nothing. Each entry points at a mark\n * already inserted into {@link _marks}, mutated in place as events arrive.\n */\n private _aggregates?: Map<string, { mark: ServerTimingMark; count: number; unit?: string }>;\n /**\n * Per-event detail samples by category, populated only while detail capture\n * is on (see {@link enableDetail}). Lazily created so a request that never\n * enables detail pays nothing.\n */\n private _detail?: Map<string, ServerTimingDetail[]>;\n private _detailOn = false;\n /**\n * Hard cap on stored detail samples per category — detail is only ever on\n * for a deliberate debug request, but a pathological request must not pin\n * unbounded memory. The aggregate {@link count} still reflects the true\n * total; only the retained per-event list is bounded.\n */\n private static readonly DETAIL_CAP = 1000;\n\n /** Record an already-measured phase. */\n record(name: string, dur: number, desc?: string): void {\n this._marks.push({ name, dur, desc });\n }\n\n /**\n * Turn on per-event DETAIL capture for this request. Off by default so the\n * hot path never allocates a per-event list; the HTTP middleware enables it\n * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.\n */\n enableDetail(): void {\n this._detailOn = true;\n }\n\n /** Whether per-event detail capture is on. */\n get detailEnabled(): boolean {\n return this._detailOn;\n }\n\n /**\n * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op\n * unless {@link enableDetail} was called, so the hot-path call site (the SQL\n * driver's query listener) pays only a boolean check when detail is off.\n * Bounded by {@link DETAIL_CAP}; excess events still count toward the\n * aggregate via {@link count} but are not retained individually.\n */\n recordDetail(category: string, label: string, dur: number): void {\n if (!this._detailOn) return;\n const detail = (this._detail ??= new Map());\n let list = detail.get(category);\n if (!list) {\n list = [];\n detail.set(category, list);\n }\n if (list.length >= PerfTiming.DETAIL_CAP) return;\n list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });\n }\n\n /** Retained detail samples for `category`, in record order (empty when none). */\n details(category: string): readonly ServerTimingDetail[] {\n return this._detail?.get(category) ?? [];\n }\n\n /**\n * Begin timing a phase. Returns an idempotent `end()` - the first call\n * records the elapsed duration; later calls are ignored, so it is safe to\n * call from both a success and an error path.\n */\n start(name: string, desc?: string): () => void {\n const t0 = perfNow();\n let done = false;\n return () => {\n if (done) return;\n done = true;\n this.record(name, perfNow() - t0, desc);\n };\n }\n\n /** Time an async (or sync) function, recording its elapsed duration. */\n async measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T> {\n const end = this.start(name, desc);\n try {\n return await fn();\n } finally {\n end();\n }\n }\n\n /**\n * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call\n * adds `dur` to the running total for `name` and increments a counter; the\n * mark serializes as `name;dur=<sum>;desc=\"<count> <unit>\"` (or just the\n * bare count when no `unit` is given).\n *\n * Use this for high-frequency phases — one SQL query, one hook execution —\n * where recording a distinct mark per event would blow the header out to\n * hundreds of entries. The single `db;dur=210;desc=\"6 queries\"` member is\n * both the total DB time and the query count, which is the number most\n * useful for spotting N sequential round-trips.\n *\n * The aggregate mark is inserted into the record stream the first time its\n * name is seen, so it keeps its natural position relative to explicit marks\n * (e.g. before the outer `total`, which is recorded last).\n */\n count(name: string, dur: number, unit?: string): void {\n const add = Number.isFinite(dur) && dur > 0 ? dur : 0;\n const aggregates = (this._aggregates ??= new Map());\n let entry = aggregates.get(name);\n if (!entry) {\n const mark: ServerTimingMark = { name, dur: 0 };\n entry = { mark, count: 0, unit };\n aggregates.set(name, entry);\n this._marks.push(mark);\n }\n entry.count += 1;\n entry.mark.dur += add;\n if (unit) entry.unit = unit;\n entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);\n }\n\n /** Snapshot of recorded marks, in record order. */\n marks(): readonly ServerTimingMark[] {\n return this._marks;\n }\n\n /** Serialize to a `Server-Timing` header value (`''` when empty). */\n toHeader(): string {\n return formatServerTiming(this._marks);\n }\n}\n\n// --- Ambient (request-scoped) collector -------------------------------\n\n/**\n * The ambient collector lives in ONE process-wide `AsyncLocalStorage`, pinned\n * to a global-registry symbol rather than a plain module-level `const`.\n *\n * Why: this module is consumed from many packages and can legitimately be\n * loaded more than once in a single process — the ESM build (`dist/index.js`)\n * and the CJS build (`dist/index.cjs`) are distinct module instances, and a\n * bundler may inline yet another copy. A plain `const store` would give each\n * copy its OWN store, so a request scope opened through one copy (the HTTP\n * server's `runWithPerfTiming`) would be invisible to code reading the ambient\n * collector through another copy (the SQL driver, the ObjectQL engine) — the\n * cross-layer `db` / `auth` / `hooks` spans would silently never record.\n * `Symbol.for` resolves to the same symbol across every copy, so they all share\n * the one store.\n */\nconst STORE_KEY = Symbol.for('@objectstack/observability:perf-timing-store');\nconst globalStore = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfTiming> | undefined>;\nconst store: AsyncLocalStorage<PerfTiming> =\n globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage<PerfTiming>());\n\n/** Run `fn` with `timing` as the ambient collector for the async call chain. */\nexport function runWithPerfTiming<T>(timing: PerfTiming, fn: () => T): T {\n return store.run(timing, fn);\n}\n\n/** The collector for the current request, or `undefined` outside a request. */\nexport function currentPerfTiming(): PerfTiming | undefined {\n return store.getStore();\n}\n\n/** Record a phase on the ambient collector. No-op when none is active. */\nexport function recordServerTiming(name: string, dur: number, desc?: string): void {\n store.getStore()?.record(name, dur, desc);\n}\n\n/**\n * Begin timing a phase on the ambient collector. Returns an `end()` callback;\n * when no collector is active the returned callback is a no-op so call sites\n * stay branch-free.\n */\nexport function startServerTiming(name: string, desc?: string): () => void {\n const t = store.getStore();\n if (!t) return () => {};\n return t.start(name, desc);\n}\n\n/**\n * Time an async function on the ambient collector. When no collector is active\n * the function is awaited with zero timing overhead.\n */\nexport async function measureServerTiming<T>(\n name: string,\n fn: () => T | Promise<T>,\n desc?: string,\n): Promise<T> {\n const t = store.getStore();\n if (!t) return fn();\n return t.measure(name, fn, desc);\n}\n\n/**\n * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the\n * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is\n * active, so the hot-path call sites (the SQL driver's query listener, the hook\n * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.\n */\nexport function countServerTiming(name: string, dur: number, unit?: string): void {\n store.getStore()?.count(name, dur, unit);\n}\n\n/**\n * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto\n * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no\n * collector is active OR detail capture is off, so the hot-path call site pays\n * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.\n */\nexport function recordServerTimingDetail(category: string, label: string, dur: number): void {\n store.getStore()?.recordDetail(category, label, dur);\n}\n\n// --- Disclosure gate (WHO may see the timing) -------------------------\n\n/**\n * Per-request disclosure gate — the policy counterpart to the collector.\n *\n * The {@link PerfTiming} collector only MEASURES; whether the measured\n * `Server-Timing` header is returned to the client is a separate decision. When\n * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has\n * opted the whole environment in, so the gate opens up front and every response\n * carries the header. When it is turned on PER-REQUEST — the caller sends an\n * `X-OS-Debug-Timing` header — the header must stay withheld until the request\n * proves an admin/service identity: phase durations are a mild\n * backend-fingerprinting surface, so an ordinary user must never be able to pull\n * them just by sending a header. The request path flips the gate open with\n * {@link allowPerfDisclosure} once it has resolved a privileged principal.\n *\n * Keeping this out of {@link PerfTiming} preserves the collector's invariant\n * (\"it only measures, it never decides whether to emit\").\n *\n * Two levels, because global mode discloses the basic header to everyone but the\n * richer per-query detail must stay admin-only:\n * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by\n * global mode for everyone, or by a proven admin per-request).\n * - `privileged` — the principal is a proven admin/service. Gates the richer,\n * SQL-shape-bearing detail payload, which must NEVER reach an\n * ordinary caller even when global mode is on.\n */\nexport interface PerfDisclosureGate {\n /** Whether the basic collected timing may be disclosed to the client. */\n allowed: boolean;\n /**\n * Whether the principal is a proven admin/service — gates the richer detail\n * payload independently of `allowed`. Absent = not privileged.\n */\n privileged?: boolean;\n}\n\n/**\n * The disclosure gate lives in its OWN global-registry-pinned\n * `AsyncLocalStorage`, for the same cross-module-copy reason as the collector\n * store above: the middleware seeds the gate and the dispatcher (a different\n * package, possibly a different module copy) flips it open — both must see the\n * one store.\n */\nconst GATE_KEY = Symbol.for('@objectstack/observability:perf-disclosure-gate');\nconst globalGate = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfDisclosureGate> | undefined>;\nconst gateStore: AsyncLocalStorage<PerfDisclosureGate> =\n globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage<PerfDisclosureGate>());\n\n/**\n * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.\n * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`\n * settles to decide whether to emit the header.\n */\nexport function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T {\n return gateStore.run(gate, fn);\n}\n\n/**\n * Open the ambient disclosure gate — the request has proven an admin/service\n * identity, so it may see its own `Server-Timing` header AND the richer detail\n * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A\n * no-op when no gate is active (perf-tuning off), so the call site stays\n * branch-free.\n */\nexport function allowPerfDisclosure(): void {\n const g = gateStore.getStore();\n if (g) {\n g.allowed = true;\n g.privileged = true;\n }\n}\n\n/** Whether the ambient disclosure gate is open. `false` when none is active. */\nexport function isPerfDisclosureAllowed(): boolean {\n return gateStore.getStore()?.allowed ?? false;\n}\n\n/**\n * Whether the ambient principal is a proven admin/service — gates the richer\n * detail payload. `false` when no gate is active or only global-mode disclosure\n * (not a proven admin) opened it.\n */\nexport function isPerfDisclosurePrivileged(): boolean {\n return gateStore.getStore()?.privileged ?? false;\n}\n\n/**\n * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header\n * (#2408 perf-tuning gating). The header exposes internal phase durations — a\n * mild backend-fingerprinting surface — so when timing is opened per-request via\n * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:\n *\n * - `isSystem` — internal/engine self-calls,\n * - `principalKind` `service` / `system` — service tokens & the system seed,\n * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.\n *\n * Ordinary human/guest/agent callers get `false`, so sending the debug header\n * yields no header for them. Global (env/option) perf mode bypasses this — it\n * opened the disclosure gate up front for the whole environment.\n *\n * This is the ONE definition of \"who may pull per-request timings\", shared by\n * every HTTP entry point that resolves a principal — the runtime dispatcher\n * (`timedResolveExecutionContext`), the REST server, and the standalone Hono\n * CRUD surface — so a new admin-serving path can never silently under- or\n * over-disclose by hand-rolling its own rule (#3361).\n */\nexport function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean {\n if (!ec) return false;\n if (ec.isSystem === true) return true;\n if (ec.principalKind === 'service' || ec.principalKind === 'system') return true;\n return ec.posture === 'PLATFORM_ADMIN' || ec.posture === 'TENANT_ADMIN';\n}\n"],"mappings":"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","names":[]}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Logger } from '@objectstack/spec/contracts';
2
2
  export { Logger } from '@objectstack/spec/contracts';
3
+ import { ExecutionContext } from '@objectstack/spec/kernel';
3
4
 
4
5
  /**
5
6
  * Observability contracts for ObjectStack.
@@ -587,5 +588,26 @@ declare function isPerfDisclosureAllowed(): boolean;
587
588
  * (not a proven admin) opened it.
588
589
  */
589
590
  declare function isPerfDisclosurePrivileged(): boolean;
591
+ /**
592
+ * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header
593
+ * (#2408 perf-tuning gating). The header exposes internal phase durations — a
594
+ * mild backend-fingerprinting surface — so when timing is opened per-request via
595
+ * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:
596
+ *
597
+ * - `isSystem` — internal/engine self-calls,
598
+ * - `principalKind` `service` / `system` — service tokens & the system seed,
599
+ * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.
600
+ *
601
+ * Ordinary human/guest/agent callers get `false`, so sending the debug header
602
+ * yields no header for them. Global (env/option) perf mode bypasses this — it
603
+ * opened the disclosure gate up front for the whole environment.
604
+ *
605
+ * This is the ONE definition of "who may pull per-request timings", shared by
606
+ * every HTTP entry point that resolves a principal — the runtime dispatcher
607
+ * (`timedResolveExecutionContext`), the REST server, and the standalone Hono
608
+ * CRUD surface — so a new admin-serving path can never silently under- or
609
+ * over-disclose by hand-rolling its own rule (#3361).
610
+ */
611
+ declare function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean;
590
612
 
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 };
613
+ export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrincipal, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Logger } from '@objectstack/spec/contracts';
2
2
  export { Logger } from '@objectstack/spec/contracts';
3
+ import { ExecutionContext } from '@objectstack/spec/kernel';
3
4
 
4
5
  /**
5
6
  * Observability contracts for ObjectStack.
@@ -587,5 +588,26 @@ declare function isPerfDisclosureAllowed(): boolean;
587
588
  * (not a proven admin) opened it.
588
589
  */
589
590
  declare function isPerfDisclosurePrivileged(): boolean;
591
+ /**
592
+ * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header
593
+ * (#2408 perf-tuning gating). The header exposes internal phase durations — a
594
+ * mild backend-fingerprinting surface — so when timing is opened per-request via
595
+ * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:
596
+ *
597
+ * - `isSystem` — internal/engine self-calls,
598
+ * - `principalKind` `service` / `system` — service tokens & the system seed,
599
+ * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.
600
+ *
601
+ * Ordinary human/guest/agent callers get `false`, so sending the debug header
602
+ * yields no header for them. Global (env/option) perf mode bypasses this — it
603
+ * opened the disclosure gate up front for the whole environment.
604
+ *
605
+ * This is the ONE definition of "who may pull per-request timings", shared by
606
+ * every HTTP entry point that resolves a principal — the runtime dispatcher
607
+ * (`timedResolveExecutionContext`), the REST server, and the standalone Hono
608
+ * CRUD surface — so a new admin-serving path can never silently under- or
609
+ * over-disclose by hand-rolling its own rule (#3361).
610
+ */
611
+ declare function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean;
590
612
 
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 };
613
+ export { type CapturedError, ConsoleErrorReporter, ConsoleLogger, ConsoleMetricsRegistry, type ErrorReporter, InMemoryErrorReporter, InMemoryMetricsRegistry, JsonLogger, LOG_LEVELS, type LogLevel, type MetricSample, type MetricsRegistry, NoopErrorReporter, NoopLogger, NoopMetricsRegistry, OBSERVABILITY_ERRORS_SERVICE, OBSERVABILITY_METRICS_SERVICE, type OtlpHttpExporterOptions, OtlpHttpMetricsRegistry, type PerfDisclosureGate, PerfTiming, RUNTIME_METRICS, SEMCONV, type ServerTimingDetail, type ServerTimingMark, allowPerfDisclosure, countServerTiming, currentPerfTiming, formatServerTiming, isPerfDisclosureAllowed, isPerfDisclosurePrincipal, isPerfDisclosurePrivileged, measureServerTiming, perfNow, recordServerTiming, recordServerTimingDetail, runWithPerfDisclosure, runWithPerfTiming, startServerTiming };
package/dist/index.js CHANGED
@@ -616,6 +616,12 @@ function isPerfDisclosureAllowed() {
616
616
  function isPerfDisclosurePrivileged() {
617
617
  return gateStore.getStore()?.privileged ?? false;
618
618
  }
619
+ function isPerfDisclosurePrincipal(ec) {
620
+ if (!ec) return false;
621
+ if (ec.isSystem === true) return true;
622
+ if (ec.principalKind === "service" || ec.principalKind === "system") return true;
623
+ return ec.posture === "PLATFORM_ADMIN" || ec.posture === "TENANT_ADMIN";
624
+ }
619
625
  export {
620
626
  ConsoleErrorReporter,
621
627
  ConsoleLogger,
@@ -638,6 +644,7 @@ export {
638
644
  currentPerfTiming,
639
645
  formatServerTiming,
640
646
  isPerfDisclosureAllowed,
647
+ isPerfDisclosurePrincipal,
641
648
  isPerfDisclosurePrivileged,
642
649
  measureServerTiming,
643
650
  perfNow,
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}, {@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":[]}
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';\nimport type { ExecutionContext } from '@objectstack/spec/kernel';\n\n/**\n * One recorded phase of a request's server-side processing, serialized as a\n * single member of the `Server-Timing` header.\n */\nexport interface ServerTimingMark {\n /** Metric name. Coerced to a Server-Timing token on record. */\n name: string;\n /** Duration in milliseconds. */\n dur: number;\n /** Optional human-readable description (rendered as the quoted `desc`). */\n desc?: string;\n}\n\n/**\n * One recorded sub-event when DETAIL capture is on (see\n * {@link PerfTiming.enableDetail}). Unlike the aggregate `db` mark — which folds\n * every query into a single count+duration — a detail sample keeps the\n * individual event so an admin can see *which* queries ran and which was\n * slowest. `label` is a description of the event (for SQL: the PARAMETRIZED\n * statement, bindings stripped — the query shape, never literal row values).\n */\nexport interface ServerTimingDetail {\n /** Event label — e.g. a parametrized SQL statement (no bindings). */\n label: string;\n /** Duration in milliseconds. */\n dur: number;\n}\n\n/**\n * Monotonic millisecond clock. Prefers `performance.now()` (monotonic, not\n * affected by wall-clock adjustments); falls back to `Date.now()` on the rare\n * runtime where `performance` is unavailable.\n */\nexport function perfNow(): number {\n try {\n return performance.now();\n } catch {\n return Date.now();\n }\n}\n\n/** Characters not allowed in a Server-Timing metric name (a token). */\nconst NAME_UNSAFE = /[^A-Za-z0-9_-]+/g;\n\nconst UNDERSCORE = 0x5f;\n\n/**\n * Coerce an arbitrary string into a Server-Timing token: non-token runs become\n * a single underscore, then leading/trailing underscores are trimmed.\n *\n * The trim is a linear scan rather than a `/^_+|_+$/g` regex on purpose - the\n * anchored `_+$` quantifier backtracks polynomially on underscore-heavy input\n * (CodeQL js/polynomial-redos), and this name comes from a public-API argument.\n */\nfunction sanitizeName(name: string): string {\n const collapsed = String(name).replace(NAME_UNSAFE, '_');\n let start = 0;\n let end = collapsed.length;\n while (start < end && collapsed.charCodeAt(start) === UNDERSCORE) start++;\n while (end > start && collapsed.charCodeAt(end - 1) === UNDERSCORE) end--;\n return collapsed.slice(start, end);\n}\n\n/**\n * Make a description safe to embed in a quoted-string. Backslashes and double\n * quotes would terminate the quoting; control chars (incl. CR/LF) could forge\n * headers. Collapse anything outside a conservative printable set to a space.\n */\nfunction sanitizeDesc(desc: string): string {\n let out = '';\n for (const ch of String(desc)) {\n const code = ch.codePointAt(0)!;\n // Printable ASCII excluding `\"` (0x22) and `\\` (0x5C); drop the rest.\n if (code >= 0x20 && code < 0x7f && ch !== '\"' && ch !== '\\\\') {\n out += ch;\n } else {\n out += ' ';\n }\n }\n return out.replace(/ +/g, ' ').trim();\n}\n\n/** Round to at most 2 decimals without trailing-zero noise (`12.3`, not `12.30`). */\nfunction fmtDur(dur: number): string {\n if (!Number.isFinite(dur)) return '0';\n return String(Math.round(dur * 100) / 100);\n}\n\n/**\n * Serialize marks into a `Server-Timing` header value. Marks with an empty\n * name after sanitization are dropped (the grammar requires a token). Returns\n * `''` when there is nothing to emit so callers can skip the header.\n */\nexport function formatServerTiming(marks: readonly ServerTimingMark[]): string {\n const parts: string[] = [];\n for (const m of marks) {\n const name = sanitizeName(m.name);\n if (!name) continue;\n let part = `${name};dur=${fmtDur(m.dur)}`;\n if (m.desc) {\n const desc = sanitizeDesc(m.desc);\n if (desc) part += `;desc=\"${desc}\"`;\n }\n parts.push(part);\n }\n return parts.join(', ');\n}\n\n/**\n * Collector for one request's timing phases. Not thread-safe by design - one\n * instance belongs to one request. All methods are allocation-light and never\n * throw on the hot path.\n */\nexport class PerfTiming {\n private readonly _marks: ServerTimingMark[] = [];\n /**\n * Live aggregate marks by name (see {@link count}). Lazily created so a\n * request that never aggregates pays nothing. Each entry points at a mark\n * already inserted into {@link _marks}, mutated in place as events arrive.\n */\n private _aggregates?: Map<string, { mark: ServerTimingMark; count: number; unit?: string }>;\n /**\n * Per-event detail samples by category, populated only while detail capture\n * is on (see {@link enableDetail}). Lazily created so a request that never\n * enables detail pays nothing.\n */\n private _detail?: Map<string, ServerTimingDetail[]>;\n private _detailOn = false;\n /**\n * Hard cap on stored detail samples per category — detail is only ever on\n * for a deliberate debug request, but a pathological request must not pin\n * unbounded memory. The aggregate {@link count} still reflects the true\n * total; only the retained per-event list is bounded.\n */\n private static readonly DETAIL_CAP = 1000;\n\n /** Record an already-measured phase. */\n record(name: string, dur: number, desc?: string): void {\n this._marks.push({ name, dur, desc });\n }\n\n /**\n * Turn on per-event DETAIL capture for this request. Off by default so the\n * hot path never allocates a per-event list; the HTTP middleware enables it\n * only for an admin-gated `X-OS-Debug-Timing: json` request. Idempotent.\n */\n enableDetail(): void {\n this._detailOn = true;\n }\n\n /** Whether per-event detail capture is on. */\n get detailEnabled(): boolean {\n return this._detailOn;\n }\n\n /**\n * Record one per-event detail sample under `category` (e.g. `'db'`). A no-op\n * unless {@link enableDetail} was called, so the hot-path call site (the SQL\n * driver's query listener) pays only a boolean check when detail is off.\n * Bounded by {@link DETAIL_CAP}; excess events still count toward the\n * aggregate via {@link count} but are not retained individually.\n */\n recordDetail(category: string, label: string, dur: number): void {\n if (!this._detailOn) return;\n const detail = (this._detail ??= new Map());\n let list = detail.get(category);\n if (!list) {\n list = [];\n detail.set(category, list);\n }\n if (list.length >= PerfTiming.DETAIL_CAP) return;\n list.push({ label: String(label), dur: Number.isFinite(dur) && dur > 0 ? dur : 0 });\n }\n\n /** Retained detail samples for `category`, in record order (empty when none). */\n details(category: string): readonly ServerTimingDetail[] {\n return this._detail?.get(category) ?? [];\n }\n\n /**\n * Begin timing a phase. Returns an idempotent `end()` - the first call\n * records the elapsed duration; later calls are ignored, so it is safe to\n * call from both a success and an error path.\n */\n start(name: string, desc?: string): () => void {\n const t0 = perfNow();\n let done = false;\n return () => {\n if (done) return;\n done = true;\n this.record(name, perfNow() - t0, desc);\n };\n }\n\n /** Time an async (or sync) function, recording its elapsed duration. */\n async measure<T>(name: string, fn: () => T | Promise<T>, desc?: string): Promise<T> {\n const end = this.start(name, desc);\n try {\n return await fn();\n } finally {\n end();\n }\n }\n\n /**\n * Accumulate a repeated sub-phase into a SINGLE aggregate mark. Each call\n * adds `dur` to the running total for `name` and increments a counter; the\n * mark serializes as `name;dur=<sum>;desc=\"<count> <unit>\"` (or just the\n * bare count when no `unit` is given).\n *\n * Use this for high-frequency phases — one SQL query, one hook execution —\n * where recording a distinct mark per event would blow the header out to\n * hundreds of entries. The single `db;dur=210;desc=\"6 queries\"` member is\n * both the total DB time and the query count, which is the number most\n * useful for spotting N sequential round-trips.\n *\n * The aggregate mark is inserted into the record stream the first time its\n * name is seen, so it keeps its natural position relative to explicit marks\n * (e.g. before the outer `total`, which is recorded last).\n */\n count(name: string, dur: number, unit?: string): void {\n const add = Number.isFinite(dur) && dur > 0 ? dur : 0;\n const aggregates = (this._aggregates ??= new Map());\n let entry = aggregates.get(name);\n if (!entry) {\n const mark: ServerTimingMark = { name, dur: 0 };\n entry = { mark, count: 0, unit };\n aggregates.set(name, entry);\n this._marks.push(mark);\n }\n entry.count += 1;\n entry.mark.dur += add;\n if (unit) entry.unit = unit;\n entry.mark.desc = entry.unit ? `${entry.count} ${entry.unit}` : String(entry.count);\n }\n\n /** Snapshot of recorded marks, in record order. */\n marks(): readonly ServerTimingMark[] {\n return this._marks;\n }\n\n /** Serialize to a `Server-Timing` header value (`''` when empty). */\n toHeader(): string {\n return formatServerTiming(this._marks);\n }\n}\n\n// --- Ambient (request-scoped) collector -------------------------------\n\n/**\n * The ambient collector lives in ONE process-wide `AsyncLocalStorage`, pinned\n * to a global-registry symbol rather than a plain module-level `const`.\n *\n * Why: this module is consumed from many packages and can legitimately be\n * loaded more than once in a single process — the ESM build (`dist/index.js`)\n * and the CJS build (`dist/index.cjs`) are distinct module instances, and a\n * bundler may inline yet another copy. A plain `const store` would give each\n * copy its OWN store, so a request scope opened through one copy (the HTTP\n * server's `runWithPerfTiming`) would be invisible to code reading the ambient\n * collector through another copy (the SQL driver, the ObjectQL engine) — the\n * cross-layer `db` / `auth` / `hooks` spans would silently never record.\n * `Symbol.for` resolves to the same symbol across every copy, so they all share\n * the one store.\n */\nconst STORE_KEY = Symbol.for('@objectstack/observability:perf-timing-store');\nconst globalStore = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfTiming> | undefined>;\nconst store: AsyncLocalStorage<PerfTiming> =\n globalStore[STORE_KEY] ?? (globalStore[STORE_KEY] = new AsyncLocalStorage<PerfTiming>());\n\n/** Run `fn` with `timing` as the ambient collector for the async call chain. */\nexport function runWithPerfTiming<T>(timing: PerfTiming, fn: () => T): T {\n return store.run(timing, fn);\n}\n\n/** The collector for the current request, or `undefined` outside a request. */\nexport function currentPerfTiming(): PerfTiming | undefined {\n return store.getStore();\n}\n\n/** Record a phase on the ambient collector. No-op when none is active. */\nexport function recordServerTiming(name: string, dur: number, desc?: string): void {\n store.getStore()?.record(name, dur, desc);\n}\n\n/**\n * Begin timing a phase on the ambient collector. Returns an `end()` callback;\n * when no collector is active the returned callback is a no-op so call sites\n * stay branch-free.\n */\nexport function startServerTiming(name: string, desc?: string): () => void {\n const t = store.getStore();\n if (!t) return () => {};\n return t.start(name, desc);\n}\n\n/**\n * Time an async function on the ambient collector. When no collector is active\n * the function is awaited with zero timing overhead.\n */\nexport async function measureServerTiming<T>(\n name: string,\n fn: () => T | Promise<T>,\n desc?: string,\n): Promise<T> {\n const t = store.getStore();\n if (!t) return fn();\n return t.measure(name, fn, desc);\n}\n\n/**\n * Accumulate a repeated sub-phase (one SQL query, one hook execution) onto the\n * ambient collector — see {@link PerfTiming.count}. A no-op when no collector is\n * active, so the hot-path call sites (the SQL driver's query listener, the hook\n * runner) pay only a single `AsyncLocalStorage` lookup when perf-tuning is off.\n */\nexport function countServerTiming(name: string, dur: number, unit?: string): void {\n store.getStore()?.count(name, dur, unit);\n}\n\n/**\n * Record a per-event DETAIL sample (e.g. one parametrized SQL statement) onto\n * the ambient collector — see {@link PerfTiming.recordDetail}. A no-op when no\n * collector is active OR detail capture is off, so the hot-path call site pays\n * only an `AsyncLocalStorage` lookup + a boolean check when not debugging.\n */\nexport function recordServerTimingDetail(category: string, label: string, dur: number): void {\n store.getStore()?.recordDetail(category, label, dur);\n}\n\n// --- Disclosure gate (WHO may see the timing) -------------------------\n\n/**\n * Per-request disclosure gate — the policy counterpart to the collector.\n *\n * The {@link PerfTiming} collector only MEASURES; whether the measured\n * `Server-Timing` header is returned to the client is a separate decision. When\n * perf-tuning is turned on GLOBALLY (env flag / plugin option) the operator has\n * opted the whole environment in, so the gate opens up front and every response\n * carries the header. When it is turned on PER-REQUEST — the caller sends an\n * `X-OS-Debug-Timing` header — the header must stay withheld until the request\n * proves an admin/service identity: phase durations are a mild\n * backend-fingerprinting surface, so an ordinary user must never be able to pull\n * them just by sending a header. The request path flips the gate open with\n * {@link allowPerfDisclosure} once it has resolved a privileged principal.\n *\n * Keeping this out of {@link PerfTiming} preserves the collector's invariant\n * (\"it only measures, it never decides whether to emit\").\n *\n * Two levels, because global mode discloses the basic header to everyone but the\n * richer per-query detail must stay admin-only:\n * - `allowed` — the basic `Server-Timing` header may be disclosed (opened by\n * global mode for everyone, or by a proven admin per-request).\n * - `privileged` — the principal is a proven admin/service. Gates the richer,\n * SQL-shape-bearing detail payload, which must NEVER reach an\n * ordinary caller even when global mode is on.\n */\nexport interface PerfDisclosureGate {\n /** Whether the basic collected timing may be disclosed to the client. */\n allowed: boolean;\n /**\n * Whether the principal is a proven admin/service — gates the richer detail\n * payload independently of `allowed`. Absent = not privileged.\n */\n privileged?: boolean;\n}\n\n/**\n * The disclosure gate lives in its OWN global-registry-pinned\n * `AsyncLocalStorage`, for the same cross-module-copy reason as the collector\n * store above: the middleware seeds the gate and the dispatcher (a different\n * package, possibly a different module copy) flips it open — both must see the\n * one store.\n */\nconst GATE_KEY = Symbol.for('@objectstack/observability:perf-disclosure-gate');\nconst globalGate = globalThis as unknown as Record<symbol, AsyncLocalStorage<PerfDisclosureGate> | undefined>;\nconst gateStore: AsyncLocalStorage<PerfDisclosureGate> =\n globalGate[GATE_KEY] ?? (globalGate[GATE_KEY] = new AsyncLocalStorage<PerfDisclosureGate>());\n\n/**\n * Run `fn` with `gate` as the ambient disclosure gate for the async call chain.\n * The caller keeps its reference to `gate` and reads `gate.allowed` after `fn`\n * settles to decide whether to emit the header.\n */\nexport function runWithPerfDisclosure<T>(gate: PerfDisclosureGate, fn: () => T): T {\n return gateStore.run(gate, fn);\n}\n\n/**\n * Open the ambient disclosure gate — the request has proven an admin/service\n * identity, so it may see its own `Server-Timing` header AND the richer detail\n * payload. Sets both {@link PerfDisclosureGate.allowed} and `privileged`. A\n * no-op when no gate is active (perf-tuning off), so the call site stays\n * branch-free.\n */\nexport function allowPerfDisclosure(): void {\n const g = gateStore.getStore();\n if (g) {\n g.allowed = true;\n g.privileged = true;\n }\n}\n\n/** Whether the ambient disclosure gate is open. `false` when none is active. */\nexport function isPerfDisclosureAllowed(): boolean {\n return gateStore.getStore()?.allowed ?? false;\n}\n\n/**\n * Whether the ambient principal is a proven admin/service — gates the richer\n * detail payload. `false` when no gate is active or only global-mode disclosure\n * (not a proven admin) opened it.\n */\nexport function isPerfDisclosurePrivileged(): boolean {\n return gateStore.getStore()?.privileged ?? false;\n}\n\n/**\n * Whether a resolved principal may see a PER-REQUEST `Server-Timing` header\n * (#2408 perf-tuning gating). The header exposes internal phase durations — a\n * mild backend-fingerprinting surface — so when timing is opened per-request via\n * `X-OS-Debug-Timing` it is disclosed only to an admin/service identity:\n *\n * - `isSystem` — internal/engine self-calls,\n * - `principalKind` `service` / `system` — service tokens & the system seed,\n * - `posture` `PLATFORM_ADMIN` / `TENANT_ADMIN` — the derived admin rungs.\n *\n * Ordinary human/guest/agent callers get `false`, so sending the debug header\n * yields no header for them. Global (env/option) perf mode bypasses this — it\n * opened the disclosure gate up front for the whole environment.\n *\n * This is the ONE definition of \"who may pull per-request timings\", shared by\n * every HTTP entry point that resolves a principal — the runtime dispatcher\n * (`timedResolveExecutionContext`), the REST server, and the standalone Hono\n * CRUD surface — so a new admin-serving path can never silently under- or\n * over-disclose by hand-rolling its own rule (#3361).\n */\nexport function isPerfDisclosurePrincipal(ec: ExecutionContext | undefined): boolean {\n if (!ec) return false;\n if (ec.isSystem === true) return true;\n if (ec.principalKind === 'service' || ec.principalKind === 'system') return true;\n return ec.posture === 'PLATFORM_ADMIN' || ec.posture === 'TENANT_ADMIN';\n}\n"],"mappings":";AAYO,IAAM,gCAAgC;AACtC,IAAM,+BAA+B;;;ACOrC,IAAM,UAAU;AAAA;AAAA;AAAA,EAGnB,mBAAmB;AAAA;AAAA,EAEnB,uBAAuB;AAAA;AAAA;AAAA;AAAA;AAAA,EAKvB,wBAAwB;AAAA;AAAA;AAAA,EAIxB,wBAAwB;AAAA;AAAA,EAExB,4BAA4B;AAAA;AAAA,EAE5B,oBAAoB;AAAA;AAAA;AAAA,EAIpB,mBAAmB;AAAA;AAAA,EAEnB,kBAAkB;AAAA;AAAA,EAElB,kBAAkB;AAAA;AAAA;AAAA,EAIlB,sBAAsB;AAAA;AAAA,EAEtB,0BAA0B;AAAA;AAAA,EAE1B,4BAA4B;AAChC;AAQO,IAAM,kBAAkB;AAAA,EAC3B,mBAAmB,QAAQ;AAAA,EAC3B,uBAAuB,QAAQ;AAAA,EAC/B,wBAAwB,QAAQ;AACpC;;;ACxDO,IAAM,sBAAN,MAAqD;AAAA,EACxD,UAAgB;AAAA,EAAE;AAAA,EAClB,YAAkB;AAAA,EAAE;AAAA,EACpB,QAAc;AAAA,EAAE;AACpB;AAWO,IAAM,0BAAN,MAAyD;AAAA,EAAzD;AACH,SAAS,UAA0B,CAAC;AAAA;AAAA,EAEpC,QAAQ,MAAc,SAAiC,CAAC,GAAG,QAAgB,GAAS;AAChF,SAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,WAAW,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAC9E;AAAA,EAEA,UAAU,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC9E,SAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,aAAa,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAChF;AAAA,EAEA,MAAM,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC1E,SAAK,QAAQ,KAAK,EAAE,MAAM,MAAM,SAAS,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAC5E;AAAA;AAAA,EAGA,aAAa,MAAc,aAAqC,CAAC,GAAW;AACxE,WAAO,KAAK,QACP,OAAO,OAAK,EAAE,SAAS,aAAa,EAAE,SAAS,QAAQ,cAAc,EAAE,QAAQ,UAAU,CAAC,EAC1F,OAAO,CAAC,KAAK,MAAM,MAAM,EAAE,OAAO,CAAC;AAAA,EAC5C;AAAA;AAAA,EAGA,gBAAgB,MAAc,aAAqC,CAAC,GAAa;AAC7E,WAAO,KAAK,QACP,OAAO,OAAK,EAAE,SAAS,eAAe,EAAE,SAAS,QAAQ,cAAc,EAAE,QAAQ,UAAU,CAAC,EAC5F,IAAI,OAAK,EAAE,KAAK;AAAA,EACzB;AAAA;AAAA,EAGA,UAAU,MAAc,aAAqC,CAAC,GAAuB;AACjF,aAAS,IAAI,KAAK,QAAQ,SAAS,GAAG,KAAK,GAAG,KAAK;AAC/C,YAAM,IAAI,KAAK,QAAQ,CAAC;AACxB,UAAI,EAAE,SAAS,WAAW,EAAE,SAAS,QAAQ,cAAc,EAAE,QAAQ,UAAU,GAAG;AAC9E,eAAO,EAAE;AAAA,MACb;AAAA,IACJ;AACA,WAAO;AAAA,EACX;AAAA;AAAA,EAGA,QAAc;AACV,SAAK,QAAQ,SAAS;AAAA,EAC1B;AACJ;AAEA,SAAS,cAAc,QAAgC,UAA2C;AAC9F,aAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,QAAQ,GAAG;AAC3C,QAAI,OAAO,CAAC,MAAM,EAAG,QAAO;AAAA,EAChC;AACA,SAAO;AACX;AAWO,IAAM,yBAAN,MAAwD;AAAA,EAC3D,YAA6B,OAA2D,CAAC,GAAG;AAA/D;AAAA,EAAiE;AAAA,EAE9F,QAAQ,MAAc,SAAiC,CAAC,GAAG,QAAgB,GAAS;AAChF,SAAK,KAAK,WAAW,MAAM,OAAO,MAAM;AAAA,EAC5C;AAAA,EACA,UAAU,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC9E,SAAK,KAAK,aAAa,MAAM,OAAO,MAAM;AAAA,EAC9C;AAAA,EACA,MAAM,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC1E,SAAK,KAAK,SAAS,MAAM,OAAO,MAAM;AAAA,EAC1C;AAAA,EAEQ,KAAK,MAAc,MAAc,OAAe,QAAsC;AAC1F,QAAI;AACA,YAAM,OAAO,KAAK,KAAK,SAAS,CAAC,MAAM;AAAE,gBAAQ,IAAI,CAAC;AAAA,MAAG;AACzD,YAAM,SAAS,KAAK,KAAK,UAAU;AACnC,YAAM,WAAW,OAAO,QAAQ,MAAM,EACjC,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,EAAE,EAC3C,KAAK,GAAG;AACb,WAAK,GAAG,MAAM,IAAI,IAAI,IAAI,IAAI,IAAI,KAAK,GAAG,WAAW,MAAM,WAAW,EAAE,EAAE;AAAA,IAC9E,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AA+DO,IAAM,0BAAN,MAAyD;AAAA,EAS5D,YAAY,SAAkC;AAR9C,SAAQ,SAAyB,CAAC;AAS9B,SAAK,WAAW,kBAAkB,QAAQ,QAAQ;AAClD,SAAK,UAAU,QAAQ,WAAW,CAAC;AACnC,SAAK,WAAW,QAAQ,YAAY,CAAC;AACrC,SAAK,gBAAgB,QAAQ,iBAAiB;AAC9C,SAAK,YAAY,QAAQ,UAAU,OAAO,UAAU,cAAc,MAAM,KAAK,UAAU,IAAI;AAC3F,SAAK,UAAU,QAAQ,YAAY,MAAM;AAAA,IAAE;AAAA,EAC/C;AAAA,EAEA,QAAQ,MAAc,SAAiC,CAAC,GAAG,QAAgB,GAAS;AAChF,SAAK,OAAO,EAAE,MAAM,MAAM,WAAW,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACxE;AAAA,EACA,UAAU,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC9E,SAAK,OAAO,EAAE,MAAM,MAAM,aAAa,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EAC1E;AAAA,EACA,MAAM,MAAc,OAAe,SAAiC,CAAC,GAAS;AAC1E,SAAK,OAAO,EAAE,MAAM,MAAM,SAAS,OAAO,QAAQ,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACtE;AAAA,EAEQ,OAAO,QAA4B;AACvC,SAAK,OAAO,KAAK,MAAM;AACvB,QAAI,KAAK,OAAO,UAAU,KAAK,eAAe;AAE1C,WAAK,KAAK,MAAM,EAAE,MAAM,KAAK,OAAO;AAAA,IACxC;AAAA,EACJ;AAAA;AAAA,EAGA,OAAgC;AAC5B,WAAO,KAAK,OAAO,MAAM;AAAA,EAC7B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,QAAuB;AACzB,QAAI,KAAK,OAAO,WAAW,EAAG;AAC9B,UAAM,UAAU,KAAK;AACrB,SAAK,SAAS,CAAC;AACf,QAAI;AACA,YAAM,OAAO,KAAK,UAAU,gBAAgB,SAAS,KAAK,QAAQ,CAAC;AACnE,YAAM,MAAM,MAAM,KAAK,UAAU,KAAK,UAAU;AAAA,QAC5C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,GAAG,KAAK,QAAQ;AAAA,QAC/D;AAAA,MACJ,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACT,aAAK,QAAQ,IAAI,MAAM,6BAA6B,IAAI,MAAM,EAAE,CAAC;AAAA,MACrE;AAAA,IACJ,SAAS,KAAK;AACV,WAAK,QAAQ,GAAG;AAAA,IACpB;AAAA,EACJ;AACJ;AAEA,SAAS,kBAAkB,KAAqB;AAC5C,QAAM,UAAU,IAAI,QAAQ,QAAQ,EAAE;AACtC,MAAI,iBAAiB,KAAK,OAAO,EAAG,QAAO;AAC3C,SAAO,UAAU;AACrB;AAEA,SAAS,UAAiB;AACtB,QAAM,IAAI,MAAM,wEAAwE;AAC5F;AAcA,SAAS,gBAAgB,SAAyB,UAA2C;AACzF,QAAM,SAAS,oBAAI,IAAoE;AACvF,aAAW,KAAK,SAAS;AACrB,UAAM,WAAW,OAAO,IAAI,EAAE,IAAI;AAClC,QAAI,SAAU,UAAS,OAAO,KAAK,CAAC;AAAA,QAC/B,QAAO,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC,CAAC,EAAE,CAAC;AAAA,EACzD;AAEA,QAAM,UAAU,MAAM,KAAK,OAAO,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,OAAO,CAAC,MAAM;AAC3E,QAAI,SAAS,WAAW;AACpB,aAAO;AAAA,QACH;AAAA,QACA,KAAK;AAAA,UACD,YAAY,OAAO,IAAI,OAAK,cAAc,CAAC,CAAC;AAAA,UAC5C,wBAAwB;AAAA,UACxB,aAAa;AAAA,QACjB;AAAA,MACJ;AAAA,IACJ;AACA,QAAI,SAAS,SAAS;AAClB,aAAO,EAAE,MAAM,OAAO,EAAE,YAAY,OAAO,IAAI,OAAK,cAAc,CAAC,CAAC,EAAE,EAAE;AAAA,IAC5E;AAGA,WAAO;AAAA,MACH;AAAA,MACA,WAAW;AAAA,QACP,wBAAwB;AAAA,QACxB,YAAY,OAAO,IAAI,QAAM;AAAA,UACzB,YAAY,aAAa,EAAE,MAAM;AAAA,UACjC,cAAc,OAAO,EAAE,EAAE,IAAI;AAAA,UAC7B,mBAAmB,OAAO,EAAE,EAAE,IAAI;AAAA,UAClC,OAAO;AAAA,UACP,KAAK,EAAE;AAAA,UACP,cAAc,CAAC,KAAK,GAAG;AAAA,UACvB,gBAAgB,CAAC,EAAE,KAAK;AAAA,QAC5B,EAAE;AAAA,MACN;AAAA,IACJ;AAAA,EACJ,CAAC;AAED,SAAO;AAAA,IACH,iBAAiB,CAAC;AAAA,MACd,UAAU,EAAE,YAAY,aAAa,QAAQ,EAAE;AAAA,MAC/C,cAAc,CAAC;AAAA,QACX,OAAO,EAAE,MAAM,8BAA8B,SAAS,QAAQ;AAAA,QAC9D;AAAA,MACJ,CAAC;AAAA,IACL,CAAC;AAAA,EACL;AACJ;AAEA,SAAS,cAAc,GAA0B;AAC7C,SAAO;AAAA,IACH,YAAY,aAAa,EAAE,MAAM;AAAA,IACjC,cAAc,OAAO,EAAE,EAAE,IAAI;AAAA,IAC7B,mBAAmB,OAAO,EAAE,EAAE,IAAI;AAAA,IAClC,UAAU,EAAE;AAAA,EAChB;AACJ;AAEA,SAAS,aAAa,QAA2C;AAC7D,SAAO,OAAO,QAAQ,MAAM,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,OAAO;AAAA,IACjD;AAAA,IACA,OAAO,EAAE,aAAa,MAAM;AAAA,EAChC,EAAE;AACN;;;ACrUO,IAAM,oBAAN,MAAiD;AAAA,EACpD,mBAAyB;AAAA,EAAE;AAC/B;AAGO,IAAM,wBAAN,MAAqD;AAAA,EAArD;AACH,SAAS,WAA4B,CAAC;AAAA;AAAA,EAEtC,iBAAiB,OAAgB,UAAmC,CAAC,GAAS;AAC1E,SAAK,SAAS,KAAK,EAAE,OAAO,SAAS,IAAI,KAAK,IAAI,EAAE,CAAC;AAAA,EACzD;AAAA,EAEA,QAAc;AACV,SAAK,SAAS,SAAS;AAAA,EAC3B;AACJ;AAUO,IAAM,uBAAN,MAAoD;AAAA,EACvD,YAA6B,OAA0C,CAAC,GAAG;AAA9C;AAAA,EAAgD;AAAA,EAE7E,iBAAiB,OAAgB,UAAmC,CAAC,GAAS;AAC1E,QAAI;AACA,YAAM,OAAO,KAAK,KAAK,SAAS,CAAC,MAAM;AAAE,gBAAQ,MAAM,CAAC;AAAA,MAAG;AAC3D,YAAM,SAAkC;AAAA,QACpC,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,QAC3B,OAAO;AAAA,QACP,KAAK,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC1D;AAAA,MACJ;AACA,UAAI,iBAAiB,SAAS,MAAM,OAAO;AACvC,eAAO,QAAQ,MAAM;AAAA,MACzB;AACA,WAAK,KAAK,UAAU,MAAM,CAAC;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;;;AC7CO,IAAM,aAAa,CAAC,SAAS,QAAQ,QAAQ,SAAS,OAAO;AAGpE,IAAM,iBAA2C;AAAA,EAC7C,OAAO;AAAA,EACP,MAAM;AAAA,EACN,MAAM;AAAA,EACN,OAAO;AAAA,EACP,OAAO;AACX;AAGO,IAAM,aAAN,MAAmC;AAAA,EACtC,QAAc;AAAA,EAAE;AAAA,EAChB,OAAa;AAAA,EAAE;AAAA,EACf,OAAa;AAAA,EAAE;AAAA,EACf,QAAc;AAAA,EAAE;AAAA,EAChB,QAAc;AAAA,EAAE;AAAA,EAChB,QAAgB;AAAE,WAAO;AAAA,EAAM;AACnC;AAQO,IAAM,gBAAN,MAAM,eAAgC;AAAA,EACzC,YACqB,OAIb,CAAC,GACP;AALmB;AAAA,EAKjB;AAAA,EAEJ,IAAY,YAAoB;AAC5B,WAAO,eAAe,KAAK,KAAK,SAAS,MAAM;AAAA,EACnD;AAAA,EACA,IAAY,OAAO;AACf,WAAO,KAAK,KAAK,QAAQ,EAAE,KAAK,CAAC,MAAc,QAAQ,IAAI,CAAC,GAAG,OAAO,CAAC,MAAc,QAAQ,MAAM,CAAC,EAAE;AAAA,EAC1G;AAAA,EACA,IAAY,UAAmC;AAC3C,WAAO,KAAK,KAAK,WAAW,CAAC;AAAA,EACjC;AAAA,EAEA,MAAM,SAAiB,MAAsC;AAAE,SAAK,KAAK,SAAS,SAAS,IAAI;AAAA,EAAG;AAAA,EAClG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAA0C;AAC5C,WAAO,IAAI,eAAc,EAAE,GAAG,KAAK,MAAM,SAAS,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,EAAE,CAAC;AAAA,EACvF;AAAA,EAEQ,KAAK,OAAiB,KAAa,MAAsC;AAC7E,QAAI,eAAe,KAAK,IAAI,KAAK,UAAW;AAC5C,QAAI;AACA,YAAM,SAAS,EAAE,GAAG,KAAK,SAAS,GAAI,QAAQ,CAAC,EAAG;AAClD,YAAM,OAAO,OAAO,KAAK,MAAM,EAAE,SAAS,MAAM,KAAK,UAAU,MAAM,IAAI;AACzE,YAAM,OAAO,IAAI,KAAK,KAAK,GAAG,GAAG,IAAI;AACrC,UAAI,UAAU,WAAW,UAAU,QAAS,MAAK,KAAK,MAAM,IAAI;AAAA,UAC3D,MAAK,KAAK,IAAI,IAAI;AAAA,IAC3B,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;AAYO,IAAM,aAAN,MAAM,YAA6B;AAAA,EACtC,YACqB,OAQb,CAAC,GACP;AATmB;AAAA,EASjB;AAAA,EAEJ,IAAY,YAAoB;AAAE,WAAO,eAAe,KAAK,KAAK,SAAS,MAAM;AAAA,EAAG;AAAA,EACpF,IAAY,OAAO;AACf,WAAO,KAAK,KAAK,QAAQ,EAAE,KAAK,CAAC,MAAc,QAAQ,IAAI,CAAC,GAAG,OAAO,CAAC,MAAc,QAAQ,MAAM,CAAC,EAAE;AAAA,EAC1G;AAAA,EACA,IAAY,UAAmC;AAAE,WAAO,KAAK,KAAK,WAAW,CAAC;AAAA,EAAG;AAAA,EACjF,IAAY,OAAgC;AAAE,WAAO,KAAK,KAAK,QAAQ,CAAC;AAAA,EAAG;AAAA,EAC3E,IAAY,MAAkB;AAAE,WAAO,KAAK,KAAK,QAAQ,MAAM,oBAAI,KAAK;AAAA,EAAI;AAAA,EAE5E,MAAM,SAAiB,MAAsC;AAAE,SAAK,KAAK,SAAS,SAAS,IAAI;AAAA,EAAG;AAAA,EAClG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,KAAK,SAAiB,MAAsC;AAAE,SAAK,KAAK,QAAQ,SAAS,IAAI;AAAA,EAAG;AAAA,EAChG,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAAiB,OAAe,MAAsC;AACxE,SAAK,KAAK,SAAS,SAAS,EAAE,GAAI,QAAQ,CAAC,GAAI,GAAI,QAAQ,EAAE,OAAO,MAAM,SAAS,OAAO,MAAM,MAAM,IAAI,CAAC,EAAG,CAAC;AAAA,EACnH;AAAA,EACA,MAAM,SAA0C;AAC5C,WAAO,IAAI,YAAW,EAAE,GAAG,KAAK,MAAM,SAAS,EAAE,GAAG,KAAK,SAAS,GAAG,QAAQ,EAAE,CAAC;AAAA,EACpF;AAAA,EAEQ,KAAK,OAAiB,KAAa,MAAsC;AAC7E,QAAI,eAAe,KAAK,IAAI,KAAK,UAAW;AAC5C,QAAI;AACA,YAAM,SAAS;AAAA,QACX,IAAI,KAAK,IAAI,EAAE,YAAY;AAAA,QAC3B;AAAA,QACA;AAAA,QACA,GAAG,KAAK;AAAA,QACR,GAAG,KAAK;AAAA,QACR,GAAI,QAAQ,CAAC;AAAA,MACjB;AACA,YAAM,OAAO,KAAK,UAAU,MAAM;AAClC,UAAI,UAAU,WAAW,UAAU,QAAS,MAAK,KAAK,MAAM,IAAI;AAAA,UAC3D,MAAK,KAAK,IAAI,IAAI;AAAA,IAC3B,QAAQ;AAAA,IAER;AAAA,EACJ;AACJ;;;AC5GA,SAAS,yBAAyB;AAoC3B,SAAS,UAAkB;AAC9B,MAAI;AACA,WAAO,YAAY,IAAI;AAAA,EAC3B,QAAQ;AACJ,WAAO,KAAK,IAAI;AAAA,EACpB;AACJ;AAGA,IAAM,cAAc;AAEpB,IAAM,aAAa;AAUnB,SAAS,aAAa,MAAsB;AACxC,QAAM,YAAY,OAAO,IAAI,EAAE,QAAQ,aAAa,GAAG;AACvD,MAAI,QAAQ;AACZ,MAAI,MAAM,UAAU;AACpB,SAAO,QAAQ,OAAO,UAAU,WAAW,KAAK,MAAM,WAAY;AAClE,SAAO,MAAM,SAAS,UAAU,WAAW,MAAM,CAAC,MAAM,WAAY;AACpE,SAAO,UAAU,MAAM,OAAO,GAAG;AACrC;AAOA,SAAS,aAAa,MAAsB;AACxC,MAAI,MAAM;AACV,aAAW,MAAM,OAAO,IAAI,GAAG;AAC3B,UAAM,OAAO,GAAG,YAAY,CAAC;AAE7B,QAAI,QAAQ,MAAQ,OAAO,OAAQ,OAAO,OAAO,OAAO,MAAM;AAC1D,aAAO;AAAA,IACX,OAAO;AACH,aAAO;AAAA,IACX;AAAA,EACJ;AACA,SAAO,IAAI,QAAQ,OAAO,GAAG,EAAE,KAAK;AACxC;AAGA,SAAS,OAAO,KAAqB;AACjC,MAAI,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AAClC,SAAO,OAAO,KAAK,MAAM,MAAM,GAAG,IAAI,GAAG;AAC7C;AAOO,SAAS,mBAAmB,OAA4C;AAC3E,QAAM,QAAkB,CAAC;AACzB,aAAW,KAAK,OAAO;AACnB,UAAM,OAAO,aAAa,EAAE,IAAI;AAChC,QAAI,CAAC,KAAM;AACX,QAAI,OAAO,GAAG,IAAI,QAAQ,OAAO,EAAE,GAAG,CAAC;AACvC,QAAI,EAAE,MAAM;AACR,YAAM,OAAO,aAAa,EAAE,IAAI;AAChC,UAAI,KAAM,SAAQ,UAAU,IAAI;AAAA,IACpC;AACA,UAAM,KAAK,IAAI;AAAA,EACnB;AACA,SAAO,MAAM,KAAK,IAAI;AAC1B;AAOO,IAAM,cAAN,MAAM,YAAW;AAAA,EAAjB;AACH,SAAiB,SAA6B,CAAC;AAa/C,SAAQ,YAAY;AAAA;AAAA;AAAA,EAUpB,OAAO,MAAc,KAAa,MAAqB;AACnD,SAAK,OAAO,KAAK,EAAE,MAAM,KAAK,KAAK,CAAC;AAAA,EACxC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,eAAqB;AACjB,SAAK,YAAY;AAAA,EACrB;AAAA;AAAA,EAGA,IAAI,gBAAyB;AACzB,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,aAAa,UAAkB,OAAe,KAAmB;AAC7D,QAAI,CAAC,KAAK,UAAW;AACrB,UAAM,SAAU,KAAK,YAAL,KAAK,UAAY,oBAAI,IAAI;AACzC,QAAI,OAAO,OAAO,IAAI,QAAQ;AAC9B,QAAI,CAAC,MAAM;AACP,aAAO,CAAC;AACR,aAAO,IAAI,UAAU,IAAI;AAAA,IAC7B;AACA,QAAI,KAAK,UAAU,YAAW,WAAY;AAC1C,SAAK,KAAK,EAAE,OAAO,OAAO,KAAK,GAAG,KAAK,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM,EAAE,CAAC;AAAA,EACtF;AAAA;AAAA,EAGA,QAAQ,UAAiD;AACrD,WAAO,KAAK,SAAS,IAAI,QAAQ,KAAK,CAAC;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,MAAc,MAA2B;AAC3C,UAAM,KAAK,QAAQ;AACnB,QAAI,OAAO;AACX,WAAO,MAAM;AACT,UAAI,KAAM;AACV,aAAO;AACP,WAAK,OAAO,MAAM,QAAQ,IAAI,IAAI,IAAI;AAAA,IAC1C;AAAA,EACJ;AAAA;AAAA,EAGA,MAAM,QAAW,MAAc,IAA0B,MAA2B;AAChF,UAAM,MAAM,KAAK,MAAM,MAAM,IAAI;AACjC,QAAI;AACA,aAAO,MAAM,GAAG;AAAA,IACpB,UAAE;AACE,UAAI;AAAA,IACR;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAkBA,MAAM,MAAc,KAAa,MAAqB;AAClD,UAAM,MAAM,OAAO,SAAS,GAAG,KAAK,MAAM,IAAI,MAAM;AACpD,UAAM,aAAc,KAAK,gBAAL,KAAK,cAAgB,oBAAI,IAAI;AACjD,QAAI,QAAQ,WAAW,IAAI,IAAI;AAC/B,QAAI,CAAC,OAAO;AACR,YAAM,OAAyB,EAAE,MAAM,KAAK,EAAE;AAC9C,cAAQ,EAAE,MAAM,OAAO,GAAG,KAAK;AAC/B,iBAAW,IAAI,MAAM,KAAK;AAC1B,WAAK,OAAO,KAAK,IAAI;AAAA,IACzB;AACA,UAAM,SAAS;AACf,UAAM,KAAK,OAAO;AAClB,QAAI,KAAM,OAAM,OAAO;AACvB,UAAM,KAAK,OAAO,MAAM,OAAO,GAAG,MAAM,KAAK,IAAI,MAAM,IAAI,KAAK,OAAO,MAAM,KAAK;AAAA,EACtF;AAAA;AAAA,EAGA,QAAqC;AACjC,WAAO,KAAK;AAAA,EAChB;AAAA;AAAA,EAGA,WAAmB;AACf,WAAO,mBAAmB,KAAK,MAAM;AAAA,EACzC;AACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AApIa,YAqBe,aAAa;AArBlC,IAAM,aAAN;AAuJP,IAAM,YAAY,uBAAO,IAAI,8CAA8C;AAC3E,IAAM,cAAc;AACpB,IAAM,QACF,YAAY,SAAS,MAAM,YAAY,SAAS,IAAI,IAAI,kBAA8B;AAGnF,SAAS,kBAAqB,QAAoB,IAAgB;AACrE,SAAO,MAAM,IAAI,QAAQ,EAAE;AAC/B;AAGO,SAAS,oBAA4C;AACxD,SAAO,MAAM,SAAS;AAC1B;AAGO,SAAS,mBAAmB,MAAc,KAAa,MAAqB;AAC/E,QAAM,SAAS,GAAG,OAAO,MAAM,KAAK,IAAI;AAC5C;AAOO,SAAS,kBAAkB,MAAc,MAA2B;AACvE,QAAM,IAAI,MAAM,SAAS;AACzB,MAAI,CAAC,EAAG,QAAO,MAAM;AAAA,EAAC;AACtB,SAAO,EAAE,MAAM,MAAM,IAAI;AAC7B;AAMA,eAAsB,oBAClB,MACA,IACA,MACU;AACV,QAAM,IAAI,MAAM,SAAS;AACzB,MAAI,CAAC,EAAG,QAAO,GAAG;AAClB,SAAO,EAAE,QAAQ,MAAM,IAAI,IAAI;AACnC;AAQO,SAAS,kBAAkB,MAAc,KAAa,MAAqB;AAC9E,QAAM,SAAS,GAAG,MAAM,MAAM,KAAK,IAAI;AAC3C;AAQO,SAAS,yBAAyB,UAAkB,OAAe,KAAmB;AACzF,QAAM,SAAS,GAAG,aAAa,UAAU,OAAO,GAAG;AACvD;AA8CA,IAAM,WAAW,uBAAO,IAAI,iDAAiD;AAC7E,IAAM,aAAa;AACnB,IAAM,YACF,WAAW,QAAQ,MAAM,WAAW,QAAQ,IAAI,IAAI,kBAAsC;AAOvF,SAAS,sBAAyB,MAA0B,IAAgB;AAC/E,SAAO,UAAU,IAAI,MAAM,EAAE;AACjC;AASO,SAAS,sBAA4B;AACxC,QAAM,IAAI,UAAU,SAAS;AAC7B,MAAI,GAAG;AACH,MAAE,UAAU;AACZ,MAAE,aAAa;AAAA,EACnB;AACJ;AAGO,SAAS,0BAAmC;AAC/C,SAAO,UAAU,SAAS,GAAG,WAAW;AAC5C;AAOO,SAAS,6BAAsC;AAClD,SAAO,UAAU,SAAS,GAAG,cAAc;AAC/C;AAsBO,SAAS,0BAA0B,IAA2C;AACjF,MAAI,CAAC,GAAI,QAAO;AAChB,MAAI,GAAG,aAAa,KAAM,QAAO;AACjC,MAAI,GAAG,kBAAkB,aAAa,GAAG,kBAAkB,SAAU,QAAO;AAC5E,SAAO,GAAG,YAAY,oBAAoB,GAAG,YAAY;AAC7D;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/observability",
3
- "version": "16.0.0-rc.1",
3
+ "version": "16.1.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": "16.0.0-rc.1"
17
+ "@objectstack/spec": "16.1.0"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^26.1.1",
@@ -33,11 +33,11 @@
33
33
  "author": "ObjectStack",
34
34
  "repository": {
35
35
  "type": "git",
36
- "url": "https://github.com/objectstack-ai/framework.git",
36
+ "url": "https://github.com/objectstack-ai/objectstack.git",
37
37
  "directory": "packages/observability"
38
38
  },
39
39
  "homepage": "https://objectstack.ai/docs",
40
- "bugs": "https://github.com/objectstack-ai/framework/issues",
40
+ "bugs": "https://github.com/objectstack-ai/objectstack/issues",
41
41
  "publishConfig": {
42
42
  "access": "public"
43
43
  },