@lunora/observability 1.0.0-alpha.4 → 1.0.0-alpha.5

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.d.mts CHANGED
@@ -488,6 +488,15 @@ type HostTracingResolver = () => HostTracingLike | Promise<HostTracingLike | und
488
488
  interface TracerDeps {
489
489
  /** The trace this ctx's spans belong to. */
490
490
  anchor: TraceAnchor;
491
+ /**
492
+ * Whether to record a span body's error message (and a `recordException`
493
+ * stacktrace) verbatim rather than redacted. Mirrors the request log's
494
+ * `captureRaw`: `true` in dev (`isDevEnvironment`), `false` in production —
495
+ * the span pipeline is the one sink third-party collectors (Datadog/Axiom
496
+ * via `otlpSink`) receive, so it must not ship raw PII/internals by default
497
+ * the way the request log and function-metrics sinks already don't.
498
+ */
499
+ captureRaw?: boolean;
491
500
  /** Function path the spans are attributed to. */
492
501
  functionPath: string;
493
502
  /**
@@ -544,11 +553,17 @@ interface SpanCollector {
544
553
  * attributes become the canonical one-event-per-request summary. Sharing the
545
554
  * implementation is what makes those two feel like the same API instead of two
546
555
  * that happen to resemble each other.
556
+ *
557
+ * `captureRaw` (default `false`) gates `recordException`'s `exception.message`/
558
+ * `exception.stacktrace` the same way {@link createTracer} gates a span's own
559
+ * error message — a stack is file paths and internals by definition, the exact
560
+ * class `isInternalCode` redaction exists for, so it rides the same dev-only
561
+ * escape hatch rather than shipping to a third-party collector by default.
547
562
  */
548
563
  declare const createSpanCollector: (ids: {
549
564
  spanId: string;
550
565
  traceId: string;
551
- }) => SpanCollector;
566
+ }, captureRaw?: boolean) => SpanCollector;
552
567
  /**
553
568
  * Build the `ctx.trace` span factory for one dispatched function.
554
569
  *
@@ -673,6 +688,14 @@ declare const createMetrics: (deps: MetricsDeps) => ContextMetrics;
673
688
  */
