@lunora/do 1.0.0-alpha.29 → 1.0.0-alpha.30

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
@@ -2531,6 +2531,7 @@ declare const ADMIN_FUNCTIONS: {
2531
2531
  readonly getCapturedMail: "__lunora_admin__:getCapturedMail";
2532
2532
  readonly getFanoutMetrics: "__lunora_admin__:getFanoutMetrics";
2533
2533
  readonly getFunctionStats: "__lunora_admin__:getFunctionStats";
2534
+ readonly getIssues: "__lunora_admin__:getIssues";
2534
2535
  readonly listSubscriptions: "__lunora_admin__:listSubscriptions";
2535
2536
  readonly listTableIndexes: "__lunora_admin__:listTableIndexes";
2536
2537
  readonly getLogs: "__lunora_admin__:getLogs";
@@ -3346,10 +3347,14 @@ type LogLevel = "debug" | "error" | "info" | "warn";
3346
3347
  /**
3347
3348
  * One buffered log line. `functionPath` is the RPC that produced it (when the
3348
3349
  * entry came from the RPC dispatch site); `timestamp` is `Date.now()` at the
3349
- * moment it was pushed.
3350
+ * moment it was pushed. `instance`/`exitCode` are populated for container
3351
+ * lifecycle entries: `instance` correlates the per-instance Durable Object id,
3352
+ * `exitCode` carries the process exit code parsed out of a `stop` event.
3350
3353
  */
3351
3354
  interface LogEntry {
3355
+ exitCode?: number;
3352
3356
  functionPath?: string;
3357
+ instance?: string;
3353
3358
  level: LogLevel;
3354
3359
  message: string;
3355
3360
  timestamp: number;
@@ -5589,6 +5594,14 @@ declare abstract class ShardDO {
5589
5594
  * the panel renders it alongside `ctx.log` lines. Admin-gated by
5590
5595
  * `handleAdminRpc`'s caller (the same `LUNORA_ADMIN_TOKEN` bearer as every
5591
5596
  * other admin write).
5597
+ *
5598
+ * An `error`-level lifecycle event (a crash/`onError`, or a non-zero-exit
5599
+ * `stop`) is ALSO appended as an `error`-outcome row to the durable
5600
+ * `__lunora_reqlog__` — the same readout `getIssues` groups over — so a
5601
+ * crash-looping container folds into the Issues list right beside Worker
5602
+ * errors (they share the `fingerprintError` hash over `functionPath ::
5603
+ * bucket(message)`). The in-memory buffer stays the live Logs feed; the
5604
+ * durable row is what survives hibernation for triage.
5592
5605
  */
5593
5606
  private handleRecordContainerEvent;
5594
5607
  /**
@@ -5777,6 +5790,25 @@ declare abstract class ShardDO {
5777
5790
  */
5778
5791
  private recordRequestLog;
5779
5792
  /**
5793
+ * The single durable-row + Logpush write seam for `__lunora_reqlog__`. Both
5794
+ * the per-dispatch {@link recordRequestLog} and the container-crash path
5795
+ * ({@link handleRecordContainerEvent}) funnel through here so they can't
5796
+ * drift — before this was extracted the container writer persisted the row
5797
+ * but silently skipped the Logpush emit every other error got.
5798
+ *
5799
+ * Best-effort by contract: a SQL failure (e.g. a test double with no `sql`
5800
+ * handle) or a serialization hiccup in the emit must NEVER turn a served
5801
+ * request — or a container event push — into a failed one, so each half is
5802
+ * swallowed independently.
5803
+ *
5804
+ * Errors are always streamed to `console` (rare, high-value — they ride CF
5805
+ * Workers Logs at error level and the dev-server formats them in the
5806
+ * terminal), redacted in prod like any other event. The full per-dispatch
5807
+ * summary stream (successful OKs too) stays opt-in behind
5808
+ * `LUNORA_REQUEST_LOG_EMIT` so a hot shard doesn't emit a line per call.
5809
+ */
5810
+ private persistRequestLog;
5811
+ /**
5780
5812
  * Resolve the request-log knobs from the Worker `env`, all PLAN3 §3.3 decisions.
5781
5813
  *
5782
5814
  * `captureRaw`: raw (un-redacted) args/identity in a dev environment, redacted
@@ -5910,6 +5942,18 @@ declare abstract class ShardDO {
5910
5942
  */
5911
5943
  private readAdminRequestLog;
5912
5944
  /**
5945
+ * Resolve a `getIssues` admin read: fold the recent `error`-outcome
5946
+ * request-log rows into grouped {@link readErrorIssues Issues} by fingerprint,
5947
+ * accepting the same optional correlation filters as `getRequestLog`
5948
+ * (function-path prefix, exact shardKey/userId) plus a `limit` on rows
5949
+ * scanned. This is a read over the bounded reqlog readout — no new store —
5950
+ * so a self-hosted worker gets grouped error triage for free. Carries the
5951
+ * {@link ADMIN_WILDCARD} like the other log reads so a live Issues
5952
+ * subscription re-runs on every write-flush (the per-socket JSON memo still
5953
+ * suppresses byte-identical pushes).
5954
+ */
5955
+ private readAdminIssues;
5956
+ /**
5913
5957
  * Resolve a `getAuthMetrics` admin read: the durable app-level auth
5914
5958
  * attempt/failure counters + minute-bucketed history the studio SLO panel
5915
5959
  * charts (PLAN3 §2.3). Auth runs as a top-level `/api/auth/*` worker route,
package/dist/index.d.ts CHANGED
@@ -2531,6 +2531,7 @@ declare const ADMIN_FUNCTIONS: {
2531
2531
  readonly getCapturedMail: "__lunora_admin__:getCapturedMail";
2532
2532
  readonly getFanoutMetrics: "__lunora_admin__:getFanoutMetrics";
2533
2533
  readonly getFunctionStats: "__lunora_admin__:getFunctionStats";
2534
+ readonly getIssues: "__lunora_admin__:getIssues";
2534
2535
  readonly listSubscriptions: "__lunora_admin__:listSubscriptions";
2535
2536
  readonly listTableIndexes: "__lunora_admin__:listTableIndexes";
2536
2537
  readonly getLogs: "__lunora_admin__:getLogs";
@@ -3346,10 +3347,14 @@ type LogLevel = "debug" | "error" | "info" | "warn";
3346
3347
  /**
3347
3348
  * One buffered log line. `functionPath` is the RPC that produced it (when the
3348
3349
  * entry came from the RPC dispatch site); `timestamp` is `Date.now()` at the
3349
- * moment it was pushed.
3350
+ * moment it was pushed. `instance`/`exitCode` are populated for container
3351
+ * lifecycle entries: `instance` correlates the per-instance Durable Object id,
3352
+ * `exitCode` carries the process exit code parsed out of a `stop` event.
3350
3353
  */
3351
3354
  interface LogEntry {
3355
+ exitCode?: number;
3352
3356
  functionPath?: string;
3357
+ instance?: string;
3353
3358
  level: LogLevel;
3354
3359
  message: string;
3355
3360
  timestamp: number;
@@ -5589,6 +5594,14 @@ declare abstract class ShardDO {
5589
5594
  * the panel renders it alongside `ctx.log` lines. Admin-gated by
5590
5595
  * `handleAdminRpc`'s caller (the same `LUNORA_ADMIN_TOKEN` bearer as every
5591
5596
  * other admin write).
5597
+ *
5598
+ * An `error`-level lifecycle event (a crash/`onError`, or a non-zero-exit
5599
+ * `stop`) is ALSO appended as an `error`-outcome row to the durable
5600
+ * `__lunora_reqlog__` — the same readout `getIssues` groups over — so a
5601
+ * crash-looping container folds into the Issues list right beside Worker
5602
+ * errors (they share the `fingerprintError` hash over `functionPath ::
5603
+ * bucket(message)`). The in-memory buffer stays the live Logs feed; the
5604
+ * durable row is what survives hibernation for triage.
5592
5605
  */
5593
5606
  private handleRecordContainerEvent;
5594
5607
  /**
@@ -5777,6 +5790,25 @@ declare abstract class ShardDO {
5777
5790
  */
5778
5791
  private recordRequestLog;
5779
5792
  /**
5793
+ * The single durable-row + Logpush write seam for `__lunora_reqlog__`. Both
5794
+ * the per-dispatch {@link recordRequestLog} and the container-crash path
5795
+ * ({@link handleRecordContainerEvent}) funnel through here so they can't
5796
+ * drift — before this was extracted the container writer persisted the row
5797
+ * but silently skipped the Logpush emit every other error got.
5798
+ *
5799
+ * Best-effort by contract: a SQL failure (e.g. a test double with no `sql`
5800
+ * handle) or a serialization hiccup in the emit must NEVER turn a served
5801
+ * request — or a container event push — into a failed one, so each half is
5802
+ * swallowed independently.
5803
+ *
5804
+ * Errors are always streamed to `console` (rare, high-value — they ride CF
5805
+ * Workers Logs at error level and the dev-server formats them in the
5806
+ * terminal), redacted in prod like any other event. The full per-dispatch
5807
+ * summary stream (successful OKs too) stays opt-in behind
5808
+ * `LUNORA_REQUEST_LOG_EMIT` so a hot shard doesn't emit a line per call.
5809
+ */
5810
+ private persistRequestLog;
5811
+ /**
5780
5812
  * Resolve the request-log knobs from the Worker `env`, all PLAN3 §3.3 decisions.
5781
5813
  *
5782
5814
  * `captureRaw`: raw (un-redacted) args/identity in a dev environment, redacted
@@ -5910,6 +5942,18 @@ declare abstract class ShardDO {
5910
5942
  */
5911
5943
  private readAdminRequestLog;
5912
5944
  /**
5945
+ * Resolve a `getIssues` admin read: fold the recent `error`-outcome
5946
+ * request-log rows into grouped {@link readErrorIssues Issues} by fingerprint,
5947
+ * accepting the same optional correlation filters as `getRequestLog`
5948
+ * (function-path prefix, exact shardKey/userId) plus a `limit` on rows
5949
+ * scanned. This is a read over the bounded reqlog readout — no new store —
5950
+ * so a self-hosted worker gets grouped error triage for free. Carries the
5951
+ * {@link ADMIN_WILDCARD} like the other log reads so a live Issues
5952
+ * subscription re-runs on every write-flush (the per-socket JSON memo still
5953
+ * suppresses byte-identical pushes).
5954
+ */
5955
+ private readAdminIssues;
5956
+ /**
5913
5957
  * Resolve a `getAuthMetrics` admin read: the durable app-level auth
5914
5958
  * attempt/failure counters + minute-bucketed history the studio SLO panel
5915
5959
  * charts (PLAN3 §2.3). Auth runs as a top-level `/api/auth/*` worker route,
package/dist/index.mjs CHANGED
@@ -11,7 +11,7 @@ export { diffExternalSource } from './packem_shared/diffExternalSource-Cx9HUPJj.
11
11
  export { materializeExternalRows, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-CTqZisSC.mjs';
12
12
  export { isSourceDue, liftSourceId, pullExternalSourceTick } from './packem_shared/isSourceDue-CYkt7Ru8.mjs';
13
13
  export { FUNCTION_METRICS_BUCKETS_TABLE, FUNCTION_METRICS_BUCKET_MS, FUNCTION_METRICS_BUCKET_RETENTION, FUNCTION_METRICS_INDEX_TABLE, FUNCTION_METRICS_TABLE, ensureFunctionMetricsTables, readFunctionMetricBuckets, readFunctionMetricIndexHits, readFunctionMetrics, readFunctionMetricsTotals, recordFunctionMetric } from './packem_shared/FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
14
- export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-Bxdijn66.mjs';
14
+ export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-CAHLZMj8.mjs';
15
15
  export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
16
16
  export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
17
17
  export { default as NotFoundError } from './packem_shared/NotFoundError-C70b9hLw.mjs';
@@ -19,14 +19,14 @@ export { armRestore, readBookmark } from './packem_shared/armRestore-4Px61hHS.mj
19
19
  export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-WQY8m62C.mjs';
20
20
  export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
21
21
  export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-BYlSGY0N.mjs';
22
- export { serveRelationFanout } from './packem_shared/serveRelationFanout-C56hKu1O.mjs';
22
+ export { serveRelationFanout } from './packem_shared/serveRelationFanout-BgaNg3Hu.mjs';
23
23
  export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
24
24
  export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-BXSq3S70.mjs';
25
25
  export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs';
26
26
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
27
27
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
28
28
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs';
29
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-CJLKsAHN.mjs';
29
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-BA8QOChj.mjs';
30
30
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
31
31
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-D57CJaT9.mjs';
32
32
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
@@ -23,6 +23,7 @@ const ADMIN_FUNCTIONS = {
23
23
  getCapturedMail: "__lunora_admin__:getCapturedMail",
24
24
  getFanoutMetrics: "__lunora_admin__:getFanoutMetrics",
25
25
  getFunctionStats: "__lunora_admin__:getFunctionStats",
26
+ getIssues: "__lunora_admin__:getIssues",
26
27
  listSubscriptions: "__lunora_admin__:listSubscriptions",
27
28
  listTableIndexes: "__lunora_admin__:listTableIndexes",
28
29
  getLogs: "__lunora_admin__:getLogs",
@@ -8,12 +8,13 @@ import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-C
8
8
  import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
9
9
  import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
10
10
  import { readFunctionMetricsTotals, readFunctionMetricIndexHits, recordFunctionMetric, mergeScanAttribution, readFunctionMetrics, readFunctionMetricBuckets } from './FUNCTION_METRICS_BUCKETS_TABLE-UDNVD7FS.mjs';
11
- import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-Bxdijn66.mjs';
11
+ import { createFanoutCounters, ADMIN_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, selectMatchingIds, ADMIN_FUNCTIONS, findStorageReferences, listTables, summarizeSubscriptions, summarizeFanoutTopics, readTablePage, facetColumn, FLAGS_FUNCTION_PREFIX, recordFanoutPass, MAX_PAGE_SIZE } from './ADMIN_FUNCTIONS-CAHLZMj8.mjs';
12
12
  import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
13
13
  import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
14
14
  import { readBookmark, armRestore } from './armRestore-4Px61hHS.mjs';
15
15
  import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-BYlSGY0N.mjs';
16
16
  import { stableStringify } from './stableStringify-MydiuScU.mjs';
17
+ import { fingerprintError } from '@lunora/fingerprint';
17
18
  import { redact, standardRules } from '@visulima/redact';
18
19
  import { i as isDevEnvironment, c as buildSettings, b as buildSecurityAudit } from './security-audit-CucgBice.mjs';
19
20
  import { runReadonlySql } from './MAX_SQL_ROWS-D57CJaT9.mjs';
@@ -1291,6 +1292,52 @@ const readRequestLog = (sql, options = {}) => {
1291
1292
  return base;
1292
1293
  });
1293
1294
  };
1295
+ const readErrorIssues = (sql, options = {}) => {
1296
+ ensureRequestLogTable(sql);
1297
+ const limit = Math.max(1, Math.min(options.limit ?? REQUEST_LOG_RETENTION, 1e4));
1298
+ const conjuncts = ["outcome = 'error'"];
1299
+ const parameters = [];
1300
+ if (options.functionPathPrefix !== void 0 && options.functionPathPrefix !== "") {
1301
+ conjuncts.push(String.raw`function_path LIKE ? ESCAPE '\'`);
1302
+ parameters.push(`${escapeLike(options.functionPathPrefix)}%`);
1303
+ }
1304
+ if (options.userId !== void 0 && options.userId !== "") {
1305
+ conjuncts.push("user_id = ?");
1306
+ parameters.push(options.userId);
1307
+ }
1308
+ if (options.shardKey !== void 0 && options.shardKey !== "") {
1309
+ conjuncts.push("shard_key = ?");
1310
+ parameters.push(options.shardKey);
1311
+ }
1312
+ parameters.push(limit);
1313
+ const rows = runSql(
1314
+ sql,
1315
+ `SELECT function_path, error_message, ts
1316
+ FROM "${REQUEST_LOG_TABLE}" WHERE ${conjuncts.join(" AND ")} ORDER BY seq DESC LIMIT ?`,
1317
+ ...parameters
1318
+ ).toArray();
1319
+ const issues = /* @__PURE__ */ new Map();
1320
+ const sampleTs = /* @__PURE__ */ new Map();
1321
+ for (const row of rows) {
1322
+ const message = row.error_message ?? "";
1323
+ const { culprit, hash, title } = fingerprintError({ functionPath: row.function_path, message });
1324
+ const existing = issues.get(hash);
1325
+ if (existing === void 0) {
1326
+ issues.set(hash, { count: 1, culprit, firstSeen: row.ts, hash, lastSeen: row.ts, sampleMessage: message, title });
1327
+ sampleTs.set(hash, row.ts);
1328
+ continue;
1329
+ }
1330
+ existing.count += 1;
1331
+ existing.firstSeen = Math.min(existing.firstSeen, row.ts);
1332
+ existing.lastSeen = Math.max(existing.lastSeen, row.ts);
1333
+ if (row.ts > (sampleTs.get(hash) ?? Number.NEGATIVE_INFINITY)) {
1334
+ sampleTs.set(hash, row.ts);
1335
+ existing.sampleMessage = message;
1336
+ existing.title = title;
1337
+ }
1338
+ }
1339
+ return [...issues.values()].toSorted((a, b) => b.lastSeen - a.lastSeen);
1340
+ };
1294
1341
 
1295
1342
  const runSocketPool = async (items, processOne, concurrency = 8) => {
1296
1343
  let cursor = 0;
@@ -1516,6 +1563,7 @@ const parseRecordAuthEventArgs = (args) => {
1516
1563
  }
1517
1564
  return { outcome };
1518
1565
  };
1566
+ const CONTAINER_EXIT_CODE_PATTERN = /\(exit (\d+)\)/;
1519
1567
  const parseRecordContainerEventArgs = (args) => {
1520
1568
  const raw = args["event"];
1521
1569
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
@@ -1530,8 +1578,13 @@ const parseRecordContainerEventArgs = (args) => {
1530
1578
  const level = envelope["level"] === "error" ? "error" : "info";
1531
1579
  const detail = typeof envelope["message"] === "string" ? envelope["message"] : void 0;
1532
1580
  const timestamp = typeof envelope["ts"] === "number" ? envelope["ts"] : Date.now();
1581
+ const instance = typeof envelope["instance"] === "string" && envelope["instance"] !== "" ? envelope["instance"] : void 0;
1582
+ const exitRaw = detail === void 0 ? void 0 : CONTAINER_EXIT_CODE_PATTERN.exec(detail)?.[1];
1583
+ const exitCode = exitRaw === void 0 ? void 0 : Number.parseInt(exitRaw, 10);
1533
1584
  return {
1585
+ exitCode,
1534
1586
  functionPath: `container:${container}`,
1587
+ instance,
1535
1588
  level,
1536
1589
  message: detail === void 0 || detail === "" ? event : `${event}: ${detail}`,
1537
1590
  timestamp
@@ -2391,6 +2444,8 @@ class ShardDO {
2391
2444
  message,
2392
2445
  timestamp: Date.now()
2393
2446
  });
2447
+ this.recordChangedTable(REQUEST_LOG_TABLE);
2448
+ await this.flushChangedTables();
2394
2449
  return this.errorToResponse(error);
2395
2450
  } finally {
2396
2451
  this.currentRequestBookmark = void 0;
@@ -4394,10 +4449,32 @@ class ShardDO {
4394
4449
  * the panel renders it alongside `ctx.log` lines. Admin-gated by
4395
4450
  * `handleAdminRpc`'s caller (the same `LUNORA_ADMIN_TOKEN` bearer as every
4396
4451
  * other admin write).
4397
- */
4398
- handleRecordContainerEvent(args) {
4452
+ *
4453
+ * An `error`-level lifecycle event (a crash/`onError`, or a non-zero-exit
4454
+ * `stop`) is ALSO appended as an `error`-outcome row to the durable
4455
+ * `__lunora_reqlog__` — the same readout `getIssues` groups over — so a
4456
+ * crash-looping container folds into the Issues list right beside Worker
4457
+ * errors (they share the `fingerprintError` hash over `functionPath ::
4458
+ * bucket(message)`). The in-memory buffer stays the live Logs feed; the
4459
+ * durable row is what survives hibernation for triage.
4460
+ */
4461
+ async handleRecordContainerEvent(args) {
4399
4462
  const entry = parseRecordContainerEventArgs(args);
4400
4463
  this.logs.push(entry);
4464
+ const crashed = entry.level === "error" || entry.exitCode !== void 0 && entry.exitCode !== 0;
4465
+ if (crashed) {
4466
+ const logEntry = {
4467
+ durationMs: 0,
4468
+ errorMessage: entry.message,
4469
+ functionPath: entry.functionPath,
4470
+ outcome: "error",
4471
+ shardKey: this.state.id?.name,
4472
+ ts: entry.timestamp
4473
+ };
4474
+ this.persistRequestLog(logEntry, this.requestLogConfig());
4475
+ this.recordChangedTable(REQUEST_LOG_TABLE);
4476
+ await this.flushChangedTables();
4477
+ }
4401
4478
  return jsonResponse({ result: { recorded: true } }, 200);
4402
4479
  }
4403
4480
  /**
@@ -4738,12 +4815,33 @@ class ShardDO {
4738
4815
  ts: Date.now(),
4739
4816
  userId: this.getCurrentUserId()
4740
4817
  };
4818
+ this.persistRequestLog(entry, config);
4819
+ }
4820
+ /**
4821
+ * The single durable-row + Logpush write seam for `__lunora_reqlog__`. Both
4822
+ * the per-dispatch {@link recordRequestLog} and the container-crash path
4823
+ * ({@link handleRecordContainerEvent}) funnel through here so they can't
4824
+ * drift — before this was extracted the container writer persisted the row
4825
+ * but silently skipped the Logpush emit every other error got.
4826
+ *
4827
+ * Best-effort by contract: a SQL failure (e.g. a test double with no `sql`
4828
+ * handle) or a serialization hiccup in the emit must NEVER turn a served
4829
+ * request — or a container event push — into a failed one, so each half is
4830
+ * swallowed independently.
4831
+ *
4832
+ * Errors are always streamed to `console` (rare, high-value — they ride CF
4833
+ * Workers Logs at error level and the dev-server formats them in the
4834
+ * terminal), redacted in prod like any other event. The full per-dispatch
4835
+ * summary stream (successful OKs too) stays opt-in behind
4836
+ * `LUNORA_REQUEST_LOG_EMIT` so a hot shard doesn't emit a line per call.
4837
+ */
4838
+ persistRequestLog(entry, config) {
4741
4839
  const writeOptions = { captureRaw: config.captureRaw, retention: config.retention };
4742
4840
  try {
4743
4841
  appendRequestLogEntry(this.state.storage.sql, entry, writeOptions);
4744
4842
  } catch {
4745
4843
  }
4746
- if (config.emit || outcome === "error") {
4844
+ if (config.emit || entry.outcome === "error") {
4747
4845
  try {
4748
4846
  emitRequestLogEvent(entry, writeOptions);
4749
4847
  } catch {
@@ -4836,6 +4934,9 @@ class ShardDO {
4836
4934
  if (functionPath === ADMIN_FUNCTIONS.getRequestLog) {
4837
4935
  return this.readAdminRequestLog(sql, args);
4838
4936
  }
4937
+ if (functionPath === ADMIN_FUNCTIONS.getIssues) {
4938
+ return this.readAdminIssues(sql, args);
4939
+ }
4839
4940
  const durable = this.readAdminDurableSignal(functionPath, sql, args);
4840
4941
  if (durable) {
4841
4942
  return durable;
@@ -5066,6 +5167,30 @@ class ShardDO {
5066
5167
  };
5067
5168
  return { result, tables: /* @__PURE__ */ new Set([ADMIN_WILDCARD]) };
5068
5169
  }
5170
+ /**
5171
+ * Resolve a `getIssues` admin read: fold the recent `error`-outcome
5172
+ * request-log rows into grouped {@link readErrorIssues Issues} by fingerprint,
5173
+ * accepting the same optional correlation filters as `getRequestLog`
5174
+ * (function-path prefix, exact shardKey/userId) plus a `limit` on rows
5175
+ * scanned. This is a read over the bounded reqlog readout — no new store —
5176
+ * so a self-hosted worker gets grouped error triage for free. Carries the
5177
+ * {@link ADMIN_WILDCARD} like the other log reads so a live Issues
5178
+ * subscription re-runs on every write-flush (the per-socket JSON memo still
5179
+ * suppresses byte-identical pushes).
5180
+ */
5181
+ // eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `readAdmin*` resolvers
5182
+ readAdminIssues(sql, args) {
5183
+ ensureRequestLogTable(sql);
5184
+ const result = {
5185
+ issues: readErrorIssues(sql, {
5186
+ functionPathPrefix: typeof args["functionPathPrefix"] === "string" ? args["functionPathPrefix"] : void 0,
5187
+ limit: typeof args["limit"] === "number" ? args["limit"] : void 0,
5188
+ shardKey: typeof args["shardKey"] === "string" ? args["shardKey"] : void 0,
5189
+ userId: typeof args["userId"] === "string" ? args["userId"] : void 0
5190
+ })
5191
+ };
5192
+ return { result, tables: /* @__PURE__ */ new Set([ADMIN_WILDCARD]) };
5193
+ }
5069
5194
  /**
5070
5195
  * Resolve a `getAuthMetrics` admin read: the durable app-level auth
5071
5196
  * attempt/failure counters + minute-bucketed history the studio SLO panel
@@ -1,5 +1,5 @@
1
1
  import { LunoraError } from '@lunora/errors';
2
- import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-Bxdijn66.mjs';
2
+ import { RELATION_FUNCTION_PREFIX } from './ADMIN_FUNCTIONS-CAHLZMj8.mjs';
3
3
 
4
4
  const serveRelationFanout = async (schema, database, functionPath, args) => {
5
5
  const table = typeof args["table"] === "string" ? args["table"] : "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.29",
3
+ "version": "1.0.0-alpha.30",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -47,6 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@lunora/errors": "1.0.0-alpha.4",
50
+ "@lunora/fingerprint": "1.0.0-alpha.1",
50
51
  "@visulima/redact": "3.0.0",
51
52
  "drizzle-orm": "^0.45.2"
52
53
  },