674
689
  declare const dispatchRootSpan: (input: {
675
690
  anchor: TraceAnchor;
691
+ /**
692
+ * Whether to record the failure message verbatim rather than redacted —
693
+ * the same dev-only escape hatch as {@link TracerDeps.captureRaw}. This
694
+ * synthetic root span carries the SAME error message the request log and
695
+ * function-metrics sinks already redact by default, so it must not be the
696
+ * one durable copy that ships it raw to a third-party collector.
697
+ */
698
+ captureRaw?: boolean;
676
699
  /**
677
700
  * What the handler attached to the dispatch through `ctx.span` — the **wide
678
701
  * event**. These are the attributes that would otherwise have been scattered
@@ -715,6 +738,14 @@ type DatabaseInstrumentation = "off" | "spans" | "summary";
715
738
  interface DatabaseTelemetryDeps {
716
739
  /** The trace produced spans belong to (`"spans"` mode only). */
717
740
  anchor: TraceAnchor;
741
+ /**
742
+ * Whether to record a failed call's error message verbatim rather than
743
+ * redacted (`"spans"` mode only) — the same dev-only escape hatch as
744
+ * `TracerDeps.captureRaw`. A constraint-error message quotes the
745
+ * conflicting row, so this CLIENT span gets the same default-redacted
746
+ * posture as the request log and function-metrics sinks.
747
+ */
748
+ captureRaw?: boolean;
718
749
  /** Function path spans and attributes are attributed to. */
719
750
  functionPath: string;
720
751
  /** Detail level; see {@link DatabaseInstrumentation}. */
@@ -786,13 +817,23 @@ declare const FUNCTION_METRICS_BUCKET_RETENTION = 1440;
786
817
  /**
787
818
  * Maximum distinct function `path`s tracked in the accumulator table. Mirrors
788
819
  * `query-metrics.ts`'s `QUERY_METRICS_MAX_STATEMENTS` cap (and exists for the
789
- * same reason): the `path` is attacker-reachable an unregistered/`FUNCTION_NOT_FOUND`
790
- * dispatch still records a row keyed by the caller-supplied `functionPath` so
791
- * without a cap a flood of distinct random paths would grow `__lunora_metrics`
792
- * (and its bucket/scan satellites) without bound, eventually filling the shard's
793
- * SQLite store shared with the app's real data. A few thousand registered
794
- * functions is already far beyond any real app, so a new path past this cap is
795
- * dropped while already-tracked paths keep accumulating.
820
+ * same reason): the real bound is the app's own registered-function set plus
821
+ * deploy churn (a rename/removal leaves its old path's row in place, still
822
+ * counted against the cap, until an operator's own retention/cleanup
823
+ * process there is none built in today) a few thousand registered
824
+ * functions is already far beyond any real app. `shard-do.ts`'s dispatch
825
+ * handler explicitly does NOT record per-function metrics for an
826
+ * unregistered/`FUNCTION_NOT_FOUND` dispatch (see the guard next to its
827
+ * `FUNCTION_NOT_FOUND` check), so a caller cannot mint arbitrary `path`s here
828
+ * the way a raw caller-supplied SQL shape can in `query-metrics.ts`. Without a
829
+ * cap, deploy churn across the app's lifetime would still grow
830
+ * `__lunora_metrics` (and its bucket/scan satellites) without bound,
831
+ * eventually filling the shard's SQLite store shared with the app's real
832
+ * data. At the cap, a brand-new path is refused — protecting the incumbent
833
+ * leaderboard from a flood of one-off paths is the point, so admission is
834
+ * refused rather than evicting an existing path to make room; already-tracked
835
+ * paths keep accumulating past the cap. `readFunctionMetricsTotals`'s
836
+ * `capped` is the read-side signal for this.
796
837
  */
797
838
  declare const FUNCTION_METRICS_MAX_PATHS = 5e3;
798
839
  /**
@@ -814,6 +855,22 @@ interface FunctionMetricBucket {
814
855
  /** Subset of `calls` that threw. */
815
856
  errors: number;
816
857
  }
858
+ /** {@link readFunctionMetricBuckets} result: the time-series window plus whether the read limit cut it short. */
859
+ interface FunctionMetricBucketsResult {
860
+ buckets: (FunctionMetricBucket & {
861
+ path: string;
862
+ })[];
863
+ /**
864
+ * True when more rows existed than {@link FUNCTION_METRICS_READ_LIMIT} could
865
+ * return, so `buckets` is a partial (newest) window rather than the app's
866
+ * full retained history. Mirrors `readQueryInsights`'s `capped` and
867
+ * `foldTraces`'s `total`: a silently truncated read looks identical to a
868
+ * complete one to a caller that doesn't check for it — the Metrics chart's
869
+ * window would appear to shrink as the app grows, with a wrong leftmost
870
+ * bar, and nothing would say why.
871
+ */
872
+ truncated: boolean;
873
+ }
817
874
  /** One declared index a dispatch exercised (used to narrow a read). */
818
875
  interface IndexHit {
819
876
  /** The declared index name. */
@@ -879,7 +936,9 @@ interface RecordFunctionMetricInput {
879
936
  * than baked into the `CREATE` so a shard whose `__lunora_metrics` predates the
880
937
  * causal-attribution feature gains the column on the next call without a
881
938
  * migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
882
- * error from a re-run is swallowed.
939
+ * error from a re-run is swallowed. Both the `CREATE`s and the back-fill only
940
+ * run once per handle (see {@link ensuredHandles}) — a handle already marked
941
+ * ensured returns immediately.
883
942
  */
884
943
  declare const ensureFunctionMetricsTables: (sql: SqlExec) => void;
885
944
  /**
@@ -934,18 +993,21 @@ declare const readFunctionMetrics: (sql: SqlExec) => FunctionCallStat[];
934
993
  /**
935
994
  * Read the coarse time-series buckets for `path` (every path when omitted),
936
995
  * oldest-bucket first so a chart can plot them left-to-right. Creates the table
937
- * first so reads on a never-called shard return `[]`.
996
+ * first so reads on a never-called shard return `{ buckets: [], truncated: false }`.
938
997
  */
939
- declare const readFunctionMetricBuckets: (sql: SqlExec, path?: string) => (FunctionMetricBucket & {
940
- path: string;
941
- })[];
998
+ declare const readFunctionMetricBuckets: (sql: SqlExec, path?: string) => FunctionMetricBucketsResult;
942
999
  /**
943
1000
  * Aggregate the persisted accumulators into the lifetime totals the metrics
944
- * health snapshot reports: total calls (`requests`), total `errors`, and the
945
- * earliest `last_called_at` seen — a best-effort "since" marker for durable
946
- * data. Returns zeroes on a never-called shard.
1001
+ * health snapshot reports: total calls (`requests`), total `errors`. Returns
1002
+ * zeroes on a never-called shard.
1003
+ *
1004
+ * `capped` is true when the distinct-path cap ({@link FUNCTION_METRICS_MAX_PATHS})
1005
+ * has been reached — the write-side signal that a brand-new function path is
1006
+ * currently being refused (see `admitPath`), mirroring `readQueryInsights`'s
1007
+ * `capped` for query-metrics and `readMetricHistory`'s for metric-history.
947
1008
  */
948
1009
  declare const readFunctionMetricsTotals: (sql: SqlExec) => {
1010
+ capped: boolean;
949
1011
  errors: number;
950
1012
  requests: number;
951
1013
  };
@@ -1261,6 +1323,17 @@ interface MetricHistorySeries {
1261
1323
  }
1262
1324
  /** {@link readMetricHistory} result: every tracked series with its buckets. */
1263
1325
  interface MetricHistoryResult {
1326
+ /**
1327
+ * True when the distinct-series cap has been reached — a **write-side**
1328
+ * signal ("this shard can no longer admit a brand-new series", see
1329
+ * `admitNewSeries`), not a read-side truncation flag. It is computed over
1330
+ * the whole table regardless of `options.sinceMs`/the row-count read
1331
+ * limit, so it can be `true` even when every series `readMetricHistory`
1332
+ * actually returned fits comfortably: the caller should read it as "a
1333
+ * flood of new series would currently be refused", the same thing
1334
+ * `readQueryInsights`'s `capped` already signals for query-metrics.
1335
+ */
1336
+ capped: boolean;
1264
1337
  series: MetricHistorySeries[];
1265
1338
  }
1266
1339
  /**
@@ -1269,7 +1342,7 @@ interface MetricHistoryResult {
1269
1342
  * omitted field keeps the historical behaviour.
1270
1343
  */
1271
1344
  interface MetricHistoryOptions {
1272
- /** Distinct series tracked before a brand-new one is dropped (default {@link METRIC_HISTORY_MAX_SERIES}). */
1345
+ /** Distinct series tracked before the least-recently-updated one is evicted to admit a new one (default {@link METRIC_HISTORY_MAX_SERIES}). */
1273
1346
  maxSeries?: number;
1274
1347
  /** Minute-buckets kept per series before older rows are trimmed (default {@link METRIC_HISTORY_BUCKET_RETENTION}). */
1275
1348
  retentionBuckets?: number;
@@ -1306,9 +1379,12 @@ declare const recordMetricHistory: (sql: SqlExec, event: MetricEvent, exemplarTr
1306
1379
  * trend line reads oldest→newest.
1307
1380
  *
1308
1381
  * `options.sinceMs`, when set, returns only buckets at or after this epoch-ms —
1309
- * the studio's time-window selector.
1382
+ * the studio's time-window selector. `options.maxSeries`, mirroring the write
1383
+ * side's tunable, is only used to compute `capped` — it does not affect which
1384
+ * rows are read.
1310
1385
  */
1311
1386
  declare const readMetricHistory: (sql: SqlExec, options?: {
1387
+ maxSeries?: number;
1312
1388
  sinceMs?: number;
1313
1389
  }) => MetricHistoryResult;
1314
1390
  /** One row of the `__lunora_metrics_queries` table, as returned by `readQueryMetrics`. */
@@ -1369,14 +1445,23 @@ interface QueryInsightsResult {
1369
1445
  */
1370
1446
  declare const readQueryInsights: (sql: SqlExec, rangeMs: number, now?: number) => QueryInsightsResult;
1371
1447
  /**
1372
- * Record one statement execution. Creates the table on first call. Silently
1448
+ * Record one statement's activity. Creates the table on first call. Silently
1373
1449
  * skips recording when the normalised statement is empty (shouldn't happen
1374
1450
  * in practice) or when the table is already at the
1375
1451
  * {@link QUERY_METRICS_MAX_STATEMENTS} cap and the statement is not yet
1376
- * tracked. The cap check is a single cheap `COUNT(*)` on the primary-key
1377
- * index, so the hot-path cost is minimal.
1452
+ * tracked. See `admitStatement` for how the cap check avoids an unconditional
1453
+ * `COUNT(*)` on every execution.
1454
+ *
1455
+ * `execCount` (default 1) lets a caller fold several executions of the SAME
1456
+ * statement into one call — `shard-do.ts` does this per dispatch so a
1457
+ * query-in-a-loop handler pays one upsert here instead of one per raw
1458
+ * execution. `durationMs`/`rowsRead`/`rowsWritten` are then the SUM across
1459
+ * `execCount` executions, exactly as `exec_count`/`total_duration_ms` already
1460
+ * accumulate sums across separate calls — folding before calling is
1461
+ * indistinguishable, from this table's point of view, from `execCount`
1462
+ * separate calls with the same totals.
1378
1463
  */
1379
- declare const recordQueryMetric: (sql: SqlExec, rawSql: string, durationMs: number, rowsRead: number, rowsWritten: number, now?: number) => void;
1464
+ declare const recordQueryMetric: (sql: SqlExec, rawSql: string, durationMs: number, rowsRead: number, rowsWritten: number, now?: number, execCount?: number) => void;
1380
1465
  /**
1381
1466
  * Read all tracked statement aggregates, ordered by `total_duration_ms DESC`
1382
1467
  * (the leaderboard's default). Creates the table first so a read on a
@@ -1393,7 +1478,7 @@ interface RequestLogEntry {
1393
1478
  cacheHit?: boolean;
1394
1479
  /** Handler wall-clock duration in milliseconds (before the subscription write-flush, matching the per-function metrics). */
1395
1480
  durationMs: number;
1396
- /** Error message when `outcome === "error"`; absent on success. */
1481
+ /** Error message when `outcome === "error"`, redacted like args/identity; absent on success. */
1397
1482
  errorMessage?: string;
1398
1483
  /** The `&lt;file>:&lt;function>` identifier dispatched, e.g. `messages:list`. */
1399
1484
  functionPath: string;
@@ -1477,11 +1562,16 @@ interface ErrorIssue {
1477
1562
  culprit: string;
1478
1563
  /** Wall-clock millis of the oldest folded row. */
1479
1564
  firstSeen: number;
1480
- /** Stable 16-char grouping hash over `functionPath :: bucket(message)`. */
1565
+ /**
1566
+ * Stable 16-char grouping hash over `functionPath :: bucket(message)`,
1567
+ * computed from the RAW (pre-redaction) message at write time and stored on
1568
+ * the row — see {@link appendRequestLogEntry} — so redacting `sampleMessage`
1569
+ * below can't change the grouping.
1570
+ */
1481
1571
  hash: string;
1482
1572
  /** Wall-clock millis of the newest folded row. */
1483
1573
  lastSeen: number;
1484
- /** A representative raw error message — taken from the most recent folded row. */
1574
+ /** A representative error message (redacted, like the durable row) — taken from the most recent folded row. */
1485
1575
  sampleMessage: string;
1486
1576
  /** Developer-tagged severity from the persisted triage state; absent when untriaged. */
1487
1577
  severity?: IssueSeverity;
@@ -1518,21 +1608,73 @@ interface ReadIssuesOptions {
1518
1608
  /** Exact acting-userId match. */
1519
1609
  userId?: string;
1520
1610
  }
1611
+ /**
1612
+ * Redact the secrets / PII out of a value before it reaches the durable log or a
1613
+ * Logpush event, via `@visulima/redact`'s `standardRules`. Unlike a blunt
1614
+ * type-tag stamp this masks sensitive values by PATTERN (not just by key name)
1615
+ * while leaving benign values readable, so the studio's args/identity columns
1616
+ * stay useful. `null` / `undefined` pass through unchanged.
1617
+ *
1618
+ * What `standardRules` actually catches differs by shape, verified against its
1619
+ * real behavior rather than assumed from its name: on a KEYED object (`args`,
1620
+ * `identity`) it also matches by key name, so `{ password: "hunter2" }` and
1621
+ * `{ token: "…" }` ARE masked regardless of the value's shape. On a PLAIN
1622
+ * STRING — which is what `errorMessage`/log `fields`-as-rendered-text are —
1623
+ * only pattern-shaped matches apply: emails, long digit runs / structured
1624
+ * numeric IDs (credit-card, phone, SSN, AWS-access-key-style), and an explicit
1625
+ * `Bearer &lt;token>` / `token=…`-shaped substring. A free-text `password=hunter2`
1626
+ * or a bare provider API key embedded in prose (e.g. `sk-live-…`) is NOT
1627
+ * caught on a plain string — there is no key to match against, and neither is
1628
+ * a recognized value pattern. So this is a PII-pattern net for rendered text,
1629
+ * not a general secrets scrubber; a handler that echoes a raw credential into
1630
+ * an error message or a log string can still leak it through here. Works on a
1631
+ * plain string too (`redact` traverses whatever value it's handed), which is
1632
+ * how {@link appendRequestLogEntry} and {@link emitRequestLogEvent} reuse this
1633
+ * for `errorMessage` — a validation error echoes the offending value, a
1634
+ * constraint error quotes the conflicting row, so the error message is at
1635
+ * least as PII-dense as args and gets the same treatment (with the free-text
1636
+ * caveat above).
1637
+ *
1638
+ * `captureRaw` is the development escape hatch: in a dev environment the dispatch
1639
+ * site (`isDevEnvironment`) passes `true` to skip redaction so a developer can
1640
+ * see real arg/identity/error values; production always redacts. The dev
1641
+ * decision is made at the call site from the deployment env, never inferred
1642
+ * here — so a real deploy that omits the env var stays redacted.
1643
+ */
1644
+ declare const redactArgs: (value: unknown, captureRaw?: boolean) => unknown;
1521
1645
  /**
1522
1646
  * Create the `__lunora_reqlog__` table. `seq` is an `AUTOINCREMENT` primary
1523
1647
  * key, giving each shard a monotonic cursor the Logs tab pages through; the
1524
1648
  * `args`/`identity`/`tables_read`/`tables_written` columns hold JSON and are
1525
1649
  * `NULL`/empty when none was recorded. Idempotent, so read and write paths can
1526
1650
  * call it defensively.
1651
+ *
1652
+ * `error_fingerprint` is the {@link fingerprintError} grouping hash captured
1653
+ * from the RAW `error_message` at write time, before {@link appendRequestLogEntry}
1654
+ * redacts it — see that function's docstring. It is added via a guarded
1655
+ * `ALTER TABLE` rather than baked into the `CREATE`, mirroring
1656
+ * `function-metrics.ts`'s `ensureFunctionMetricsTables`, so a shard whose
1657
+ * `__lunora_reqlog__` predates this column gains it on the next call without a
1658
+ * migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
1659
+ * error from a re-run (or the freshly-created schema above) is swallowed.
1527
1660
  */
1528
1661
  declare const ensureRequestLogTable: (sql: SqlExec) => void;
1529
1662
  /**
1530
1663
  * Append one dispatch to the request log, then trim the log back to the most
1531
1664
  * recent `retention` rows (default {@link REQUEST_LOG_RETENTION}). Creates the
1532
- * table first so callers needn't. Args/identity are redacted here so a raw value
1533
- * never reaches the durable table — callers pass the unredacted entry and rely on
1534
- * this, unless `captureRaw` (dev only) is set. `retention` is the operator's
1535
- * `LUNORA_REQUEST_LOG_RETENTION` override, threaded in by the dispatch site.
1665
+ * table first so callers needn't. Args/identity/error message are redacted here
1666
+ * so a raw value never reaches the durable table — callers pass the unredacted
1667
+ * entry and rely on this, unless `captureRaw` (dev only) is set. `retention` is
1668
+ * the operator's `LUNORA_REQUEST_LOG_RETENTION` override, threaded in by the
1669
+ * dispatch site.
1670
+ *
1671
+ * The error-grouping fingerprint is computed from `entry.errorMessage` BEFORE
1672
+ * it's redacted below, and the resulting hash is persisted in
1673
+ * `error_fingerprint`. `readErrorIssues` groups off that stored hash instead of
1674
+ * recomputing `fingerprintError` from the (redacted) `error_message` column, so
1675
+ * masking a PII-bearing value — e.g. two different `&lt;n>`-bucketed IDs that
1676
+ * redact to two different tag lengths (`&lt;DL>` vs `&lt;BANKACC>`) — can't split an
1677
+ * existing Issue or change its identity.
1536
1678
  */
1537
1679
  declare const appendRequestLogEntry: (sql: SqlExec, entry: AppendRequestLogEntry, options?: RequestLogWriteOptions) => void;
1538
1680
  /**
@@ -1541,8 +1683,8 @@ declare const appendRequestLogEntry: (sql: SqlExec, entry: AppendRequestLogEntry
1541
1683
  * NOT reimplement a transport: it produces a richer, lunora-attributed event and
1542
1684
  * lets CF's existing trace-log pipe ship it. The event mirrors the durable
1543
1685
  * `__lunora_reqlog__` row (function path, shard, user, outcome, duration, tables
1544
- * read/written, cache hit), with `args` AND `identity` redacted exactly like the
1545
- * durable write so no raw PII/secret reaches the log pipeline.
1686
+ * read/written, cache hit), with `args`, `identity`, AND `error` redacted
1687
+ * exactly like the durable write so no raw PII/secret reaches the log pipeline.
1546
1688
  *
1547
1689
  * An `error` outcome goes to `console.error` (surfacing at error level in the
1548
1690
  * trace so a SIEM can alert on it); everything else to `console.log`. The
@@ -1591,11 +1733,19 @@ declare const parseLogArgs: (args: unknown[], boundFields?: LogFields) => {
1591
1733
  *
1592
1734
  * Structured `fields` (plus `traceId`/`spanId` for correlation) ARE emitted here
1593
1735
  * — they are intentional metadata a log pipeline filters on, unlike raw `args`.
1594
- * A field value that can't be serialised (a circular object) would make
1595
- * `JSON.stringify` throw and drop the whole line, so serialisation falls back to
1596
- * a fields-free line rather than losing the event.
1597
- */
1598
- declare const emitLogEvent: (input: LogEventInput) => void;
1736
+ * Unlike `args`, `fields` IS redacted before it rides this console line — a
1737
+ * developer can attach anything to a fields bag (`ctx.log.info("charged",
1738
+ * { email, cardLast4 })`), and this is the one line that's told to a SIEM as
1739
+ * trustworthy, exactly like the request-log `args`/`identity`/`error` columns.
1740
+ * `options.captureRaw` (dev only) skips it, mirroring every other redaction
1741
+ * point in this module; the sole current caller (`ShardDO.recordUserLog`)
1742
+ * doesn't yet thread a dev flag through, so `fields` redacts unconditionally
1743
+ * there today — a conservative default, never a correctness gap. A field value
1744
+ * that can't be serialised (a circular object) would make `JSON.stringify`
1745
+ * throw and drop the whole line, so serialisation falls back to a fields-free
1746
+ * line rather than losing the event.
1747
+ */
1748
+ declare const emitLogEvent: (input: LogEventInput, options?: RequestLogWriteOptions) => void;
1599
1749
  /**
1600
1750
  * Read request-log entries newest-first, AND-combining the supplied filters
1601
1751
  * (function-path prefix, exact userId/shardKey/outcome, and a table-touched
@@ -1815,4 +1965,4 @@ declare const resolveTraceAnchor: (traceparent: string | undefined) => {
1815
1965
  sampled: boolean;
1816
1966
  traceId: string;
1817
1967
  };
1818
- export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, type AiRunBinding, type AppendRequestLogEntry, type AuthMetrics, type AuthMetricsBucket, type ContextFetch, type ContextLogLevel, type ContextMetrics, type ContextTracer, DEFAULT_EXPLAIN_ISSUE_MODEL, type DatabaseInstrumentation, type DatabaseTally, type ExplainIssueArgs, type ExplainIssueDegradedReason, type ExplainIssueGrounding, type ExplainIssueResult, FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, type FunctionMetricBucket, type FunctionMetricIndexHit, type HostTracingLike, ISSUE_SEVERITIES, ISSUE_STATE_TABLE, ISSUE_STATUSES, type IndexHit, type IssueSeverity, type IssueState, type IssueStatePatch, type IssueStatus, type IssuesResult, LogBuffer, type LogEntry, type LogEventInput, type LogLevel, MIN_ADMIN_TOKEN_LENGTH, MIN_AUTH_SECRET_LENGTH, MetricBuffer, type MetricHistoryOptions, type MetricHistoryPoint, type MetricHistorySeries, type MetricSeries, type MetricsDeps, type QueryStatEntry, REQUEST_LOG_TABLE, type RecordAuthEventInput, type RecordFunctionMetricInput, type RequestLogResult, type RequestLogWriteOptions, type SecurityAuditResult, type SecurityFinding, type SecurityFindingKind, type SecurityFindingLevel, SpanBuffer, type SpanCollection, type SpanCollector, type SpanHandle, type TraceAnchor, type TraceSpan, type TraceSummary, type TracerDeps, appendRequestLogEntry, buildSecurityAudit, createDatabaseTally, createMetrics, createSpanCollector, createTracedFetch, createTracer, dispatchRootSpan, emitLogEvent, emitRequestLogEvent, ensureAuthMetricsTables, ensureFunctionMetricsTables, ensureRequestLogTable, explainIssue, findDanglingReferences, foldTraces, formatTally, instrumentDatabase, mergeScanAttribution, parseExplainIssueArgs, parseLogArgs, readAuthMetrics, readErrorIssues, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, readMetricHistory, readQueryInsights, readQueryMetrics, readRequestLog, recordAuthEvent, recordFunctionMetric, recordMetricHistory, recordQueryMetric, resolveTraceAnchor, upsertIssueState };
1968
+ export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, type AiRunBinding, type AppendRequestLogEntry, type AuthMetrics, type AuthMetricsBucket, type ContextFetch, type ContextLogLevel, type ContextMetrics, type ContextTracer, DEFAULT_EXPLAIN_ISSUE_MODEL, type DanglingReference, type DanglingReferenceResult, type DatabaseInstrumentation, type DatabaseTally, type DatabaseTelemetryDeps, type ErrorIssue, type ExplainIssueArgs, type ExplainIssueDegradedReason, type ExplainIssueGrounding, type ExplainIssueResult, FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, type FoldedTraces, type FunctionMetricBucket, type FunctionMetricBucketsResult, type FunctionMetricIndexHit, type HostSpanLike, type HostTracingLike, type HostTracingResolver, ISSUE_SEVERITIES, ISSUE_STATE_TABLE, ISSUE_STATUSES, type IndexHit, type IssueSeverity, type IssueState, type IssueStatePatch, type IssueStatus, type IssuesResult, LogBuffer, type LogEntry, type LogEventInput, type LogLevel, MIN_ADMIN_TOKEN_LENGTH, MIN_AUTH_SECRET_LENGTH, MetricBuffer, type MetricEvent, type MetricHistoryOptions, type MetricHistoryPoint, type MetricHistoryResult, type MetricHistorySeries, type MetricKind, type MetricSeries, type MetricsDeps, type QueryInsightBucket, type QueryInsightEntry, type QueryInsightsResult, type QueryStatEntry, REQUEST_LOG_TABLE, type ReadIssuesOptions, type ReadRequestLogOptions, type RecordAuthEventInput, type RecordFunctionMetricInput, type RequestLogEntry, type RequestLogResult, type RequestLogWriteOptions, type RequestOutcome, type SecurityAuditResult, type SecurityFinding, type SecurityFindingKind, type SecurityFindingLevel, SpanBuffer, type SpanCollection, type SpanCollector, type SpanEvent, type SpanEventPoint, type SpanHandle, type OtlpSpanKind as SpanKind, type SpanLink, type SpanOptions, type TraceAnchor, type TraceSpan, type TraceSummary, type TracedFetchDeps, type TracerDeps, appendRequestLogEntry, buildSecurityAudit, createDatabaseTally, createMetrics, createSpanCollector, createTracedFetch, createTracer, dispatchRootSpan, emitLogEvent, emitRequestLogEvent, ensureAuthMetricsTables, ensureFunctionMetricsTables, ensureRequestLogTable, explainIssue, findDanglingReferences, foldTraces, formatTally, instrumentDatabase, mergeScanAttribution, parseExplainIssueArgs, parseLogArgs, readAuthMetrics, readErrorIssues, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, readMetricHistory, readQueryInsights, readQueryMetrics, readRequestLog, recordAuthEvent, recordFunctionMetric, recordMetricHistory, recordQueryMetric, redactArgs, resolveTraceAnchor, upsertIssueState };
package/dist/index.d.ts CHANGED
@@ -488,6 +488,15 @@ type HostTracingResolver = () => HostTracingLike | Promise<HostTracingLike | und
488
488
  interface TracerDeps {
489
489
  /** The trace this ctx's spans belong to. */
490
490
  anchor: TraceAnchor;
491
+ /**
492
+ * Whether to record a span body's error message (and a `recordException`
493
+ * stacktrace) verbatim rather than redacted. Mirrors the request log's
494
+ * `captureRaw`: `true` in dev (`isDevEnvironment`), `false` in production —
495
+ * the span pipeline is the one sink third-party collectors (Datadog/Axiom
496
+ * via `otlpSink`) receive, so it must not ship raw PII/internals by default
497
+ * the way the request log and function-metrics sinks already don't.
498
+ */
499
+ captureRaw?: boolean;
491
500
  /** Function path the spans are attributed to. */
492
501
  functionPath: string;
493
502
  /**
@@ -544,11 +553,17 @@ interface SpanCollector {
544
553
  * attributes become the canonical one-event-per-request summary. Sharing the
545
554
  * implementation is what makes those two feel like the same API instead of two
546
555
  * that happen to resemble each other.
556
+ *
557
+ * `captureRaw` (default `false`) gates `recordException`'s `exception.message`/
558
+ * `exception.stacktrace` the same way {@link createTracer} gates a span's own
559
+ * error message — a stack is file paths and internals by definition, the exact
560
+ * class `isInternalCode` redaction exists for, so it rides the same dev-only
561
+ * escape hatch rather than shipping to a third-party collector by default.
547
562
  */
548
563
  declare const createSpanCollector: (ids: {
549
564
  spanId: string;
550
565
  traceId: string;
551
- }) => SpanCollector;
566
+ }, captureRaw?: boolean) => SpanCollector;
552
567
  /**
553
568
  * Build the `ctx.trace` span factory for one dispatched function.
554
569
  *
@@ -673,6 +688,14 @@ declare const createMetrics: (deps: MetricsDeps) => ContextMetrics;
673
688
  */
674
689
  declare const dispatchRootSpan: (input: {
675
690
  anchor: TraceAnchor;
691
+ /**
692
+ * Whether to record the failure message verbatim rather than redacted —
693
+ * the same dev-only escape hatch as {@link TracerDeps.captureRaw}. This
694
+ * synthetic root span carries the SAME error message the request log and
695
+ * function-metrics sinks already redact by default, so it must not be the
696
+ * one durable copy that ships it raw to a third-party collector.
697
+ */
698
+ captureRaw?: boolean;
676
699
  /**
677
700
  * What the handler attached to the dispatch through `ctx.span` — the **wide
678
701
  * event**. These are the attributes that would otherwise have been scattered
@@ -715,6 +738,14 @@ type DatabaseInstrumentation = "off" | "spans" | "summary";
715
738
  interface DatabaseTelemetryDeps {
716
739
  /** The trace produced spans belong to (`"spans"` mode only). */
717
740
  anchor: TraceAnchor;
741
+ /**
742
+ * Whether to record a failed call's error message verbatim rather than
743
+ * redacted (`"spans"` mode only) — the same dev-only escape hatch as
744
+ * `TracerDeps.captureRaw`. A constraint-error message quotes the
745
+ * conflicting row, so this CLIENT span gets the same default-redacted
746
+ * posture as the request log and function-metrics sinks.
747
+ */
748
+ captureRaw?: boolean;
718
749
  /** Function path spans and attributes are attributed to. */
719
750
  functionPath: string;
720
751
  /** Detail level; see {@link DatabaseInstrumentation}. */
@@ -786,13 +817,23 @@ declare const FUNCTION_METRICS_BUCKET_RETENTION = 1440;
786
817
  /**
787
818
  * Maximum distinct function `path`s tracked in the accumulator table. Mirrors
788
819
  * `query-metrics.ts`'s `QUERY_METRICS_MAX_STATEMENTS` cap (and exists for the
789
- * same reason): the `path` is attacker-reachable an unregistered/`FUNCTION_NOT_FOUND`
790
- * dispatch still records a row keyed by the caller-supplied `functionPath` so
791
- * without a cap a flood of distinct random paths would grow `__lunora_metrics`
792
- * (and its bucket/scan satellites) without bound, eventually filling the shard's
793
- * SQLite store shared with the app's real data. A few thousand registered
794
- * functions is already far beyond any real app, so a new path past this cap is
795
- * dropped while already-tracked paths keep accumulating.
820
+ * same reason): the real bound is the app's own registered-function set plus
821
+ * deploy churn (a rename/removal leaves its old path's row in place, still
822
+ * counted against the cap, until an operator's own retention/cleanup
823
+ * process there is none built in today) a few thousand registered
824
+ * functions is already far beyond any real app. `shard-do.ts`'s dispatch
825
+ * handler explicitly does NOT record per-function metrics for an
826
+ * unregistered/`FUNCTION_NOT_FOUND` dispatch (see the guard next to its
827
+ * `FUNCTION_NOT_FOUND` check), so a caller cannot mint arbitrary `path`s here
828
+ * the way a raw caller-supplied SQL shape can in `query-metrics.ts`. Without a
829
+ * cap, deploy churn across the app's lifetime would still grow
830
+ * `__lunora_metrics` (and its bucket/scan satellites) without bound,
831
+ * eventually filling the shard's SQLite store shared with the app's real
832
+ * data. At the cap, a brand-new path is refused — protecting the incumbent
833
+ * leaderboard from a flood of one-off paths is the point, so admission is
834
+ * refused rather than evicting an existing path to make room; already-tracked
835
+ * paths keep accumulating past the cap. `readFunctionMetricsTotals`'s
836
+ * `capped` is the read-side signal for this.
796
837
  */
797
838
  declare const FUNCTION_METRICS_MAX_PATHS = 5e3;
798
839
  /**
@@ -814,6 +855,22 @@ interface FunctionMetricBucket {
814
855
  /** Subset of `calls` that threw. */
815
856
  errors: number;
816
857
  }
858
+ /** {@link readFunctionMetricBuckets} result: the time-series window plus whether the read limit cut it short. */
859
+ interface FunctionMetricBucketsResult {
860
+ buckets: (FunctionMetricBucket & {
861
+ path: string;
862
+ })[];
863
+ /**
864
+ * True when more rows existed than {@link FUNCTION_METRICS_READ_LIMIT} could
865
+ * return, so `buckets` is a partial (newest) window rather than the app's
866
+ * full retained history. Mirrors `readQueryInsights`'s `capped` and
867
+ * `foldTraces`'s `total`: a silently truncated read looks identical to a
868
+ * complete one to a caller that doesn't check for it — the Metrics chart's
869
+ * window would appear to shrink as the app grows, with a wrong leftmost
870
+ * bar, and nothing would say why.
871
+ */
872
+ truncated: boolean;
873
+ }
817
874
  /** One declared index a dispatch exercised (used to narrow a read). */
818
875
  interface IndexHit {
819
876
  /** The declared index name. */
@@ -879,7 +936,9 @@ interface RecordFunctionMetricInput {
879
936
  * than baked into the `CREATE` so a shard whose `__lunora_metrics` predates the
880
937
  * causal-attribution feature gains the column on the next call without a
881
938
  * migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
882
- * error from a re-run is swallowed.
939
+ * error from a re-run is swallowed. Both the `CREATE`s and the back-fill only
940
+ * run once per handle (see {@link ensuredHandles}) — a handle already marked
941
+ * ensured returns immediately.
883
942
  */
884
943
  declare const ensureFunctionMetricsTables: (sql: SqlExec) => void;
885
944
  /**
@@ -934,18 +993,21 @@ declare const readFunctionMetrics: (sql: SqlExec) => FunctionCallStat[];
934
993
  /**
935
994
  * Read the coarse time-series buckets for `path` (every path when omitted),
936
995
  * oldest-bucket first so a chart can plot them left-to-right. Creates the table
937
- * first so reads on a never-called shard return `[]`.
996
+ * first so reads on a never-called shard return `{ buckets: [], truncated: false }`.
938
997
  */
939
- declare const readFunctionMetricBuckets: (sql: SqlExec, path?: string) => (FunctionMetricBucket & {
940
- path: string;
941
- })[];
998
+ declare const readFunctionMetricBuckets: (sql: SqlExec, path?: string) => FunctionMetricBucketsResult;
942
999
  /**
943
1000
  * Aggregate the persisted accumulators into the lifetime totals the metrics
944
- * health snapshot reports: total calls (`requests`), total `errors`, and the
945
- * earliest `last_called_at` seen — a best-effort "since" marker for durable
946
- * data. Returns zeroes on a never-called shard.
1001
+ * health snapshot reports: total calls (`requests`), total `errors`. Returns
1002
+ * zeroes on a never-called shard.
1003
+ *
1004
+ * `capped` is true when the distinct-path cap ({@link FUNCTION_METRICS_MAX_PATHS})
1005
+ * has been reached — the write-side signal that a brand-new function path is
1006
+ * currently being refused (see `admitPath`), mirroring `readQueryInsights`'s
1007
+ * `capped` for query-metrics and `readMetricHistory`'s for metric-history.
947
1008
  */
948
1009
  declare const readFunctionMetricsTotals: (sql: SqlExec) => {
1010
+ capped: boolean;
949
1011
  errors: number;
950
1012
  requests: number;
951
1013
  };
@@ -1261,6 +1323,17 @@ interface MetricHistorySeries {
1261
1323
  }
1262
1324
  /** {@link readMetricHistory} result: every tracked series with its buckets. */
1263
1325
  interface MetricHistoryResult {
1326
+ /**
1327
+ * True when the distinct-series cap has been reached — a **write-side**
1328
+ * signal ("this shard can no longer admit a brand-new series", see
1329
+ * `admitNewSeries`), not a read-side truncation flag. It is computed over
1330
+ * the whole table regardless of `options.sinceMs`/the row-count read
1331
+ * limit, so it can be `true` even when every series `readMetricHistory`
1332
+ * actually returned fits comfortably: the caller should read it as "a
1333
+ * flood of new series would currently be refused", the same thing
1334
+ * `readQueryInsights`'s `capped` already signals for query-metrics.
1335
+ */
1336
+ capped: boolean;
1264
1337
  series: MetricHistorySeries[];
1265
1338
  }
1266
1339
  /**
@@ -1269,7 +1342,7 @@ interface MetricHistoryResult {
1269
1342
  * omitted field keeps the historical behaviour.
1270
1343
  */
1271
1344
  interface MetricHistoryOptions {
1272
- /** Distinct series tracked before a brand-new one is dropped (default {@link METRIC_HISTORY_MAX_SERIES}). */
1345
+ /** Distinct series tracked before the least-recently-updated one is evicted to admit a new one (default {@link METRIC_HISTORY_MAX_SERIES}). */
1273
1346
  maxSeries?: number;
1274
1347
  /** Minute-buckets kept per series before older rows are trimmed (default {@link METRIC_HISTORY_BUCKET_RETENTION}). */
1275
1348
  retentionBuckets?: number;
@@ -1306,9 +1379,12 @@ declare const recordMetricHistory: (sql: SqlExec, event: MetricEvent, exemplarTr
1306
1379
  * trend line reads oldest→newest.
1307
1380
  *
1308
1381
  * `options.sinceMs`, when set, returns only buckets at or after this epoch-ms —
1309
- * the studio's time-window selector.
1382
+ * the studio's time-window selector. `options.maxSeries`, mirroring the write
1383
+ * side's tunable, is only used to compute `capped` — it does not affect which
1384
+ * rows are read.
1310
1385
  */
1311
1386
  declare const readMetricHistory: (sql: SqlExec, options?: {
1387
+ maxSeries?: number;
1312
1388
  sinceMs?: number;
1313
1389
  }) => MetricHistoryResult;
1314
1390
  /** One row of the `__lunora_metrics_queries` table, as returned by `readQueryMetrics`. */
@@ -1369,14 +1445,23 @@ interface QueryInsightsResult {
1369
1445
  */
1370
1446
  declare const readQueryInsights: (sql: SqlExec, rangeMs: number, now?: number) => QueryInsightsResult;
1371
1447
  /**
1372
- * Record one statement execution. Creates the table on first call. Silently
1448
+ * Record one statement's activity. Creates the table on first call. Silently
1373
1449
  * skips recording when the normalised statement is empty (shouldn't happen
1374
1450
  * in practice) or when the table is already at the
1375
1451
  * {@link QUERY_METRICS_MAX_STATEMENTS} cap and the statement is not yet
1376
- * tracked. The cap check is a single cheap `COUNT(*)` on the primary-key
1377
- * index, so the hot-path cost is minimal.
1452
+ * tracked. See `admitStatement` for how the cap check avoids an unconditional
1453
+ * `COUNT(*)` on every execution.
1454
+ *
1455
+ * `execCount` (default 1) lets a caller fold several executions of the SAME
1456
+ * statement into one call — `shard-do.ts` does this per dispatch so a
1457
+ * query-in-a-loop handler pays one upsert here instead of one per raw
1458
+ * execution. `durationMs`/`rowsRead`/`rowsWritten` are then the SUM across
1459
+ * `execCount` executions, exactly as `exec_count`/`total_duration_ms` already
1460
+ * accumulate sums across separate calls — folding before calling is
1461
+ * indistinguishable, from this table's point of view, from `execCount`
1462
+ * separate calls with the same totals.
1378
1463
  */
1379
- declare const recordQueryMetric: (sql: SqlExec, rawSql: string, durationMs: number, rowsRead: number, rowsWritten: number, now?: number) => void;
1464
+ declare const recordQueryMetric: (sql: SqlExec, rawSql: string, durationMs: number, rowsRead: number, rowsWritten: number, now?: number, execCount?: number) => void;
1380
1465
  /**
1381
1466
  * Read all tracked statement aggregates, ordered by `total_duration_ms DESC`
1382
1467
  * (the leaderboard's default). Creates the table first so a read on a
@@ -1393,7 +1478,7 @@ interface RequestLogEntry {
1393
1478
  cacheHit?: boolean;
1394
1479
  /** Handler wall-clock duration in milliseconds (before the subscription write-flush, matching the per-function metrics). */
1395
1480
  durationMs: number;
1396
- /** Error message when `outcome === "error"`; absent on success. */
1481
+ /** Error message when `outcome === "error"`, redacted like args/identity; absent on success. */
1397
1482
  errorMessage?: string;
1398
1483
  /** The `&lt;file>:&lt;function>` identifier dispatched, e.g. `messages:list`. */
1399
1484
  functionPath: string;
@@ -1477,11 +1562,16 @@ interface ErrorIssue {
1477
1562
  culprit: string;
1478
1563
  /** Wall-clock millis of the oldest folded row. */
1479
1564
  firstSeen: number;
1480
- /** Stable 16-char grouping hash over `functionPath :: bucket(message)`. */
1565
+ /**
1566
+ * Stable 16-char grouping hash over `functionPath :: bucket(message)`,
1567
+ * computed from the RAW (pre-redaction) message at write time and stored on
1568
+ * the row — see {@link appendRequestLogEntry} — so redacting `sampleMessage`
1569
+ * below can't change the grouping.
1570
+ */
1481
1571
  hash: string;
1482
1572
  /** Wall-clock millis of the newest folded row. */
1483
1573
  lastSeen: number;
1484
- /** A representative raw error message — taken from the most recent folded row. */
1574
+ /** A representative error message (redacted, like the durable row) — taken from the most recent folded row. */
1485
1575
  sampleMessage: string;
1486
1576
  /** Developer-tagged severity from the persisted triage state; absent when untriaged. */
1487
1577
  severity?: IssueSeverity;
@@ -1518,21 +1608,73 @@ interface ReadIssuesOptions {
1518
1608
  /** Exact acting-userId match. */
1519
1609
  userId?: string;
1520
1610
  }
1611
+ /**
1612
+ * Redact the secrets / PII out of a value before it reaches the durable log or a
1613
+ * Logpush event, via `@visulima/redact`'s `standardRules`. Unlike a blunt
1614
+ * type-tag stamp this masks sensitive values by PATTERN (not just by key name)
1615
+ * while leaving benign values readable, so the studio's args/identity columns
1616
+ * stay useful. `null` / `undefined` pass through unchanged.
1617
+ *
1618
+ * What `standardRules` actually catches differs by shape, verified against its
1619
+ * real behavior rather than assumed from its name: on a KEYED object (`args`,
1620
+ * `identity`) it also matches by key name, so `{ password: "hunter2" }` and
1621
+ * `{ token: "…" }` ARE masked regardless of the value's shape. On a PLAIN
1622
+ * STRING — which is what `errorMessage`/log `fields`-as-rendered-text are —
1623
+ * only pattern-shaped matches apply: emails, long digit runs / structured
1624
+ * numeric IDs (credit-card, phone, SSN, AWS-access-key-style), and an explicit
1625
+ * `Bearer &lt;token>` / `token=…`-shaped substring. A free-text `password=hunter2`
1626
+ * or a bare provider API key embedded in prose (e.g. `sk-live-…`) is NOT
1627
+ * caught on a plain string — there is no key to match against, and neither is
1628
+ * a recognized value pattern. So this is a PII-pattern net for rendered text,
1629
+ * not a general secrets scrubber; a handler that echoes a raw credential into
1630
+ * an error message or a log string can still leak it through here. Works on a
1631
+ * plain string too (`redact` traverses whatever value it's handed), which is
1632
+ * how {@link appendRequestLogEntry} and {@link emitRequestLogEvent} reuse this
1633
+ * for `errorMessage` — a validation error echoes the offending value, a
1634
+ * constraint error quotes the conflicting row, so the error message is at
1635
+ * least as PII-dense as args and gets the same treatment (with the free-text
1636
+ * caveat above).
1637
+ *
1638
+ * `captureRaw` is the development escape hatch: in a dev environment the dispatch
1639
+ * site (`isDevEnvironment`) passes `true` to skip redaction so a developer can
1640
+ * see real arg/identity/error values; production always redacts. The dev
1641
+ * decision is made at the call site from the deployment env, never inferred
1642
+ * here — so a real deploy that omits the env var stays redacted.
1643
+ */
1644
+ declare const redactArgs: (value: unknown, captureRaw?: boolean) => unknown;
1521
1645
  /**
1522
1646
  * Create the `__lunora_reqlog__` table. `seq` is an `AUTOINCREMENT` primary
1523
1647
  * key, giving each shard a monotonic cursor the Logs tab pages through; the
1524
1648
  * `args`/`identity`/`tables_read`/`tables_written` columns hold JSON and are
1525
1649
  * `NULL`/empty when none was recorded. Idempotent, so read and write paths can
1526
1650
  * call it defensively.
1651
+ *
1652
+ * `error_fingerprint` is the {@link fingerprintError} grouping hash captured
1653
+ * from the RAW `error_message` at write time, before {@link appendRequestLogEntry}
1654
+ * redacts it — see that function's docstring. It is added via a guarded
1655
+ * `ALTER TABLE` rather than baked into the `CREATE`, mirroring
1656
+ * `function-metrics.ts`'s `ensureFunctionMetricsTables`, so a shard whose
1657
+ * `__lunora_reqlog__` predates this column gains it on the next call without a
1658
+ * migration. SQLite has no `ADD COLUMN IF NOT EXISTS`, so the duplicate-column
1659
+ * error from a re-run (or the freshly-created schema above) is swallowed.
1527
1660
  */
1528
1661
  declare const ensureRequestLogTable: (sql: SqlExec) => void;
1529
1662
  /**
1530
1663
  * Append one dispatch to the request log, then trim the log back to the most
1531
1664
  * recent `retention` rows (default {@link REQUEST_LOG_RETENTION}). Creates the
1532
- * table first so callers needn't. Args/identity are redacted here so a raw value
1533
- * never reaches the durable table — callers pass the unredacted entry and rely on
1534
- * this, unless `captureRaw` (dev only) is set. `retention` is the operator's
1535
- * `LUNORA_REQUEST_LOG_RETENTION` override, threaded in by the dispatch site.
1665
+ * table first so callers needn't. Args/identity/error message are redacted here
1666
+ * so a raw value never reaches the durable table — callers pass the unredacted
1667
+ * entry and rely on this, unless `captureRaw` (dev only) is set. `retention` is
1668
+ * the operator's `LUNORA_REQUEST_LOG_RETENTION` override, threaded in by the
1669
+ * dispatch site.
1670
+ *
1671
+ * The error-grouping fingerprint is computed from `entry.errorMessage` BEFORE
1672
+ * it's redacted below, and the resulting hash is persisted in
1673
+ * `error_fingerprint`. `readErrorIssues` groups off that stored hash instead of
1674
+ * recomputing `fingerprintError` from the (redacted) `error_message` column, so
1675
+ * masking a PII-bearing value — e.g. two different `&lt;n>`-bucketed IDs that
1676
+ * redact to two different tag lengths (`&lt;DL>` vs `&lt;BANKACC>`) — can't split an
1677
+ * existing Issue or change its identity.
1536
1678
  */
1537
1679
  declare const appendRequestLogEntry: (sql: SqlExec, entry: AppendRequestLogEntry, options?: RequestLogWriteOptions) => void;
1538
1680
  /**
@@ -1541,8 +1683,8 @@ declare const appendRequestLogEntry: (sql: SqlExec, entry: AppendRequestLogEntry
1541
1683
  * NOT reimplement a transport: it produces a richer, lunora-attributed event and
1542
1684
  * lets CF's existing trace-log pipe ship it. The event mirrors the durable
1543
1685
  * `__lunora_reqlog__` row (function path, shard, user, outcome, duration, tables
1544
- * read/written, cache hit), with `args` AND `identity` redacted exactly like the
1545
- * durable write so no raw PII/secret reaches the log pipeline.
1686
+ * read/written, cache hit), with `args`, `identity`, AND `error` redacted
1687
+ * exactly like the durable write so no raw PII/secret reaches the log pipeline.
1546
1688
  *
1547
1689
  * An `error` outcome goes to `console.error` (surfacing at error level in the
1548
1690
  * trace so a SIEM can alert on it); everything else to `console.log`. The
@@ -1591,11 +1733,19 @@ declare const parseLogArgs: (args: unknown[], boundFields?: LogFields) => {
1591
1733
  *
1592
1734
  * Structured `fields` (plus `traceId`/`spanId` for correlation) ARE emitted here
1593
1735
  * — they are intentional metadata a log pipeline filters on, unlike raw `args`.
1594
- * A field value that can't be serialised (a circular object) would make
1595
- * `JSON.stringify` throw and drop the whole line, so serialisation falls back to
1596
- * a fields-free line rather than losing the event.
1597
- */
1598
- declare const emitLogEvent: (input: LogEventInput) => void;
1736
+ * Unlike `args`, `fields` IS redacted before it rides this console line — a
1737
+ * developer can attach anything to a fields bag (`ctx.log.info("charged",
1738
+ * { email, cardLast4 })`), and this is the one line that's told to a SIEM as
1739
+ * trustworthy, exactly like the request-log `args`/`identity`/`error` columns.
1740
+ * `options.captureRaw` (dev only) skips it, mirroring every other redaction
1741
+ * point in this module; the sole current caller (`ShardDO.recordUserLog`)
1742
+ * doesn't yet thread a dev flag through, so `fields` redacts unconditionally
1743
+ * there today — a conservative default, never a correctness gap. A field value
1744
+ * that can't be serialised (a circular object) would make `JSON.stringify`
1745
+ * throw and drop the whole line, so serialisation falls back to a fields-free
1746
+ * line rather than losing the event.
1747
+ */
1748
+ declare const emitLogEvent: (input: LogEventInput, options?: RequestLogWriteOptions) => void;
1599
1749
  /**
1600
1750
  * Read request-log entries newest-first, AND-combining the supplied filters
1601
1751
  * (function-path prefix, exact userId/shardKey/outcome, and a table-touched
@@ -1815,4 +1965,4 @@ declare const resolveTraceAnchor: (traceparent: string | undefined) => {
1815
1965
  sampled: boolean;
1816
1966
  traceId: string;
1817
1967
  };
1818
- export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, type AiRunBinding, type AppendRequestLogEntry, type AuthMetrics, type AuthMetricsBucket, type ContextFetch, type ContextLogLevel, type ContextMetrics, type ContextTracer, DEFAULT_EXPLAIN_ISSUE_MODEL, type DatabaseInstrumentation, type DatabaseTally, type ExplainIssueArgs, type ExplainIssueDegradedReason, type ExplainIssueGrounding, type ExplainIssueResult, FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, type FunctionMetricBucket, type FunctionMetricIndexHit, type HostTracingLike, ISSUE_SEVERITIES, ISSUE_STATE_TABLE, ISSUE_STATUSES, type IndexHit, type IssueSeverity, type IssueState, type IssueStatePatch, type IssueStatus, type IssuesResult, LogBuffer, type LogEntry, type LogEventInput, type LogLevel, MIN_ADMIN_TOKEN_LENGTH, MIN_AUTH_SECRET_LENGTH, MetricBuffer, type MetricHistoryOptions, type MetricHistoryPoint, type MetricHistorySeries, type MetricSeries, type MetricsDeps, type QueryStatEntry, REQUEST_LOG_TABLE, type RecordAuthEventInput, type RecordFunctionMetricInput, type RequestLogResult, type RequestLogWriteOptions, type SecurityAuditResult, type SecurityFinding, type SecurityFindingKind, type SecurityFindingLevel, SpanBuffer, type SpanCollection, type SpanCollector, type SpanHandle, type TraceAnchor, type TraceSpan, type TraceSummary, type TracerDeps, appendRequestLogEntry, buildSecurityAudit, createDatabaseTally, createMetrics, createSpanCollector, createTracedFetch, createTracer, dispatchRootSpan, emitLogEvent, emitRequestLogEvent, ensureAuthMetricsTables, ensureFunctionMetricsTables, ensureRequestLogTable, explainIssue, findDanglingReferences, foldTraces, formatTally, instrumentDatabase, mergeScanAttribution, parseExplainIssueArgs, parseLogArgs, readAuthMetrics, readErrorIssues, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, readMetricHistory, readQueryInsights, readQueryMetrics, readRequestLog, recordAuthEvent, recordFunctionMetric, recordMetricHistory, recordQueryMetric, resolveTraceAnchor, upsertIssueState };
1968
+ export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, type AiRunBinding, type AppendRequestLogEntry, type AuthMetrics, type AuthMetricsBucket, type ContextFetch, type ContextLogLevel, type ContextMetrics, type ContextTracer, DEFAULT_EXPLAIN_ISSUE_MODEL, type DanglingReference, type DanglingReferenceResult, type DatabaseInstrumentation, type DatabaseTally, type DatabaseTelemetryDeps, type ErrorIssue, type ExplainIssueArgs, type ExplainIssueDegradedReason, type ExplainIssueGrounding, type ExplainIssueResult, FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_MAX_PATHS, FUNCTION_METRICS_READ_LIMIT, FUNCTION_METRICS_SCANS_TABLE, FUNCTION_METRICS_TABLE, type FoldedTraces, type FunctionMetricBucket, type FunctionMetricBucketsResult, type FunctionMetricIndexHit, type HostSpanLike, type HostTracingLike, type HostTracingResolver, ISSUE_SEVERITIES, ISSUE_STATE_TABLE, ISSUE_STATUSES, type IndexHit, type IssueSeverity, type IssueState, type IssueStatePatch, type IssueStatus, type IssuesResult, LogBuffer, type LogEntry, type LogEventInput, type LogLevel, MIN_ADMIN_TOKEN_LENGTH, MIN_AUTH_SECRET_LENGTH, MetricBuffer, type MetricEvent, type MetricHistoryOptions, type MetricHistoryPoint, type MetricHistoryResult, type MetricHistorySeries, type MetricKind, type MetricSeries, type MetricsDeps, type QueryInsightBucket, type QueryInsightEntry, type QueryInsightsResult, type QueryStatEntry, REQUEST_LOG_TABLE, type ReadIssuesOptions, type ReadRequestLogOptions, type RecordAuthEventInput, type RecordFunctionMetricInput, type RequestLogEntry, type RequestLogResult, type RequestLogWriteOptions, type RequestOutcome, type SecurityAuditResult, type SecurityFinding, type SecurityFindingKind, type SecurityFindingLevel, SpanBuffer, type SpanCollection, type SpanCollector, type SpanEvent, type SpanEventPoint, type SpanHandle, type OtlpSpanKind as SpanKind, type SpanLink, type SpanOptions, type TraceAnchor, type TraceSpan, type TraceSummary, type TracedFetchDeps, type TracerDeps, appendRequestLogEntry, buildSecurityAudit, createDatabaseTally, createMetrics, createSpanCollector, createTracedFetch, createTracer, dispatchRootSpan, emitLogEvent, emitRequestLogEvent, ensureAuthMetricsTables, ensureFunctionMetricsTables, ensureRequestLogTable, explainIssue, findDanglingReferences, foldTraces, formatTally, instrumentDatabase, mergeScanAttribution, parseExplainIssueArgs, parseLogArgs, readAuthMetrics, readErrorIssues, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetricScans, readFunctionMetrics, readFunctionMetricsTotals, readMetricHistory, readQueryInsights, readQueryMetrics, readRequestLog, recordAuthEvent, recordFunctionMetric, recordMetricHistory, recordQueryMetric, redactArgs, resolveTraceAnchor, upsertIssueState };
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{AUTH_METRICS_BUCKETS_TABLE as t,AUTH_METRICS_BUCKET_MS as T,AUTH_METRICS_BUCKET_RETENTION as o,AUTH_METRICS_TABLE as E,ensureAuthMetricsTables as _,readAuthMetrics as a,recordAuthEvent as s}from"./packem_shared/AUTH_METRICS_BUCKETS_TABLE-C2V-DiGu.mjs";import{createMetrics as S,createSpanCollector as I,createTracedFetch as i,createTracer as n,dispatchRootSpan as M}from"./packem_shared/createMetrics-fpVig63l.mjs";import{createDatabaseTally as A,formatTally as C,instrumentDatabase as N}from"./packem_shared/createDatabaseTally-BTqRpC0j.mjs";import{FUNCTION_METRICS_BUCKETS_TABLE as U,FUNCTION_METRICS_BUCKET_MS as f,FUNCTION_METRICS_BUCKET_RETENTION as R,FUNCTION_METRICS_INDEX_TABLE as d,FUNCTION_METRICS_MAX_PATHS as L,FUNCTION_METRICS_READ_LIMIT as m,FUNCTION_METRICS_SCANS_TABLE as x,FUNCTION_METRICS_TABLE as B,ensureFunctionMetricsTables as F,mergeScanAttribution as l,readFunctionMetricBuckets as g,readFunctionMetricIndexHits as O,readFunctionMetricScans as H,readFunctionMetrics as y,readFunctionMetricsTotals as D,recordFunctionMetric as b}from"./packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-B1U7xWMo.mjs";import{DEFAULT_EXPLAIN_ISSUE_MODEL as K,explainIssue as q,parseExplainIssueArgs as v}from"./packem_shared/DEFAULT_EXPLAIN_ISSUE_MODEL-xF11R4vX.mjs";import{ISSUE_SEVERITIES as G,ISSUE_STATE_TABLE as X,ISSUE_STATUSES as P,upsertIssueState as k}from"./packem_shared/ISSUE_SEVERITIES-Js6lGUG5.mjs";import{LogBuffer as j}from"./packem_shared/LogBuffer-bIvCelI-.mjs";import{M as z}from"./packem_shared/metric-buffer-CdgXal7w.mjs";import{readMetricHistory as W,recordMetricHistory as Y}from"./packem_shared/readMetricHistory-DtnhufUq.mjs";import{readQueryInsights as $,readQueryMetrics as ee,recordQueryMetric as re}from"./packem_shared/readQueryInsights-CwSKB0I1.mjs";import{REQUEST_LOG_TABLE as Te,appendRequestLogEntry as oe,emitLogEvent as Ee,emitRequestLogEvent as _e,ensureRequestLogTable as ae,parseLogArgs as se,readErrorIssues as ce,readRequestLog as Se}from"./packem_shared/REQUEST_LOG_TABLE-Q_BYgAzY.mjs";import{MIN_ADMIN_TOKEN_LENGTH as ie,MIN_AUTH_SECRET_LENGTH as ne,buildSecurityAudit as Me}from"./packem_shared/MIN_ADMIN_TOKEN_LENGTH-T-zuwRVR.mjs";import{SpanBuffer as Ae,foldTraces as Ce}from"./packem_shared/SpanBuffer-BR6Ff0M-.mjs";import{findDanglingReferences as pe}from"./packem_shared/findDanglingReferences-D-x9LvY0.mjs";import{r as fe}from"./packem_shared/trace-context-DrdF960P.mjs";export{t as AUTH_METRICS_BUCKETS_TABLE,T as AUTH_METRICS_BUCKET_MS,o as AUTH_METRICS_BUCKET_RETENTION,E as AUTH_METRICS_TABLE,K as DEFAULT_EXPLAIN_ISSUE_MODEL,U as FUNCTION_METRICS_BUCKETS_TABLE,f as FUNCTION_METRICS_BUCKET_MS,R as FUNCTION_METRICS_BUCKET_RETENTION,d as FUNCTION_METRICS_INDEX_TABLE,L as FUNCTION_METRICS_MAX_PATHS,m as FUNCTION_METRICS_READ_LIMIT,x as FUNCTION_METRICS_SCANS_TABLE,B as FUNCTION_METRICS_TABLE,G as ISSUE_SEVERITIES,X as ISSUE_STATE_TABLE,P as ISSUE_STATUSES,j as LogBuffer,ie as MIN_ADMIN_TOKEN_LENGTH,ne as MIN_AUTH_SECRET_LENGTH,z as MetricBuffer,Te as REQUEST_LOG_TABLE,Ae as SpanBuffer,oe as appendRequestLogEntry,Me as buildSecurityAudit,A as createDatabaseTally,S as createMetrics,I as createSpanCollector,i as createTracedFetch,n as createTracer,M as dispatchRootSpan,Ee as emitLogEvent,_e as emitRequestLogEvent,_ as ensureAuthMetricsTables,F as ensureFunctionMetricsTables,ae as ensureRequestLogTable,q as explainIssue,pe as findDanglingReferences,Ce as foldTraces,C as formatTally,N as instrumentDatabase,l as mergeScanAttribution,v as parseExplainIssueArgs,se as parseLogArgs,a as readAuthMetrics,ce as readErrorIssues,g as readFunctionMetricBuckets,O as readFunctionMetricIndexHits,H as readFunctionMetricScans,y as readFunctionMetrics,D as readFunctionMetricsTotals,W as readMetricHistory,$ as readQueryInsights,ee as readQueryMetrics,Se as readRequestLog,s as recordAuthEvent,b as recordFunctionMetric,Y as recordMetricHistory,re as recordQueryMetric,fe as resolveTraceAnchor,k as upsertIssueState};
1
+ import{AUTH_METRICS_BUCKETS_TABLE as t,AUTH_METRICS_BUCKET_MS as T,AUTH_METRICS_BUCKET_RETENTION as o,AUTH_METRICS_TABLE as a,ensureAuthMetricsTables as s,readAuthMetrics as E,recordAuthEvent as _}from"./packem_shared/AUTH_METRICS_BUCKETS_TABLE-C2V-DiGu.mjs";import{createMetrics as S,createSpanCollector as I,createTracedFetch as i,createTracer as n,dispatchRootSpan as M}from"./packem_shared/createMetrics-BXNmqy0o.mjs";import{createDatabaseTally as A,formatTally as C,instrumentDatabase as N}from"./packem_shared/createDatabaseTally-DBs7wDeF.mjs";import{FUNCTION_METRICS_BUCKETS_TABLE as p,FUNCTION_METRICS_BUCKET_MS as d,FUNCTION_METRICS_BUCKET_RETENTION as f,FUNCTION_METRICS_INDEX_TABLE as R,FUNCTION_METRICS_MAX_PATHS as L,FUNCTION_METRICS_READ_LIMIT as m,FUNCTION_METRICS_SCANS_TABLE as x,FUNCTION_METRICS_TABLE as B,ensureFunctionMetricsTables as F,mergeScanAttribution as l,readFunctionMetricBuckets as g,readFunctionMetricIndexHits as O,readFunctionMetricScans as H,readFunctionMetrics as y,readFunctionMetricsTotals as D,recordFunctionMetric as K}from"./packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-BB8kcFBe.mjs";import{DEFAULT_EXPLAIN_ISSUE_MODEL as h,explainIssue as q,parseExplainIssueArgs as v}from"./packem_shared/DEFAULT_EXPLAIN_ISSUE_MODEL-xF11R4vX.mjs";import{ISSUE_SEVERITIES as G,ISSUE_STATE_TABLE as X,ISSUE_STATUSES as k,upsertIssueState as P}from"./packem_shared/ISSUE_SEVERITIES-Js6lGUG5.mjs";import{LogBuffer as V}from"./packem_shared/LogBuffer-bIvCelI-.mjs";import{M as j}from"./packem_shared/metric-buffer-CdgXal7w.mjs";import{readMetricHistory as J,recordMetricHistory as Y}from"./packem_shared/readMetricHistory-CVcUgQEL.mjs";import{readQueryInsights as $,readQueryMetrics as ee,recordQueryMetric as re}from"./packem_shared/readQueryInsights-DN07YcQy.mjs";import{d as Te,k as oe,K as ae,w as se,m as Ee,C as _e,W as ce,U as Se,c as Ie}from"./packem_shared/request-log-BwFic6VV.mjs";import{MIN_ADMIN_TOKEN_LENGTH as ne,MIN_AUTH_SECRET_LENGTH as Me,buildSecurityAudit as ue}from"./packem_shared/MIN_ADMIN_TOKEN_LENGTH-T-zuwRVR.mjs";import{SpanBuffer as Ce,foldTraces as Ne}from"./packem_shared/SpanBuffer-BR6Ff0M-.mjs";import{findDanglingReferences as pe}from"./packem_shared/findDanglingReferences-D-x9LvY0.mjs";import{r as fe}from"./packem_shared/trace-context-DrdF960P.mjs";export{t as AUTH_METRICS_BUCKETS_TABLE,T as AUTH_METRICS_BUCKET_MS,o as AUTH_METRICS_BUCKET_RETENTION,a as AUTH_METRICS_TABLE,h as DEFAULT_EXPLAIN_ISSUE_MODEL,p as FUNCTION_METRICS_BUCKETS_TABLE,d as FUNCTION_METRICS_BUCKET_MS,f as FUNCTION_METRICS_BUCKET_RETENTION,R as FUNCTION_METRICS_INDEX_TABLE,L as FUNCTION_METRICS_MAX_PATHS,m as FUNCTION_METRICS_READ_LIMIT,x as FUNCTION_METRICS_SCANS_TABLE,B as FUNCTION_METRICS_TABLE,G as ISSUE_SEVERITIES,X as ISSUE_STATE_TABLE,k as ISSUE_STATUSES,V as LogBuffer,ne as MIN_ADMIN_TOKEN_LENGTH,Me as MIN_AUTH_SECRET_LENGTH,j as MetricBuffer,Te as REQUEST_LOG_TABLE,Ce as SpanBuffer,oe as appendRequestLogEntry,ue as buildSecurityAudit,A as createDatabaseTally,S as createMetrics,I as createSpanCollector,i as createTracedFetch,n as createTracer,M as dispatchRootSpan,ae as emitLogEvent,se as emitRequestLogEvent,s as ensureAuthMetricsTables,F as ensureFunctionMetricsTables,Ee as ensureRequestLogTable,q as explainIssue,pe as findDanglingReferences,Ne as foldTraces,C as formatTally,N as instrumentDatabase,l as mergeScanAttribution,v as parseExplainIssueArgs,_e as parseLogArgs,E as readAuthMetrics,ce as readErrorIssues,g as readFunctionMetricBuckets,O as readFunctionMetricIndexHits,H as readFunctionMetricScans,y as readFunctionMetrics,D as readFunctionMetricsTotals,J as readMetricHistory,$ as readQueryInsights,ee as readQueryMetrics,Se as readRequestLog,_ as recordAuthEvent,K as recordFunctionMetric,Y as recordMetricHistory,re as recordQueryMetric,Ie as redactArgs,fe as resolveTraceAnchor,P as upsertIssueState};
@@ -0,0 +1,56 @@
1
+ const E="__lunora_metrics",l="__lunora_metrics_buckets",c="__lunora_metrics_scans",d="__lunora_metrics_index",M=6e4,U=1440,F=5e3,p=1e3,r=(e,a,...s)=>e.exec.call(e,a,...s),A=e=>Math.floor(e/6e4)*6e4,I=e=>{const a=new Map;for(const s of e)a.set(`${s.table}\0${s.index}`,{index:s.index,table:s.table});return[...a.values()]},N=new WeakSet,_=e=>{if(!N.has(e)){r(e,`CREATE TABLE IF NOT EXISTS "${E}" (
2
+ path TEXT PRIMARY KEY,
3
+ calls INTEGER NOT NULL DEFAULT 0,
4
+ errors INTEGER NOT NULL DEFAULT 0,
5
+ conflicts INTEGER NOT NULL DEFAULT 0,
6
+ scans INTEGER NOT NULL DEFAULT 0,
7
+ total_duration_ms REAL NOT NULL DEFAULT 0,
8
+ min_duration_ms REAL,
9
+ max_duration_ms REAL NOT NULL DEFAULT 0,
10
+ last_called_at REAL NOT NULL DEFAULT 0,
11
+ last_error_at REAL,
12
+ last_error_message TEXT
13
+ )`);for(const a of["scans","conflicts"])try{r(e,`ALTER TABLE "${E}" ADD COLUMN ${a} INTEGER NOT NULL DEFAULT 0`)}catch{}r(e,`CREATE TABLE IF NOT EXISTS "${l}" (
14
+ path TEXT NOT NULL,
15
+ bucket_ms INTEGER NOT NULL,
16
+ calls INTEGER NOT NULL DEFAULT 0,
17
+ errors INTEGER NOT NULL DEFAULT 0,
18
+ PRIMARY KEY (path, bucket_ms)
19
+ )`),r(e,`CREATE TABLE IF NOT EXISTS "${c}" (
20
+ path TEXT NOT NULL,
21
+ table_name TEXT NOT NULL,
22
+ scans INTEGER NOT NULL DEFAULT 0,
23
+ PRIMARY KEY (path, table_name)
24
+ )`),r(e,`CREATE TABLE IF NOT EXISTS "${d}" (
25
+ table_name TEXT NOT NULL,
26
+ index_name TEXT NOT NULL,
27
+ reads INTEGER NOT NULL DEFAULT 0,
28
+ PRIMARY KEY (table_name, index_name)
29
+ )`),N.add(e)}},L=new WeakMap,R=e=>{let a=L.get(e);return a===void 0&&(a=new Set,L.set(e,a)),a},O=(e,a)=>{const s=R(e);return s.has(a)?!0:r(e,`SELECT 1 AS c FROM "${E}" WHERE path = ? LIMIT 1`,a).toArray().length>0?(s.add(a),!0):r(e,`SELECT COUNT(*) AS n FROM "${E}"`).one().n>=5e3?!1:(s.add(a),!0)},b=(e,a)=>{if(_(e),!O(e,a.path))return;const s=a.scannedTables?[...new Set(a.scannedTables)]:[],t=s.length,n=I(a.indexHits??[]),T=a.errored?1:0,i=a.conflicted?1:0,u=a.errored?a.ts:null,m=a.errored?a.errorMessage??null:null;r(e,`INSERT INTO "${E}"
30
+ (path, calls, errors, conflicts, scans, total_duration_ms, min_duration_ms, max_duration_ms, last_called_at, last_error_at, last_error_message)
31
+ VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32
+ ON CONFLICT(path) DO UPDATE SET
33
+ calls = calls + 1,
34
+ errors = errors + excluded.errors,
35
+ conflicts = conflicts + excluded.conflicts,
36
+ scans = scans + excluded.scans,
37
+ total_duration_ms = total_duration_ms + excluded.total_duration_ms,
38
+ min_duration_ms = MIN(COALESCE(min_duration_ms, excluded.min_duration_ms), excluded.min_duration_ms),
39
+ max_duration_ms = MAX(max_duration_ms, excluded.max_duration_ms),
40
+ last_called_at = excluded.last_called_at,
41
+ last_error_at = CASE WHEN excluded.last_error_at IS NULL THEN last_error_at ELSE excluded.last_error_at END,
42
+ last_error_message = CASE WHEN excluded.last_error_at IS NULL THEN last_error_message ELSE excluded.last_error_message END`,a.path,T,i,t,a.durationMs,a.durationMs,a.durationMs,a.ts,u,m);const S=A(a.ts);r(e,`INSERT INTO "${l}" (path, bucket_ms, calls, errors)
43
+ VALUES (?, ?, 1, ?)
44
+ ON CONFLICT(path, bucket_ms) DO UPDATE SET
45
+ calls = calls + 1,
46
+ errors = errors + excluded.errors`,a.path,S,T),r(e,`DELETE FROM "${l}"
47
+ WHERE path = ?
48
+ AND bucket_ms <= (
49
+ SELECT MAX(bucket_ms) - ? FROM "${l}" WHERE path = ?
50
+ )`,a.path,1440*6e4,a.path);for(const o of s)r(e,`INSERT INTO "${c}" (path, table_name, scans)
51
+ VALUES (?, ?, 1)
52
+ ON CONFLICT(path, table_name) DO UPDATE SET
53
+ scans = scans + 1`,a.path,o);for(const o of n)r(e,`INSERT INTO "${d}" (table_name, index_name, reads)
54
+ VALUES (?, ?, 1)
55
+ ON CONFLICT(table_name, index_name) DO UPDATE SET
56
+ reads = reads + 1`,o.table,o.index)},C=e=>{_(e);const a=r(e,`SELECT path, table_name, scans FROM "${c}" ORDER BY scans DESC, path ASC, table_name ASC LIMIT ${String(1e3)}`).toArray(),s=new Map;for(const t of a){const n=s.get(t.path),T={scans:t.scans,table:t.table_name};n===void 0?s.set(t.path,[T]):n.push(T)}return s},h=e=>(_(e),r(e,`SELECT table_name, index_name, reads FROM "${d}" ORDER BY table_name ASC, index_name ASC LIMIT ${String(1e3)}`).toArray().map(a=>({index:a.index_name,reads:a.reads,table:a.table_name}))),D=(e,a)=>{for(const s of a){const t=e.find(n=>n.table===s);t===void 0?e.push({scans:1,table:s}):t.scans+=1}return e.sort((s,t)=>t.scans-s.scans||s.table.localeCompare(t.table)),e},x=e=>{_(e);const a=C(e);return r(e,`SELECT * FROM "${E}" ORDER BY last_called_at DESC LIMIT ${String(1e3)}`).toArray().map(s=>({calls:s.calls,conflicts:s.conflicts,errors:s.errors,lastCalledAt:s.last_called_at,lastErrorAt:s.last_error_at,lastErrorMessage:s.last_error_message,maxDurationMs:s.max_duration_ms,path:s.path,scannedTables:a.get(s.path)??[],scans:s.scans,totalDurationMs:s.total_duration_ms}))},$=(e,a)=>{_(e);const s=a===void 0?r(e,`SELECT path, bucket_ms, calls, errors FROM "${l}" ORDER BY bucket_ms DESC, path ASC LIMIT ${String(1001)}`).toArray():r(e,`SELECT path, bucket_ms, calls, errors FROM "${l}" WHERE path = ? ORDER BY bucket_ms DESC LIMIT ${String(1001)}`,a).toArray(),t=s.length>1e3;return{buckets:(t?s.slice(0,1e3):s).toReversed().map(n=>({bucketMs:n.bucket_ms,calls:n.calls,errors:n.errors,path:n.path})),truncated:t}},f=e=>{_(e);const a=r(e,`SELECT SUM(calls) AS requests, SUM(errors) AS errors FROM "${E}"`).one();return{capped:r(e,`SELECT COUNT(*) AS n FROM "${E}"`).one().n>=5e3,errors:a.errors??0,requests:a.requests??0}};export{l as FUNCTION_METRICS_BUCKETS_TABLE,M as FUNCTION_METRICS_BUCKET_MS,U as FUNCTION_METRICS_BUCKET_RETENTION,d as FUNCTION_METRICS_INDEX_TABLE,F as FUNCTION_METRICS_MAX_PATHS,p as FUNCTION_METRICS_READ_LIMIT,c as FUNCTION_METRICS_SCANS_TABLE,E as FUNCTION_METRICS_TABLE,_ as ensureFunctionMetricsTables,D as mergeScanAttribution,$ as readFunctionMetricBuckets,h as readFunctionMetricIndexHits,C as readFunctionMetricScans,x as readFunctionMetrics,f as readFunctionMetricsTotals,b as recordFunctionMetric};
@@ -0,0 +1 @@
1
+ import"@lunora/fingerprint";import"@visulima/redact";import{E as o,d as E,k as g,K as L,w as m,m as p,C as R,W as d,U as n,c as u,y as T}from"./request-log-BwFic6VV.mjs";import"./ISSUE_SEVERITIES-Js6lGUG5.mjs";export{o as REQUEST_LOG_RETENTION,E as REQUEST_LOG_TABLE,g as appendRequestLogEntry,L as emitLogEvent,m as emitRequestLogEvent,p as ensureRequestLogTable,R as parseLogArgs,d as readErrorIssues,n as readRequestLog,u as redactArgs,T as renderLogMessage};
@@ -0,0 +1 @@
1
+ import{O as h,t as m}from"./trace-context-DrdF960P.mjs";import{c as b}from"./request-log-BwFic6VV.mjs";const M=100,w=new Set(["aggregate","count","delete","deleteMany","deleteWhere","findFirst","findFirstOrThrow","findMany","get","groupBy","insert","insertMany","insertManyUnsafe","lookupById","patch","patchMany","patchWhere","rank","rankBefore","rankPage","rankPageRows","replace","restore"]),k=new Set(["aggregate","count","deleteWhere","findFirst","findFirstOrThrow","findMany","groupBy","insert","insertMany","insertManyUnsafe","patchWhere","rank","rankBefore","rankPage","rankPageRows"]),O=(t,r)=>{if(!k.has(t))return;const e=r[0];return typeof e=="string"&&e.length>0?e:void 0},T=t=>t instanceof Error?t.message:typeof t=="string"?t:JSON.stringify(t)??String(t),I=t=>{const{deps:r,durationMs:e,failure:n,operation:s,startTs:a,table:o}=t;return{attributes:{"db.operation.name":s,...o===void 0?{}:{"db.collection.name":o},"db.system.name":"sqlite"},durationMs:e,...n===void 0?{}:{error:{message:b(T(n),r.captureRaw),type:m(n)}},functionPath:r.functionPath,kind:"client",name:o===void 0?`db.${s}`:`db.${s} ${o}`,ok:n===void 0,parentSpanId:r.anchor.rootSpanId,shardKey:r.shardKey,spanId:h(8),startTs:a,traceId:r.anchor.traceId,userId:r.userId()}},S=(t,r)=>{if(r.mode==="off")return t;const{tally:e}=r,n=new Map;return new Proxy(t,{get(s,a,o){const d=Reflect.get(s,a,o);if(typeof a!="string"||typeof d!="function"||!w.has(a))return d;const p=n.get(a);if(p!==void 0)return p;const y=d,u=async(...f)=>{const l=Date.now(),g=O(a,f);let c;try{return await y.apply(s,f)}catch(i){throw c=i,i}finally{const i=Date.now()-l;e.calls+=1,e.durationMs+=i,e.perOperation[a]=(e.perOperation[a]??0)+1,c!==void 0&&(e.errors+=1);try{r.mode==="spans"&&(e.spansEmitted>=M?e.spansTruncated=!0:(e.spansEmitted+=1,r.record(I({deps:r,durationMs:i,failure:c,operation:a,startTs:l,table:g}))))}catch{}}};return n.set(a,u),u}})},E=()=>({calls:0,durationMs:0,errors:0,perOperation:{},spansEmitted:0,spansTruncated:!1}),B=t=>{const r={"db.calls":t.calls,"db.duration_ms":t.durationMs};t.errors>0&&(r["db.errors"]=t.errors),t.spansTruncated&&(r["db.spans_truncated"]=!0);for(const[e,n]of Object.entries(t.perOperation))r[`db.op.${e}`]=n;return r};export{E as createDatabaseTally,B as formatTally,S as instrumentDatabase};
@@ -0,0 +1 @@
1
+ import{c as K,n as g}from"./request-log-BwFic6VV.mjs";import{w as b,O as M,m as R,t as T}from"./trace-context-DrdF960P.mjs";const x=/[\w.-]/u,D=t=>{let e="";for(const r of t)e+=x.test(r)?r:"_";return e},L=t=>{if(typeof t.name!="string"||t.name.length===0)throw new Error("recordEvaluation requires a non-empty `name`");if(typeof t.score!="number"||!Number.isFinite(t.score))throw new Error("recordEvaluation `score` must be a finite number");const e=D(t.name),r={[`gen_ai.evaluation.${e}.score`]:t.score};return t.label!==void 0&&(r[`gen_ai.evaluation.${e}.label`]=t.label),r},_=t=>{const e=Object.keys(t);return e.length>0&&e.every(r=>r==="attributes"||r==="kind"||r==="links")},F=128,H=128,U=t=>{try{const e=new URL(t);return`${e.protocol}//${e.host}${e.pathname}`}catch{return t}},q=t=>{try{return new URL(t).host}catch{return t}},C=(t,e)=>{try{return t(new URL(e))}catch{return!1}},N=t=>t===void 0?{}:_(t)?t:{attributes:t},z=(t,e)=>{if(t.isTraced){t.setAttribute(b.functionPath,e.functionPath),t.setAttribute(b.ok,e.ok),t.setAttribute(b.durationMs,e.durationMs),e.shardKey!==void 0&&t.setAttribute(b.shardKey,e.shardKey),e.userId!==void 0&&t.setAttribute(b.userId,e.userId),e.error!==void 0&&(t.setAttribute(b.errorType,e.error.type),t.setAttribute(b.errorMessage,e.error.message));for(const[r,s]of Object.entries(e.attributes))(typeof s=="boolean"||typeof s=="number"||typeof s=="string")&&t.setAttribute(`lunora.attr.${r}`,s)}},B=(t,e=!1)=>{const r={attributes:{},events:[],links:[]},s={spanContext:()=>t,addEvent:(n,a)=>{if(r.events.length>=F)return;const o=g(a);r.events.push({...o===void 0?{}:{attributes:o},name:n,ts:Date.now()})},addLink:n=>{if(r.links.length>=H)return;const a=g(n.attributes);r.links.push({...a===void 0?{}:{attributes:a},spanId:n.spanId,traceId:n.traceId})},recordEvaluation:n=>{Object.assign(r.attributes,g(L(n)))},recordException:n=>{const a=n instanceof Error?n.message:String(n);s.addEvent("exception",{"exception.message":K(a,e),...e&&n instanceof Error&&typeof n.stack=="string"?{"exception.stacktrace":n.stack}:{},"exception.type":T(n)})},setAttribute:(n,a)=>{Object.assign(r.attributes,g({[n]:a}))},setAttributes:n=>{Object.assign(r.attributes,g(n))}};return{collected:r,handle:s}},Q=t=>{const{anchor:e,captureRaw:r=!1,fuseHostSpans:s,functionPath:n,record:a,resolveHostTracing:o,shardKey:i,userId:h}=t,u=p=>async(m,d,f)=>{const y=M(8),c=Date.now(),I=N(f),j=g(I.attributes),{collected:w,handle:O}=B({spanId:y,traceId:e.traceId},r),P=async v=>{let k=!0,S;try{return await d(u(y),O)}catch(l){k=!1;const E=l instanceof Error?l.message:String(l);throw S={message:K(E,r),type:T(l)},l}finally{const l=Date.now()-c,E=h(),A={...j,...w.attributes},$=[...I.links??[],...w.links];try{a({...Object.keys(A).length===0?{}:{attributes:A},durationMs:l,...w.events.length===0?{}:{events:w.events},...S===void 0?{}:{error:S},functionPath:n,...I.kind===void 0||I.kind==="internal"?{}:{kind:I.kind},...$.length===0?{}:{links:$},name:m,ok:k,parentSpanId:p,shardKey:i,spanId:y,startTs:c,traceId:e.traceId,userId:E})}catch{}if(v!==void 0)try{z(v,{attributes:A,durationMs:l,error:S,functionPath:n,ok:k,shardKey:i,userId:E})}catch{}}};if(s===!0&&o!==void 0){const v=await o();if(v!==void 0&&typeof v.enterSpan=="function")return await v.enterSpan(m,k=>P(k))}return await P()};return u(e.rootSpanId)},V=(t,e)=>{const{anchor:r,functionPath:s,propagate:n=!0,record:a,shardKey:o,userId:i}=t;return async(h,u)=>{const p=M(8),m=Date.now(),d=new Request(h,u);(typeof n=="function"?C(n,d.url):n)&&d.headers.set("traceparent",R(r.traceId,p,r.sampled??!0));let f,y;try{const c=await e(d);return y=c.status,c.ok||(f={message:`HTTP ${String(c.status)}`,type:`HTTP_${String(c.status)}`}),c}catch(c){throw f={message:c instanceof Error?c.message:String(c),type:T(c)},c}finally{try{a({attributes:{"http.request.method":d.method,...y===void 0?{}:{"http.response.status_code":y},"url.full":U(d.url)},durationMs:Date.now()-m,...f===void 0?{}:{error:f},functionPath:s,kind:"client",name:`${d.method} ${q(d.url)}`,ok:f===void 0,parentSpanId:r.rootSpanId,shardKey:o,spanId:p,startTs:m,traceId:r.traceId,userId:i()})}catch{}}}},W=t=>{const{functionPath:e,record:r,shardKey:s}=t,n=(a,o,i,h)=>{if(!Number.isFinite(i))return;const u=g(h);try{r({...u===void 0?{}:{attributes:u},functionPath:e,kind:a,name:o,shardKey:s,ts:Date.now(),value:i})}catch{}};return{count:(a,o=1,i)=>{n("counter",a,o,i)},gauge:(a,o,i)=>{n("gauge",a,o,i)},record:(a,o,i)=>{n("histogram",a,o,i)}}},X=t=>{const{anchor:e,captureRaw:r=!1,collected:s,durationMs:n,failure:a,functionPath:o,shardKey:i,startTs:h,userId:u}=t,p=s?.attributes??{};return{...Object.keys(p).length===0?{}:{attributes:p},dispatch:!0,durationMs:n,...s===void 0||s.events.length===0?{}:{events:s.events},...a===void 0?{}:{error:{message:K(a.thrown instanceof Error?a.thrown.message:String(a.thrown),r),type:T(a.thrown)}},functionPath:o,...s===void 0||s.links.length===0?{}:{links:s.links},name:o,ok:a===void 0,parentSpanId:"",shardKey:i,spanId:e.rootSpanId,startTs:h,traceId:e.traceId,userId:u}};export{z as applyHostSpanAttributes,W as createMetrics,B as createSpanCollector,V as createTracedFetch,Q as createTracer,X as dispatchRootSpan};
@@ -0,0 +1,31 @@
1
+ import{m as O,e as S}from"./metric-buffer-CdgXal7w.mjs";const a="__lunora_metric_history",o=6e4,x=1440,d=1e3,l=5e3,n=(e,s,...T)=>e.exec.call(e,s,...T),y=e=>Math.floor(e/o)*o,_=new WeakSet,k=e=>{_.has(e)||(n(e,`CREATE TABLE IF NOT EXISTS "${a}" (
2
+ series_key TEXT NOT NULL,
3
+ bucket_ms INTEGER NOT NULL,
4
+ name TEXT NOT NULL,
5
+ kind TEXT NOT NULL,
6
+ attrs TEXT NOT NULL DEFAULT '{}',
7
+ function_path TEXT NOT NULL DEFAULT '',
8
+ shard_key TEXT,
9
+ count INTEGER NOT NULL DEFAULT 0,
10
+ sum REAL NOT NULL DEFAULT 0,
11
+ min REAL NOT NULL DEFAULT 0,
12
+ max REAL NOT NULL DEFAULT 0,
13
+ last REAL NOT NULL DEFAULT 0,
14
+ last_ts REAL NOT NULL DEFAULT 0,
15
+ exemplar_trace TEXT,
16
+ PRIMARY KEY (series_key, bucket_ms)
17
+ )`),_.add(e))},R=4096,N=new WeakMap,M=e=>{let s=N.get(e);return s===void 0&&(s=new Set,N.set(e,s)),s},p=(e,s)=>n(e,`SELECT COUNT(DISTINCT series_key) AS n FROM "${a}"`).one().n<s,h=(e,s,T,i={})=>{const c=i.maxSeries??d,t=i.retentionBuckets??x;k(e);const r=O(s),E=y(s.ts),u=M(e),L=`${r}\0${E.toString()}`,m=u.has(L)||n(e,`SELECT 1 AS c FROM "${a}" WHERE series_key = ? AND bucket_ms = ? LIMIT 1`,r,E).toArray().length>0;if(!m&&!(n(e,`SELECT 1 AS c FROM "${a}" WHERE series_key = ? LIMIT 1`,r).toArray().length>0)&&!p(e,c))return;const A=T??null;n(e,`INSERT INTO "${a}"
18
+ (series_key, bucket_ms, name, kind, attrs, function_path, shard_key, count, sum, min, max, last, last_ts, exemplar_trace)
19
+ VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
20
+ ON CONFLICT(series_key, bucket_ms) DO UPDATE SET
21
+ count = count + 1,
22
+ sum = sum + excluded.sum,
23
+ min = MIN(min, excluded.min),
24
+ max = MAX(max, excluded.max),
25
+ last = excluded.last,
26
+ last_ts = excluded.last_ts,
27
+ exemplar_trace = CASE WHEN excluded.exemplar_trace IS NULL THEN exemplar_trace ELSE excluded.exemplar_trace END`,r,E,s.name,s.kind,S(s.attributes??{}),s.functionPath,s.shardKey??null,s.value,s.value,s.value,s.value,s.ts,A),m||n(e,`DELETE FROM "${a}"
28
+ WHERE series_key = ?
29
+ AND bucket_ms <= (
30
+ SELECT MAX(bucket_ms) - ? FROM "${a}" WHERE series_key = ?
31
+ )`,r,t*o,r),m&&!u.has(L)&&(u.size>=R&&u.clear(),u.add(L))},U=e=>{if(!(e===""||e==="{}"))try{const s=JSON.parse(e);return s!==null&&typeof s=="object"?s:void 0}catch{return}},D=(e,s={})=>{k(e);const T=s.maxSeries??d,i=s.sinceMs===void 0?n(e,`SELECT * FROM "${a}" ORDER BY bucket_ms DESC LIMIT ?`,l).toArray():n(e,`SELECT * FROM "${a}" WHERE bucket_ms >= ? ORDER BY bucket_ms DESC LIMIT ?`,s.sinceMs,l).toArray(),c=new Map;for(const t of i){let r=c.get(t.series_key);if(r===void 0){const E=U(t.attrs);r={...E===void 0?{}:{attributes:E},functionPath:t.function_path,kind:t.kind,name:t.name,points:[],...t.shard_key===null?{}:{shardKey:t.shard_key}},c.set(t.series_key,r)}r.points.push({bucketMs:t.bucket_ms,count:t.count,...t.exemplar_trace===null?{}:{exemplarTraceId:t.exemplar_trace},last:t.last,max:t.max,min:t.min,sum:t.sum})}for(const t of c.values())t.points.sort((r,E)=>r.bucketMs-E.bucketMs);return{capped:n(e,`SELECT COUNT(DISTINCT series_key) AS n FROM "${a}"`).one().n>=T,series:[...c.values()]}};export{D as readMetricHistory,h as recordMetricHistory};
@@ -0,0 +1,32 @@
1
+ const i="__lunora_metrics_queries",m="__lunora_metrics_queries_buckets",f=6e4,B=90,E=[1,2,5,10,25,50,100,250,500,1e3,5e3],R=512,I=500,o=(e,t,...r)=>e.exec.call(e,t,...r),k=e=>{let t=e.replaceAll(/'(?:[^']|'')*'/g,"?").replaceAll(/\b0x[\da-f]+\b/gi,"?").replaceAll(/(?<=[=,([\s])\d+(?:\.\d+)?/g,"?").replaceAll(/\s+/g," ").trim();return t.length>R&&(t=`${t.slice(0,R-1)}…`),t},U=new WeakSet,N=e=>{U.has(e)||(o(e,`CREATE TABLE IF NOT EXISTS "${i}" (
2
+ normalized_sql TEXT PRIMARY KEY,
3
+ exec_count INTEGER NOT NULL DEFAULT 0,
4
+ total_duration_ms REAL NOT NULL DEFAULT 0,
5
+ rows_read INTEGER NOT NULL DEFAULT 0,
6
+ rows_written INTEGER NOT NULL DEFAULT 0
7
+ )`),U.add(e))},M=new WeakMap,w=e=>Math.floor(e/6e4)*6e4,C=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.codePointAt(r)??0,t=Math.imul(t,16777619)>>>0;return t.toString(16).padStart(8,"0")},b=e=>{const t=E.findIndex(r=>e<=r);return t===-1?E.length:t},T=e=>`lat_${String(e)}`,h=new WeakSet,D=e=>{if(h.has(e))return;const t=Array.from({length:E.length+1},(r,n)=>`${T(n)} INTEGER NOT NULL DEFAULT 0`).join(", ");o(e,`CREATE TABLE IF NOT EXISTS "${m}" (
8
+ sql_hash TEXT NOT NULL,
9
+ bucket_ms INTEGER NOT NULL,
10
+ exec_count INTEGER NOT NULL DEFAULT 0,
11
+ total_duration_ms REAL NOT NULL DEFAULT 0,
12
+ rows_read INTEGER NOT NULL DEFAULT 0,
13
+ rows_written INTEGER NOT NULL DEFAULT 0,
14
+ ${t},
15
+ PRIMARY KEY (sql_hash, bucket_ms)
16
+ )`),h.add(e)},$=(e,t)=>{o(e,`DELETE FROM "${m}" WHERE bucket_ms < ?`,w(t)-90*6e4)},q=(e,t,r,n,u,_,s=1)=>{try{D(e);const c=s>0?r/s:r,l=T(b(c)),S=`INSERT INTO "${m}" (sql_hash, bucket_ms, exec_count, total_duration_ms, rows_read, rows_written, ${l})
17
+ VALUES (?, ?, ?, ?, ?, ?, ?)
18
+ ON CONFLICT(sql_hash, bucket_ms) DO UPDATE SET
19
+ exec_count = exec_count + excluded.exec_count,
20
+ total_duration_ms = total_duration_ms + excluded.total_duration_ms,
21
+ rows_read = rows_read + excluded.rows_read,
22
+ rows_written = rows_written + excluded.rows_written,
23
+ ${l} = ${l} + excluded.${l}`;o(e,S,C(t),w(_),s,r,n,u,s);const a=w(_);M.get(e)!==a&&(M.set(e,a),$(e,_))}catch{}},x=(e,t)=>{const r=e.reduce((_,s)=>_+s,0);if(r===0)return 0;const n=r*t;let u=0;for(const[_,s]of e.entries())if(u+=s,u>=n)return E[_]??E.at(-1)??0;return E.at(-1)??0},Y=(e,t,r=Date.now())=>{try{N(e),D(e)}catch{return{buckets:[],capped:!1,entries:[],trackedStatements:0}}const n=w(r-t),u=Array.from({length:E.length+1},(a,d)=>`SUM(${T(d)}) AS ${T(d)}`).join(", "),_=o(e,`SELECT sql_hash, SUM(exec_count) AS exec_count, SUM(total_duration_ms) AS total_duration_ms,
24
+ SUM(rows_read) AS rows_read, SUM(rows_written) AS rows_written, ${u}
25
+ FROM "${m}" WHERE bucket_ms >= ? GROUP BY sql_hash`,n).toArray(),s=new Map;for(const a of o(e,`SELECT normalized_sql FROM "${i}"`))s.set(C(a.normalized_sql),a.normalized_sql);const c=_.map(a=>{const d=Array.from({length:E.length+1},(y,g)=>Number(a[T(g)]??0)),L=Number(a.exec_count??0),A=Number(a.total_duration_ms??0);return{avgDurationMs:L>0?A/L:0,execCount:L,normalizedSql:s.get(String(a.sql_hash))??String(a.sql_hash),p50DurationMs:x(d,.5),p95DurationMs:x(d,.95),rowsRead:Number(a.rows_read??0),rowsWritten:Number(a.rows_written??0),totalDurationMs:A}});c.sort((a,d)=>d.totalDurationMs-a.totalDurationMs);const l=o(e,`SELECT bucket_ms, SUM(exec_count) AS exec_count, SUM(total_duration_ms) AS total_duration_ms
26
+ FROM "${m}" WHERE bucket_ms >= ? GROUP BY bucket_ms ORDER BY bucket_ms ASC`,n).toArray().map(a=>({avgDurationMs:a.exec_count>0?a.total_duration_ms/a.exec_count:0,bucketMs:a.bucket_ms,execCount:a.exec_count})),S=o(e,`SELECT COUNT(*) AS n FROM "${i}"`).one().n;return{buckets:l,capped:S>=I,entries:c,trackedStatements:S}},O=new WeakMap,F=e=>{let t=O.get(e);return t===void 0&&(t=new Set,O.set(e,t)),t},p=(e,t)=>{const r=F(e);return r.has(t)?!0:o(e,`SELECT 1 AS c FROM "${i}" WHERE normalized_sql = ? LIMIT 1`,t).toArray().length>0?(r.add(t),!0):o(e,`SELECT COUNT(*) AS n FROM "${i}"`).one().n>=I?!1:(r.add(t),!0)},Q=(e,t,r,n,u,_=Date.now(),s=1)=>{const c=k(t);if(c.length===0||(N(e),!p(e,c)))return;const l=`INSERT INTO "${i}" (normalized_sql, exec_count, total_duration_ms, rows_read, rows_written)
27
+ VALUES (?, ?, ?, ?, ?)
28
+ ON CONFLICT(normalized_sql) DO UPDATE SET
29
+ exec_count = exec_count + excluded.exec_count,
30
+ total_duration_ms = total_duration_ms + excluded.total_duration_ms,
31
+ rows_read = rows_read + excluded.rows_read,
32
+ rows_written = rows_written + excluded.rows_written`;o(e,l,c,s,r,n,u),q(e,c,r,n,u,_,s)},z=e=>(N(e),o(e,`SELECT normalized_sql, exec_count, total_duration_ms, rows_read, rows_written FROM "${i}" ORDER BY total_duration_ms DESC`).toArray().map(t=>({execCount:t.exec_count,normalizedSql:t.normalized_sql,rowsRead:t.rows_read,rowsWritten:t.rows_written,totalDurationMs:t.total_duration_ms})));export{E as LATENCY_BUCKET_EDGES,m as QUERY_BUCKETS_TABLE,f as QUERY_BUCKET_MS,B as QUERY_BUCKET_RETENTION,R as QUERY_METRICS_MAX_SQL_LEN,I as QUERY_METRICS_MAX_STATEMENTS,i as QUERY_METRICS_TABLE,D as ensureQueryBucketsTable,N as ensureQueryMetricsTable,C as hashStatement,b as latencyBucketIndex,k as normalizeSql,x as percentileFrom,$ as pruneQueryBuckets,Y as readQueryInsights,z as readQueryMetrics,q as recordQueryBucket,Q as recordQueryMetric};
@@ -0,0 +1,21 @@
1
+ import{fingerprintError as I}from"@lunora/fingerprint";import{redact as L,standardRules as R}from"@visulima/redact";import{readIssueStates as M}from"./ISSUE_SEVERITIES-Js6lGUG5.mjs";const P=e=>{if(typeof e=="string")return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}},m=e=>typeof e=="boolean"||typeof e=="number"||typeof e=="string"?e:P(e),v=(e,r)=>{if(e===void 0&&r===void 0)return;const o={};if(r!==void 0)for(const[t,s]of Object.entries(r))o[t]=m(s);if(e!==void 0)for(const[t,s]of Object.entries(e))o[t]=m(s);return Object.keys(o).length===0?void 0:o},d="__lunora_reqlog__",E=1e3,b="lunora",h=(e,r,...o)=>e.exec.call(e,r,...o),c=(e,r=!1)=>r||e===null||e===void 0?e:L(e,R),y=e=>{h(e,`CREATE TABLE IF NOT EXISTS "${d}" (
2
+ seq INTEGER PRIMARY KEY AUTOINCREMENT,
3
+ ts REAL NOT NULL,
4
+ function_path TEXT NOT NULL,
5
+ shard_key TEXT,
6
+ user_id TEXT,
7
+ identity TEXT,
8
+ args TEXT,
9
+ outcome TEXT NOT NULL,
10
+ error_message TEXT,
11
+ error_fingerprint TEXT,
12
+ duration_ms REAL NOT NULL,
13
+ tables_read TEXT NOT NULL DEFAULT '[]',
14
+ tables_written TEXT NOT NULL DEFAULT '[]',
15
+ cache_hit INTEGER,
16
+ subscriptions_rerun INTEGER NOT NULL DEFAULT 0
17
+ )`);try{h(e,`ALTER TABLE "${d}" ADD COLUMN error_fingerprint TEXT`)}catch{}},S=e=>JSON.stringify([...new Set(e)].toSorted((r,o)=>r.localeCompare(o))),w=e=>e===void 0?null:e?1:0,x=(e,r,o={})=>{y(e);const t=o.captureRaw??!1,s=o.retention??E,i=r.outcome==="error"&&r.errorMessage!==void 0?I({functionPath:r.functionPath,message:r.errorMessage}).hash:void 0;h(e,`INSERT INTO "${d}"
18
+ (ts, function_path, shard_key, user_id, identity, args, outcome, error_message, error_fingerprint, duration_ms, tables_read, tables_written, cache_hit, subscriptions_rerun)
19
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,r.ts,r.functionPath,r.shardKey??null,r.userId??null,r.identity===void 0?null:JSON.stringify(c(r.identity,t)),r.redactedArgs===void 0?null:JSON.stringify(c(r.redactedArgs,t)),r.outcome,r.errorMessage===void 0?null:c(r.errorMessage,t),i??null,r.durationMs,S(r.tablesRead),S(r.tablesWritten),w(r.cacheHit),r.subscriptionsReRun??0),h(e,`DELETE FROM "${d}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${d}")`,s)},D=(e,r={})=>{const o=r.captureRaw??!1,t={args:e.redactedArgs===void 0?void 0:c(e.redactedArgs,o),cacheHit:e.cacheHit,durationMs:e.durationMs,error:e.errorMessage===void 0?void 0:c(e.errorMessage,o),function:e.functionPath,identity:e.identity===void 0?void 0:c(e.identity,o),outcome:e.outcome,shard:e.shardKey,source:b,tablesRead:e.tablesRead??[],tablesWritten:e.tablesWritten??[],ts:e.ts,type:"request",userId:e.userId},s=JSON.stringify(t);e.outcome==="error"?console.error(s):console.log(s)},K="log",U=e=>e.map(r=>{if(typeof r=="string")return r;try{return JSON.stringify(r)??String(r)}catch{return String(r)}}).join(" "),C=e=>{if(typeof e!="object"||e===null||Array.isArray(e))return!1;const r=Object.getPrototypeOf(e);return r===Object.prototype||r===null},J=(e,r)=>e.length===2&&typeof e[0]=="string"&&C(e[1])?{fields:v(e[1],r),message:e[0]}:{fields:v(void 0,r),message:U(e)},k=(e,r={})=>{const o=r.captureRaw??!1,t={fields:e.fields===void 0?void 0:c(e.fields,o),function:e.functionPath,level:e.level,message:e.message,shard:e.shardKey,source:b,spanId:e.spanId,traceId:e.traceId,ts:e.ts,type:K,userId:e.userId};let s;try{s=JSON.stringify(t)}catch{s=JSON.stringify({...t,fields:void 0})}e.level==="error"||e.level==="fatal"?console.error(s):e.level==="warn"?console.warn(s):console.log(s)},g=e=>e.replaceAll(/[\\%_]/g,r=>`\\${r}`),N=e=>{try{const r=JSON.parse(e);return Array.isArray(r)?r.filter(o=>typeof o=="string"):[]}catch{return[]}},j=(e,r={})=>{y(e);const o=Math.max(1,Math.min(r.limit??E,1e4)),t=["seq > ?"],s=[r.sinceSeq??0];if(r.functionPathPrefix!==void 0&&r.functionPathPrefix!==""&&(t.push(String.raw`function_path LIKE ? ESCAPE '\'`),s.push(`${g(r.functionPathPrefix)}%`)),r.userId!==void 0&&r.userId!==""&&(t.push("user_id = ?"),s.push(r.userId)),r.shardKey!==void 0&&r.shardKey!==""&&(t.push("shard_key = ?"),s.push(r.shardKey)),r.outcome!==void 0&&(t.push("outcome = ?"),s.push(r.outcome)),r.tableTouched!==void 0&&r.tableTouched!==""){const i=`%${g(JSON.stringify(r.tableTouched))}%`;t.push(String.raw`(tables_read LIKE ? ESCAPE '\' OR tables_written LIKE ? ESCAPE '\')`),s.push(i,i)}return s.push(o),h(e,`SELECT seq, ts, function_path, shard_key, user_id, identity, args, outcome, error_message, duration_ms, tables_read, tables_written, cache_hit, subscriptions_rerun
20
+ FROM "${d}" WHERE ${t.join(" AND ")} ORDER BY seq DESC LIMIT ?`,...s).toArray().map(i=>{const n={durationMs:i.duration_ms,functionPath:i.function_path,outcome:i.outcome==="error"?"error":"ok",seq:i.seq,subscriptionsReRun:i.subscriptions_rerun,tablesRead:N(i.tables_read),tablesWritten:N(i.tables_written),ts:i.ts};return i.shard_key!==null&&(n.shardKey=i.shard_key),i.user_id!==null&&(n.userId=i.user_id),i.identity!==null&&(n.identity=JSON.parse(i.identity)),i.args!==null&&(n.redactedArgs=JSON.parse(i.args)),i.error_message!==null&&(n.errorMessage=i.error_message),i.cache_hit!==null&&(n.cacheHit=i.cache_hit===1),n})},q=(e,r)=>{const o=M(e,[...r.keys()]);for(const t of r.values()){const s=o.get(t.hash);s!==void 0&&(t.stateUpdatedAt=s.updatedAt,s.assignee!==void 0&&(t.assignee=s.assignee),s.severity!==void 0&&(t.severity=s.severity),t.status=s.status==="resolved"&&t.lastSeen>s.updatedAt?"open":s.status)}},W=(e,r={})=>{y(e);const o=Math.max(1,Math.min(r.limit??E,1e4)),t=["outcome = 'error'"],s=[];r.functionPathPrefix!==void 0&&r.functionPathPrefix!==""&&(t.push(String.raw`function_path LIKE ? ESCAPE '\'`),s.push(`${g(r.functionPathPrefix)}%`)),r.userId!==void 0&&r.userId!==""&&(t.push("user_id = ?"),s.push(r.userId)),r.shardKey!==void 0&&r.shardKey!==""&&(t.push("shard_key = ?"),s.push(r.shardKey)),s.push(o);const i=h(e,`SELECT function_path, error_message, error_fingerprint, ts
21
+ FROM "${d}" WHERE ${t.join(" AND ")} ORDER BY seq DESC LIMIT ?`,...s).toArray(),n=new Map,p=new Map;for(const a of i){const f=a.error_message??"",{culprit:O,hash:A,title:T}=I({functionPath:a.function_path,message:f}),l=a.error_fingerprint??A,u=n.get(l);if(u===void 0){n.set(l,{count:1,culprit:O,firstSeen:a.ts,hash:l,lastSeen:a.ts,sampleMessage:f,status:"open",title:T}),p.set(l,a.ts);continue}u.count+=1,u.firstSeen=Math.min(u.firstSeen,a.ts),u.lastSeen=Math.max(u.lastSeen,a.ts),a.ts>(p.get(l)??Number.NEGATIVE_INFINITY)&&(p.set(l,a.ts),u.sampleMessage=f,u.title=T)}q(e,n);const _=[...n.values()];return(r.status===void 0?_:_.filter(a=>a.status===r.status)).toSorted((a,f)=>f.lastSeen-a.lastSeen)};export{J as C,E,k as K,j as U,W,c,d,x as k,y as m,v as n,D as w,U as y};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/observability",
3
- "version": "1.0.0-alpha.4",
3
+ "version": "1.0.0-alpha.5",
4
4
  "description": "Host-neutral telemetry storage and read models for Lunora, backing the Studio's Logs, Traces, Metrics and Issues views",
5
5
  "keywords": [
6
6
  "lunora",
@@ -43,9 +43,9 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@lunora/errors": "1.0.0-alpha.10",
46
+ "@lunora/errors": "1.0.0-alpha.12",
47
47
  "@lunora/fingerprint": "1.0.0-alpha.4",
48
- "@lunora/shard-engine": "1.0.0-alpha.4",
48
+ "@lunora/shard-engine": "1.0.0-alpha.6",
49
49
  "@visulima/redact": "3.0.0"
50
50
  },
51
51
  "engines": {
@@ -1,56 +0,0 @@
1
- const n="__lunora_metrics",_="__lunora_metrics_buckets",c="__lunora_metrics_scans",N="__lunora_metrics_index",I=6e4,R=1440,O=5e3,C=1e3,t=(a,s,...e)=>a.exec.call(a,s,...e),u=a=>Math.floor(a/6e4)*6e4,S=a=>{const s=new Map;for(const e of a)s.set(`${e.table}\0${e.index}`,{index:e.index,table:e.table});return[...s.values()]},l=a=>{t(a,`CREATE TABLE IF NOT EXISTS "${n}" (
2
- path TEXT PRIMARY KEY,
3
- calls INTEGER NOT NULL DEFAULT 0,
4
- errors INTEGER NOT NULL DEFAULT 0,
5
- conflicts INTEGER NOT NULL DEFAULT 0,
6
- scans INTEGER NOT NULL DEFAULT 0,
7
- total_duration_ms REAL NOT NULL DEFAULT 0,
8
- min_duration_ms REAL,
9
- max_duration_ms REAL NOT NULL DEFAULT 0,
10
- last_called_at REAL NOT NULL DEFAULT 0,
11
- last_error_at REAL,
12
- last_error_message TEXT
13
- )`);for(const s of["scans","conflicts"])try{t(a,`ALTER TABLE "${n}" ADD COLUMN ${s} INTEGER NOT NULL DEFAULT 0`)}catch{}t(a,`CREATE TABLE IF NOT EXISTS "${_}" (
14
- path TEXT NOT NULL,
15
- bucket_ms INTEGER NOT NULL,
16
- calls INTEGER NOT NULL DEFAULT 0,
17
- errors INTEGER NOT NULL DEFAULT 0,
18
- PRIMARY KEY (path, bucket_ms)
19
- )`),t(a,`CREATE TABLE IF NOT EXISTS "${c}" (
20
- path TEXT NOT NULL,
21
- table_name TEXT NOT NULL,
22
- scans INTEGER NOT NULL DEFAULT 0,
23
- PRIMARY KEY (path, table_name)
24
- )`),t(a,`CREATE TABLE IF NOT EXISTS "${N}" (
25
- table_name TEXT NOT NULL,
26
- index_name TEXT NOT NULL,
27
- reads INTEGER NOT NULL DEFAULT 0,
28
- PRIMARY KEY (table_name, index_name)
29
- )`)},U=(a,s)=>{if(l(a),t(a,`SELECT COUNT(*) AS n FROM "${n}"`).one().n>=5e3&&t(a,`SELECT COUNT(*) AS c FROM "${n}" WHERE path = ?`,s.path).one().c===0)return;const e=s.scannedTables?[...new Set(s.scannedTables)]:[],r=e.length,E=S(s.indexHits??[]),T=s.errored?1:0,L=s.conflicted?1:0,d=s.errored?s.ts:null,i=s.errored?s.errorMessage??null:null;t(a,`INSERT INTO "${n}"
30
- (path, calls, errors, conflicts, scans, total_duration_ms, min_duration_ms, max_duration_ms, last_called_at, last_error_at, last_error_message)
31
- VALUES (?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)
32
- ON CONFLICT(path) DO UPDATE SET
33
- calls = calls + 1,
34
- errors = errors + excluded.errors,
35
- conflicts = conflicts + excluded.conflicts,
36
- scans = scans + excluded.scans,
37
- total_duration_ms = total_duration_ms + excluded.total_duration_ms,
38
- min_duration_ms = MIN(COALESCE(min_duration_ms, excluded.min_duration_ms), excluded.min_duration_ms),
39
- max_duration_ms = MAX(max_duration_ms, excluded.max_duration_ms),
40
- last_called_at = excluded.last_called_at,
41
- last_error_at = CASE WHEN excluded.last_error_at IS NULL THEN last_error_at ELSE excluded.last_error_at END,
42
- last_error_message = CASE WHEN excluded.last_error_at IS NULL THEN last_error_message ELSE excluded.last_error_message END`,s.path,T,L,r,s.durationMs,s.durationMs,s.durationMs,s.ts,d,i);const m=u(s.ts);t(a,`INSERT INTO "${_}" (path, bucket_ms, calls, errors)
43
- VALUES (?, ?, 1, ?)
44
- ON CONFLICT(path, bucket_ms) DO UPDATE SET
45
- calls = calls + 1,
46
- errors = errors + excluded.errors`,s.path,m,T),t(a,`DELETE FROM "${_}"
47
- WHERE path = ?
48
- AND bucket_ms <= (
49
- SELECT MAX(bucket_ms) - ? FROM "${_}" WHERE path = ?
50
- )`,s.path,864e5,s.path);for(const o of e)t(a,`INSERT INTO "${c}" (path, table_name, scans)
51
- VALUES (?, ?, 1)
52
- ON CONFLICT(path, table_name) DO UPDATE SET
53
- scans = scans + 1`,s.path,o);for(const o of E)t(a,`INSERT INTO "${N}" (table_name, index_name, reads)
54
- VALUES (?, ?, 1)
55
- ON CONFLICT(table_name, index_name) DO UPDATE SET
56
- reads = reads + 1`,o.table,o.index)},A=a=>{l(a);const s=t(a,`SELECT path, table_name, scans FROM "${c}" ORDER BY scans DESC, path ASC, table_name ASC LIMIT ${String(1e3)}`).toArray(),e=new Map;for(const r of s){const E=e.get(r.path),T={scans:r.scans,table:r.table_name};E===void 0?e.set(r.path,[T]):E.push(T)}return e},M=a=>(l(a),t(a,`SELECT table_name, index_name, reads FROM "${N}" ORDER BY table_name ASC, index_name ASC LIMIT ${String(1e3)}`).toArray().map(s=>({index:s.index_name,reads:s.reads,table:s.table_name}))),F=(a,s)=>{for(const e of s){const r=a.find(E=>E.table===e);r===void 0?a.push({scans:1,table:e}):r.scans+=1}return a.sort((e,r)=>r.scans-e.scans||e.table.localeCompare(r.table)),a},b=a=>{l(a);const s=A(a);return t(a,`SELECT * FROM "${n}" ORDER BY last_called_at DESC LIMIT ${String(1e3)}`).toArray().map(e=>({calls:e.calls,conflicts:e.conflicts,errors:e.errors,lastCalledAt:e.last_called_at,lastErrorAt:e.last_error_at,lastErrorMessage:e.last_error_message,maxDurationMs:e.max_duration_ms,path:e.path,scannedTables:s.get(e.path)??[],scans:e.scans,totalDurationMs:e.total_duration_ms}))},p=(a,s)=>(l(a),(s===void 0?t(a,`SELECT path, bucket_ms, calls, errors FROM "${_}" ORDER BY bucket_ms DESC, path ASC LIMIT ${String(1e3)}`).toArray():t(a,`SELECT path, bucket_ms, calls, errors FROM "${_}" WHERE path = ? ORDER BY bucket_ms DESC LIMIT ${String(1e3)}`,s).toArray()).toReversed().map(e=>({bucketMs:e.bucket_ms,calls:e.calls,errors:e.errors,path:e.path}))),h=a=>{l(a);const s=t(a,`SELECT SUM(calls) AS requests, SUM(errors) AS errors FROM "${n}"`).one();return{errors:s.errors??0,requests:s.requests??0}};export{_ as FUNCTION_METRICS_BUCKETS_TABLE,I as FUNCTION_METRICS_BUCKET_MS,R as FUNCTION_METRICS_BUCKET_RETENTION,N as FUNCTION_METRICS_INDEX_TABLE,O as FUNCTION_METRICS_MAX_PATHS,C as FUNCTION_METRICS_READ_LIMIT,c as FUNCTION_METRICS_SCANS_TABLE,n as FUNCTION_METRICS_TABLE,l as ensureFunctionMetricsTables,F as mergeScanAttribution,p as readFunctionMetricBuckets,M as readFunctionMetricIndexHits,A as readFunctionMetricScans,b as readFunctionMetrics,h as readFunctionMetricsTotals,U as recordFunctionMetric};
@@ -1,20 +0,0 @@
1
- import{fingerprintError as R}from"@lunora/fingerprint";import{redact as v,standardRules as A}from"@visulima/redact";import{n as m}from"./log-fields-d5ouN1Pe.mjs";import{readIssueStates as O}from"./ISSUE_SEVERITIES-Js6lGUG5.mjs";const c="__lunora_reqlog__",f=1e3,I="lunora",h=(s,e,...a)=>s.exec.call(s,e,...a),E=(s,e=!1)=>e||s===null||s===void 0?s:v(s,A),_=s=>{h(s,`CREATE TABLE IF NOT EXISTS "${c}" (
2
- seq INTEGER PRIMARY KEY AUTOINCREMENT,
3
- ts REAL NOT NULL,
4
- function_path TEXT NOT NULL,
5
- shard_key TEXT,
6
- user_id TEXT,
7
- identity TEXT,
8
- args TEXT,
9
- outcome TEXT NOT NULL,
10
- error_message TEXT,
11
- duration_ms REAL NOT NULL,
12
- tables_read TEXT NOT NULL DEFAULT '[]',
13
- tables_written TEXT NOT NULL DEFAULT '[]',
14
- cache_hit INTEGER,
15
- subscriptions_rerun INTEGER NOT NULL DEFAULT 0
16
- )`)},S=s=>JSON.stringify([...new Set(s)].toSorted((e,a)=>e.localeCompare(a))),b=s=>s===void 0?null:s?1:0,$=(s,e,a={})=>{_(s);const i=a.captureRaw??!1,t=a.retention??f;h(s,`INSERT INTO "${c}"
17
- (ts, function_path, shard_key, user_id, identity, args, outcome, error_message, duration_ms, tables_read, tables_written, cache_hit, subscriptions_rerun)
18
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,e.ts,e.functionPath,e.shardKey??null,e.userId??null,e.identity===void 0?null:JSON.stringify(E(e.identity,i)),e.redactedArgs===void 0?null:JSON.stringify(E(e.redactedArgs,i)),e.outcome,e.errorMessage??null,e.durationMs,S(e.tablesRead),S(e.tablesWritten),b(e.cacheHit),e.subscriptionsReRun??0),h(s,`DELETE FROM "${c}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${c}")`,t)},F=(s,e={})=>{const a=e.captureRaw??!1,i={args:s.redactedArgs===void 0?void 0:E(s.redactedArgs,a),cacheHit:s.cacheHit,durationMs:s.durationMs,error:s.errorMessage,function:s.functionPath,identity:s.identity===void 0?void 0:E(s.identity,a),outcome:s.outcome,shard:s.shardKey,source:I,tablesRead:s.tablesRead??[],tablesWritten:s.tablesWritten??[],ts:s.ts,type:"request",userId:s.userId},t=JSON.stringify(i);s.outcome==="error"?console.error(t):console.log(t)},M="log",P=s=>s.map(e=>{if(typeof e=="string")return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}}).join(" "),q=s=>{if(typeof s!="object"||s===null||Array.isArray(s))return!1;const e=Object.getPrototypeOf(s);return e===Object.prototype||e===null},J=(s,e)=>s.length===2&&typeof s[0]=="string"&&q(s[1])?{fields:m(s[1],e),message:s[0]}:{fields:m(void 0,e),message:P(s)},X=s=>{const e={fields:s.fields,function:s.functionPath,level:s.level,message:s.message,shard:s.shardKey,source:I,spanId:s.spanId,traceId:s.traceId,ts:s.ts,type:M,userId:s.userId};let a;try{a=JSON.stringify(e)}catch{a=JSON.stringify({...e,fields:void 0})}s.level==="error"||s.level==="fatal"?console.error(a):s.level==="warn"?console.warn(a):console.log(a)},g=s=>s.replaceAll(/[\\%_]/g,e=>`\\${e}`),L=s=>{try{const e=JSON.parse(s);return Array.isArray(e)?e.filter(a=>typeof a=="string"):[]}catch{return[]}},k=(s,e={})=>{_(s);const a=Math.max(1,Math.min(e.limit??f,1e4)),i=["seq > ?"],t=[e.sinceSeq??0];if(e.functionPathPrefix!==void 0&&e.functionPathPrefix!==""&&(i.push(String.raw`function_path LIKE ? ESCAPE '\'`),t.push(`${g(e.functionPathPrefix)}%`)),e.userId!==void 0&&e.userId!==""&&(i.push("user_id = ?"),t.push(e.userId)),e.shardKey!==void 0&&e.shardKey!==""&&(i.push("shard_key = ?"),t.push(e.shardKey)),e.outcome!==void 0&&(i.push("outcome = ?"),t.push(e.outcome)),e.tableTouched!==void 0&&e.tableTouched!==""){const r=`%${g(JSON.stringify(e.tableTouched))}%`;i.push(String.raw`(tables_read LIKE ? ESCAPE '\' OR tables_written LIKE ? ESCAPE '\')`),t.push(r,r)}return t.push(a),h(s,`SELECT seq, ts, function_path, shard_key, user_id, identity, args, outcome, error_message, duration_ms, tables_read, tables_written, cache_hit, subscriptions_rerun
19
- FROM "${c}" WHERE ${i.join(" AND ")} ORDER BY seq DESC LIMIT ?`,...t).toArray().map(r=>{const n={durationMs:r.duration_ms,functionPath:r.function_path,outcome:r.outcome==="error"?"error":"ok",seq:r.seq,subscriptionsReRun:r.subscriptions_rerun,tablesRead:L(r.tables_read),tablesWritten:L(r.tables_written),ts:r.ts};return r.shard_key!==null&&(n.shardKey=r.shard_key),r.user_id!==null&&(n.userId=r.user_id),r.identity!==null&&(n.identity=JSON.parse(r.identity)),r.args!==null&&(n.redactedArgs=JSON.parse(r.args)),r.error_message!==null&&(n.errorMessage=r.error_message),r.cache_hit!==null&&(n.cacheHit=r.cache_hit===1),n})},w=(s,e)=>{const a=O(s,[...e.keys()]);for(const i of e.values()){const t=a.get(i.hash);t!==void 0&&(i.stateUpdatedAt=t.updatedAt,t.assignee!==void 0&&(i.assignee=t.assignee),t.severity!==void 0&&(i.severity=t.severity),i.status=t.status==="resolved"&&i.lastSeen>t.updatedAt?"open":t.status)}},D=(s,e={})=>{_(s);const a=Math.max(1,Math.min(e.limit??f,1e4)),i=["outcome = 'error'"],t=[];e.functionPathPrefix!==void 0&&e.functionPathPrefix!==""&&(i.push(String.raw`function_path LIKE ? ESCAPE '\'`),t.push(`${g(e.functionPathPrefix)}%`)),e.userId!==void 0&&e.userId!==""&&(i.push("user_id = ?"),t.push(e.userId)),e.shardKey!==void 0&&e.shardKey!==""&&(i.push("shard_key = ?"),t.push(e.shardKey)),t.push(a);const r=h(s,`SELECT function_path, error_message, ts
20
- FROM "${c}" WHERE ${i.join(" AND ")} ORDER BY seq DESC LIMIT ?`,...t).toArray(),n=new Map,p=new Map;for(const o of r){const l=o.error_message??"",{culprit:N,hash:d,title:T}=R({functionPath:o.function_path,message:l}),u=n.get(d);if(u===void 0){n.set(d,{count:1,culprit:N,firstSeen:o.ts,hash:d,lastSeen:o.ts,sampleMessage:l,status:"open",title:T}),p.set(d,o.ts);continue}u.count+=1,u.firstSeen=Math.min(u.firstSeen,o.ts),u.lastSeen=Math.max(u.lastSeen,o.ts),o.ts>(p.get(d)??Number.NEGATIVE_INFINITY)&&(p.set(d,o.ts),u.sampleMessage=l,u.title=T)}w(s,n);const y=[...n.values()];return(e.status===void 0?y:y.filter(o=>o.status===e.status)).toSorted((o,l)=>l.lastSeen-o.lastSeen)};export{f as REQUEST_LOG_RETENTION,c as REQUEST_LOG_TABLE,$ as appendRequestLogEntry,X as emitLogEvent,F as emitRequestLogEvent,_ as ensureRequestLogTable,J as parseLogArgs,D as readErrorIssues,k as readRequestLog,E as redactArgs,P as renderLogMessage};
@@ -1 +0,0 @@
1
- import{O as h,t as m}from"./trace-context-DrdF960P.mjs";const b=100,M=new Set(["aggregate","count","delete","deleteMany","deleteWhere","findFirst","findFirstOrThrow","findMany","get","groupBy","insert","insertMany","insertManyUnsafe","lookupById","patch","patchMany","patchWhere","rank","rankBefore","rankPage","rankPageRows","replace","restore"]),w=new Set(["aggregate","count","deleteWhere","findFirst","findFirstOrThrow","findMany","groupBy","insert","insertMany","insertManyUnsafe","patchWhere","rank","rankBefore","rankPage","rankPageRows"]),k=(t,e)=>{if(!w.has(t))return;const r=e[0];return typeof r=="string"&&r.length>0?r:void 0},T=t=>t instanceof Error?t.message:typeof t=="string"?t:JSON.stringify(t),I=t=>{const{deps:e,durationMs:r,failure:n,operation:s,startTs:a,table:o}=t;return{attributes:{"db.operation.name":s,...o===void 0?{}:{"db.collection.name":o},"db.system.name":"sqlite"},durationMs:r,...n===void 0?{}:{error:{message:T(n),type:m(n)}},functionPath:e.functionPath,kind:"client",name:o===void 0?`db.${s}`:`db.${s} ${o}`,ok:n===void 0,parentSpanId:e.anchor.rootSpanId,shardKey:e.shardKey,spanId:h(8),startTs:a,traceId:e.anchor.traceId,userId:e.userId()}},v=(t,e)=>{if(e.mode==="off")return t;const{tally:r}=e,n=new Map;return new Proxy(t,{get(s,a,o){const d=Reflect.get(s,a,o);if(typeof a!="string"||typeof d!="function"||!M.has(a))return d;const p=n.get(a);if(p!==void 0)return p;const y=d,u=async(...f)=>{const l=Date.now(),g=k(a,f);let c;try{return await y.apply(s,f)}catch(i){throw c=i,i}finally{const i=Date.now()-l;r.calls+=1,r.durationMs+=i,r.perOperation[a]=(r.perOperation[a]??0)+1,c!==void 0&&(r.errors+=1);try{e.mode==="spans"&&(r.spansEmitted>=b?r.spansTruncated=!0:(r.spansEmitted+=1,e.record(I({deps:e,durationMs:i,failure:c,operation:a,startTs:l,table:g}))))}catch{}}};return n.set(a,u),u}})},P=()=>({calls:0,durationMs:0,errors:0,perOperation:{},spansEmitted:0,spansTruncated:!1}),E=t=>{const e={"db.calls":t.calls,"db.duration_ms":t.durationMs};t.errors>0&&(e["db.errors"]=t.errors),t.spansTruncated&&(e["db.spans_truncated"]=!0);for(const[r,n]of Object.entries(t.perOperation))e[`db.op.${r}`]=n;return e};export{P as createDatabaseTally,E as formatTally,v as instrumentDatabase};
@@ -1 +0,0 @@
1
- import{n as y}from"./log-fields-d5ouN1Pe.mjs";import{w as f,O as A,m as O,t as S}from"./trace-context-DrdF960P.mjs";const j=/[\w.-]/u,x=e=>{let t="";for(const n of e)t+=j.test(n)?n:"_";return t},D=e=>{if(typeof e.name!="string"||e.name.length===0)throw new Error("recordEvaluation requires a non-empty `name`");if(typeof e.score!="number"||!Number.isFinite(e.score))throw new Error("recordEvaluation `score` must be a finite number");const t=x(e.name),n={[`gen_ai.evaluation.${t}.score`]:e.score};return e.label!==void 0&&(n[`gen_ai.evaluation.${t}.label`]=e.label),n},H=e=>{const t=Object.keys(e);return t.length>0&&t.every(n=>n==="attributes"||n==="kind"||n==="links")},L=128,_=128,F=e=>{try{const t=new URL(e);return`${t.protocol}//${t.host}${t.pathname}`}catch{return e}},R=e=>{try{return new URL(e).host}catch{return e}},U=(e,t)=>{try{return e(new URL(t))}catch{return!1}},q=e=>e===void 0?{}:H(e)?e:{attributes:e},C=(e,t)=>{if(e.isTraced){e.setAttribute(f.functionPath,t.functionPath),e.setAttribute(f.ok,t.ok),e.setAttribute(f.durationMs,t.durationMs),t.shardKey!==void 0&&e.setAttribute(f.shardKey,t.shardKey),t.userId!==void 0&&e.setAttribute(f.userId,t.userId),t.error!==void 0&&(e.setAttribute(f.errorType,t.error.type),e.setAttribute(f.errorMessage,t.error.message));for(const[n,r]of Object.entries(t.attributes))(typeof r=="boolean"||typeof r=="number"||typeof r=="string")&&e.setAttribute(`lunora.attr.${n}`,r)}},N=e=>{const t={attributes:{},events:[],links:[]},n={spanContext:()=>e,addEvent:(r,a)=>{if(t.events.length>=L)return;const s=y(a);t.events.push({...s===void 0?{}:{attributes:s},name:r,ts:Date.now()})},addLink:r=>{if(t.links.length>=_)return;const a=y(r.attributes);t.links.push({...a===void 0?{}:{attributes:a},spanId:r.spanId,traceId:r.traceId})},recordEvaluation:r=>{Object.assign(t.attributes,y(D(r)))},recordException:r=>{n.addEvent("exception",{"exception.message":r instanceof Error?r.message:String(r),...r instanceof Error&&typeof r.stack=="string"?{"exception.stacktrace":r.stack}:{},"exception.type":S(r)})},setAttribute:(r,a)=>{Object.assign(t.attributes,y({[r]:a}))},setAttributes:r=>{Object.assign(t.attributes,y(r))}};return{collected:t,handle:n}},G=e=>{const{anchor:t,fuseHostSpans:n,functionPath:r,record:a,resolveHostTracing:s,shardKey:i,userId:c}=e,h=u=>async(v,I,d)=>{const l=A(8),b=Date.now(),o=q(d),$=y(o.attributes),{collected:k,handle:M}=N({spanId:l,traceId:t.traceId}),T=async g=>{let m=!0,w;try{return await I(h(l),M)}catch(p){throw m=!1,w={message:p instanceof Error?p.message:String(p),type:S(p)},p}finally{const p=Date.now()-b,P=c(),E={...$,...k.attributes},K=[...o.links??[],...k.links];try{a({...Object.keys(E).length===0?{}:{attributes:E},durationMs:p,...k.events.length===0?{}:{events:k.events},...w===void 0?{}:{error:w},functionPath:r,...o.kind===void 0||o.kind==="internal"?{}:{kind:o.kind},...K.length===0?{}:{links:K},name:v,ok:m,parentSpanId:u,shardKey:i,spanId:l,startTs:b,traceId:t.traceId,userId:P})}catch{}if(g!==void 0)try{C(g,{attributes:E,durationMs:p,error:w,functionPath:r,ok:m,shardKey:i,userId:P})}catch{}}};if(n===!0&&s!==void 0){const g=await s();if(g!==void 0&&typeof g.enterSpan=="function")return await g.enterSpan(v,m=>T(m))}return await T()};return h(t.rootSpanId)},J=(e,t)=>{const{anchor:n,functionPath:r,propagate:a=!0,record:s,shardKey:i,userId:c}=e;return async(h,u)=>{const v=A(8),I=Date.now(),d=new Request(h,u);(typeof a=="function"?U(a,d.url):a)&&d.headers.set("traceparent",O(n.traceId,v,n.sampled??!0));let l,b;try{const o=await t(d);return b=o.status,o.ok||(l={message:`HTTP ${String(o.status)}`,type:`HTTP_${String(o.status)}`}),o}catch(o){throw l={message:o instanceof Error?o.message:String(o),type:S(o)},o}finally{try{s({attributes:{"http.request.method":d.method,...b===void 0?{}:{"http.response.status_code":b},"url.full":F(d.url)},durationMs:Date.now()-I,...l===void 0?{}:{error:l},functionPath:r,kind:"client",name:`${d.method} ${R(d.url)}`,ok:l===void 0,parentSpanId:n.rootSpanId,shardKey:i,spanId:v,startTs:I,traceId:n.traceId,userId:c()})}catch{}}}},Q=e=>{const{functionPath:t,record:n,shardKey:r}=e,a=(s,i,c,h)=>{if(!Number.isFinite(c))return;const u=y(h);try{n({...u===void 0?{}:{attributes:u},functionPath:t,kind:s,name:i,shardKey:r,ts:Date.now(),value:c})}catch{}};return{count:(s,i=1,c)=>{a("counter",s,i,c)},gauge:(s,i,c)=>{a("gauge",s,i,c)},record:(s,i,c)=>{a("histogram",s,i,c)}}},V=e=>{const{anchor:t,collected:n,durationMs:r,failure:a,functionPath:s,shardKey:i,startTs:c,userId:h}=e,u=n?.attributes??{};return{...Object.keys(u).length===0?{}:{attributes:u},dispatch:!0,durationMs:r,...n===void 0||n.events.length===0?{}:{events:n.events},...a===void 0?{}:{error:{message:a.thrown instanceof Error?a.thrown.message:String(a.thrown),type:S(a.thrown)}},functionPath:s,...n===void 0||n.links.length===0?{}:{links:n.links},name:s,ok:a===void 0,parentSpanId:"",shardKey:i,spanId:t.rootSpanId,startTs:c,traceId:t.traceId,userId:h}};export{C as applyHostSpanAttributes,Q as createMetrics,N as createSpanCollector,J as createTracedFetch,G as createTracer,V as dispatchRootSpan};
@@ -1 +0,0 @@
1
- const s=e=>{if(typeof e=="string")return e;try{return JSON.stringify(e)??String(e)}catch{return String(e)}},f=e=>typeof e=="boolean"||typeof e=="number"||typeof e=="string"?e:s(e),n=(e,t)=>{if(e===void 0&&t===void 0)return;const r={};if(t!==void 0)for(const[o,i]of Object.entries(t))r[o]=f(i);if(e!==void 0)for(const[o,i]of Object.entries(e))r[o]=f(i);return Object.keys(r).length===0?void 0:r};export{n};
@@ -1,31 +0,0 @@
1
- import{m as A,e as O}from"./metric-buffer-CdgXal7w.mjs";const r="__lunora_metric_history",o=6e4,R=1440,x=1e3,l=5e3,E=(e,s,...T)=>e.exec.call(e,s,...T),y=e=>Math.floor(e/o)*o,_=new WeakSet,d=e=>{_.has(e)||(E(e,`CREATE TABLE IF NOT EXISTS "${r}" (
2
- series_key TEXT NOT NULL,
3
- bucket_ms INTEGER NOT NULL,
4
- name TEXT NOT NULL,
5
- kind TEXT NOT NULL,
6
- attrs TEXT NOT NULL DEFAULT '{}',
7
- function_path TEXT NOT NULL DEFAULT '',
8
- shard_key TEXT,
9
- count INTEGER NOT NULL DEFAULT 0,
10
- sum REAL NOT NULL DEFAULT 0,
11
- min REAL NOT NULL DEFAULT 0,
12
- max REAL NOT NULL DEFAULT 0,
13
- last REAL NOT NULL DEFAULT 0,
14
- last_ts REAL NOT NULL DEFAULT 0,
15
- exemplar_trace TEXT,
16
- PRIMARY KEY (series_key, bucket_ms)
17
- )`),_.add(e))},S=4096,N=new WeakMap,M=e=>{let s=N.get(e);return s===void 0&&(s=new Set,N.set(e,s)),s},I=(e,s,T,c={})=>{const t=c.maxSeries??x,n=c.retentionBuckets??R;d(e);const a=A(s),i=y(s.ts),u=M(e),L=`${a}\0${i.toString()}`,m=u.has(L)||E(e,`SELECT 1 AS c FROM "${r}" WHERE series_key = ? AND bucket_ms = ? LIMIT 1`,a,i).toArray().length>0;if(!m&&!(E(e,`SELECT 1 AS c FROM "${r}" WHERE series_key = ? LIMIT 1`,a).toArray().length>0)&&E(e,`SELECT COUNT(DISTINCT series_key) AS n FROM "${r}"`).one().n>=t)return;const k=T??null;E(e,`INSERT INTO "${r}"
18
- (series_key, bucket_ms, name, kind, attrs, function_path, shard_key, count, sum, min, max, last, last_ts, exemplar_trace)
19
- VALUES (?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?)
20
- ON CONFLICT(series_key, bucket_ms) DO UPDATE SET
21
- count = count + 1,
22
- sum = sum + excluded.sum,
23
- min = MIN(min, excluded.min),
24
- max = MAX(max, excluded.max),
25
- last = excluded.last,
26
- last_ts = excluded.last_ts,
27
- exemplar_trace = CASE WHEN excluded.exemplar_trace IS NULL THEN exemplar_trace ELSE excluded.exemplar_trace END`,a,i,s.name,s.kind,O(s.attributes??{}),s.functionPath,s.shardKey??null,s.value,s.value,s.value,s.value,s.ts,k),m||E(e,`DELETE FROM "${r}"
28
- WHERE series_key = ?
29
- AND bucket_ms <= (
30
- SELECT MAX(bucket_ms) - ? FROM "${r}" WHERE series_key = ?
31
- )`,a,n*o,a),m&&!u.has(L)&&(u.size>=S&&u.clear(),u.add(L))},U=e=>{if(!(e===""||e==="{}"))try{const s=JSON.parse(e);return s!==null&&typeof s=="object"?s:void 0}catch{return}},h=(e,s={})=>{d(e);const T=s.sinceMs===void 0?E(e,`SELECT * FROM "${r}" ORDER BY bucket_ms DESC LIMIT ?`,l).toArray():E(e,`SELECT * FROM "${r}" WHERE bucket_ms >= ? ORDER BY bucket_ms DESC LIMIT ?`,s.sinceMs,l).toArray(),c=new Map;for(const t of T){let n=c.get(t.series_key);if(n===void 0){const a=U(t.attrs);n={...a===void 0?{}:{attributes:a},functionPath:t.function_path,kind:t.kind,name:t.name,points:[],...t.shard_key===null?{}:{shardKey:t.shard_key}},c.set(t.series_key,n)}n.points.push({bucketMs:t.bucket_ms,count:t.count,...t.exemplar_trace===null?{}:{exemplarTraceId:t.exemplar_trace},last:t.last,max:t.max,min:t.min,sum:t.sum})}for(const t of c.values())t.points.sort((n,a)=>n.bucketMs-a.bucketMs);return{series:[...c.values()]}};export{h as readMetricHistory,I as recordMetricHistory};
@@ -1,32 +0,0 @@
1
- const E="__lunora_metrics_queries",d="__lunora_metrics_queries_buckets",q=6e4,F=90,u=[1,2,5,10,25,50,100,250,500,1e3,5e3],R=512,O=500,a=(e,t,...r)=>e.exec.call(e,t,...r),D=e=>{let t=e.replaceAll(/'(?:[^']|'')*'/g,"?").replaceAll(/\b0x[\da-f]+\b/gi,"?").replaceAll(/(?<=[=,([\s])\d+(?:\.\d+)?/g,"?").replaceAll(/\s+/g," ").trim();return t.length>R&&(t=`${t.slice(0,R-1)}…`),t},N=e=>{a(e,`CREATE TABLE IF NOT EXISTS "${E}" (
2
- normalized_sql TEXT PRIMARY KEY,
3
- exec_count INTEGER NOT NULL DEFAULT 0,
4
- total_duration_ms REAL NOT NULL DEFAULT 0,
5
- rows_read INTEGER NOT NULL DEFAULT 0,
6
- rows_written INTEGER NOT NULL DEFAULT 0
7
- )`)},U=new WeakMap,S=e=>Math.floor(e/6e4)*6e4,h=e=>{let t=2166136261;for(let r=0;r<e.length;r+=1)t^=e.codePointAt(r)??0,t=Math.imul(t,16777619)>>>0;return t.toString(16).padStart(8,"0")},I=e=>{const t=u.findIndex(r=>e<=r);return t===-1?u.length:t},T=e=>`lat_${String(e)}`,x=e=>{const t=Array.from({length:u.length+1},(r,_)=>`${T(_)} INTEGER NOT NULL DEFAULT 0`).join(", ");a(e,`CREATE TABLE IF NOT EXISTS "${d}" (
8
- sql_hash TEXT NOT NULL,
9
- bucket_ms INTEGER NOT NULL,
10
- exec_count INTEGER NOT NULL DEFAULT 0,
11
- total_duration_ms REAL NOT NULL DEFAULT 0,
12
- rows_read INTEGER NOT NULL DEFAULT 0,
13
- rows_written INTEGER NOT NULL DEFAULT 0,
14
- ${t},
15
- PRIMARY KEY (sql_hash, bucket_ms)
16
- )`)},b=(e,t)=>{a(e,`DELETE FROM "${d}" WHERE bucket_ms < ?`,S(t)-90*6e4)},g=(e,t,r,_,c,n)=>{try{x(e);const s=T(I(r)),i=`INSERT INTO "${d}" (sql_hash, bucket_ms, exec_count, total_duration_ms, rows_read, rows_written, ${s})
17
- VALUES (?, ?, 1, ?, ?, ?, 1)
18
- ON CONFLICT(sql_hash, bucket_ms) DO UPDATE SET
19
- exec_count = exec_count + 1,
20
- total_duration_ms = total_duration_ms + excluded.total_duration_ms,
21
- rows_read = rows_read + excluded.rows_read,
22
- rows_written = rows_written + excluded.rows_written,
23
- ${s} = ${s} + 1`;a(e,i,h(t),S(n),r,_,c);const m=S(n);U.get(e)!==m&&(U.set(e,m),b(e,n))}catch{}},M=(e,t)=>{const r=e.reduce((n,s)=>n+s,0);if(r===0)return 0;const _=r*t;let c=0;for(const[n,s]of e.entries())if(c+=s,c>=_)return u[n]??u.at(-1)??0;return u.at(-1)??0},$=(e,t,r=Date.now())=>{try{N(e),x(e)}catch{return{buckets:[],capped:!1,entries:[],trackedStatements:0}}const _=S(r-t),c=Array.from({length:u.length+1},(o,l)=>`SUM(${T(l)}) AS ${T(l)}`).join(", "),n=a(e,`SELECT sql_hash, SUM(exec_count) AS exec_count, SUM(total_duration_ms) AS total_duration_ms,
24
- SUM(rows_read) AS rows_read, SUM(rows_written) AS rows_written, ${c}
25
- FROM "${d}" WHERE bucket_ms >= ? GROUP BY sql_hash`,_).toArray(),s=new Map;for(const o of a(e,`SELECT normalized_sql FROM "${E}"`))s.set(h(o.normalized_sql),o.normalized_sql);const i=n.map(o=>{const l=Array.from({length:u.length+1},(k,C)=>Number(o[T(C)]??0)),w=Number(o.exec_count??0),A=Number(o.total_duration_ms??0);return{avgDurationMs:w>0?A/w:0,execCount:w,normalizedSql:s.get(String(o.sql_hash))??String(o.sql_hash),p50DurationMs:M(l,.5),p95DurationMs:M(l,.95),rowsRead:Number(o.rows_read??0),rowsWritten:Number(o.rows_written??0),totalDurationMs:A}});i.sort((o,l)=>l.totalDurationMs-o.totalDurationMs);const m=a(e,`SELECT bucket_ms, SUM(exec_count) AS exec_count, SUM(total_duration_ms) AS total_duration_ms
26
- FROM "${d}" WHERE bucket_ms >= ? GROUP BY bucket_ms ORDER BY bucket_ms ASC`,_).toArray().map(o=>({avgDurationMs:o.exec_count>0?o.total_duration_ms/o.exec_count:0,bucketMs:o.bucket_ms,execCount:o.exec_count})),L=a(e,`SELECT COUNT(*) AS n FROM "${E}"`).one().n;return{buckets:m,capped:L>=O,entries:i,trackedStatements:L}},p=(e,t,r,_,c,n=Date.now())=>{const s=D(t);if(s.length===0||(N(e),a(e,`SELECT COUNT(*) AS n FROM "${E}"`).one().n>=O&&a(e,`SELECT COUNT(*) AS c FROM "${E}" WHERE normalized_sql = ?`,s).one().c===0))return;const i=`INSERT INTO "${E}" (normalized_sql, exec_count, total_duration_ms, rows_read, rows_written)
27
- VALUES (?, 1, ?, ?, ?)
28
- ON CONFLICT(normalized_sql) DO UPDATE SET
29
- exec_count = exec_count + 1,
30
- total_duration_ms = total_duration_ms + excluded.total_duration_ms,
31
- rows_read = rows_read + excluded.rows_read,
32
- rows_written = rows_written + excluded.rows_written`;a(e,i,s,r,_,c),g(e,s,r,_,c,n)},y=e=>(N(e),a(e,`SELECT normalized_sql, exec_count, total_duration_ms, rows_read, rows_written FROM "${E}" ORDER BY total_duration_ms DESC`).toArray().map(t=>({execCount:t.exec_count,normalizedSql:t.normalized_sql,rowsRead:t.rows_read,rowsWritten:t.rows_written,totalDurationMs:t.total_duration_ms})));export{u as LATENCY_BUCKET_EDGES,d as QUERY_BUCKETS_TABLE,q as QUERY_BUCKET_MS,F as QUERY_BUCKET_RETENTION,R as QUERY_METRICS_MAX_SQL_LEN,O as QUERY_METRICS_MAX_STATEMENTS,E as QUERY_METRICS_TABLE,x as ensureQueryBucketsTable,N as ensureQueryMetricsTable,h as hashStatement,I as latencyBucketIndex,D as normalizeSql,M as percentileFrom,b as pruneQueryBuckets,$ as readQueryInsights,y as readQueryMetrics,g as recordQueryBucket,p as recordQueryMetric};