@lunora/do 1.0.0-alpha.34 → 1.0.0-alpha.36
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 +407 -69
- package/dist/index.d.ts +407 -69
- package/dist/index.mjs +4 -3
- package/dist/packem_shared/{ADMIN_FUNCTIONS-CAHLZMj8.mjs → ADMIN_FUNCTIONS-DVk02KpP.mjs} +6 -0
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-B5c6kQIN.mjs → ROOT_DO_SIZE_WARN_BYTES-CBlw_ir3.mjs} +582 -72
- package/dist/packem_shared/context-telemetry-DWfYDxCS.mjs +165 -0
- package/dist/packem_shared/createMetrics-BIL8Cl8X.mjs +1 -0
- package/dist/packem_shared/{serveRelationFanout-BgaNg3Hu.mjs → serveRelationFanout-FdrPflv1.mjs} +1 -1
- package/package.json +1 -1
|
@@ -5,12 +5,14 @@ import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
|
|
|
5
5
|
import { e as encodeWire, d as decodeWire } from './wire-codec-CzQc1pvf.mjs';
|
|
6
6
|
import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-Dy3oFZ26.mjs';
|
|
7
7
|
import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
|
|
8
|
+
import { n as normalizeLogFields, r as resolveTraceAnchor, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-DWfYDxCS.mjs';
|
|
8
9
|
import { DATA_MIGRATION_STATE_TABLE, readMigrationStatus } from './DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
|
|
9
10
|
import { SCAN_DEP, createDependencyTracker, tableFromDepKey } from './SCAN_DEP-DLJF8dsj.mjs';
|
|
10
11
|
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-
|
|
12
|
+
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-DVk02KpP.mjs';
|
|
12
13
|
import { LogBuffer } from './LogBuffer-B_Ezju_N.mjs';
|
|
13
14
|
import { recordCapturedMail, clearCapturedMail, readCapturedMail, MAIL_TABLE } from './MAIL_RETENTION-CPpgl-dX.mjs';
|
|
15
|
+
import { stableStringify } from './stableStringify-mC40mZts.mjs';
|
|
14
16
|
import { readBookmark, armRestore } from './armRestore-4Px61hHS.mjs';
|
|
15
17
|
import { ReactiveCache, reactiveCacheKey } from './ReactiveCache-DnSvbjil.mjs';
|
|
16
18
|
import { stableWireKey } from './stableWireKey-DKuXO7T5.mjs';
|
|
@@ -22,35 +24,10 @@ import { runReadonlySql } from './MAX_SQL_ROWS-iFAA8FbD.mjs';
|
|
|
22
24
|
import { ConflictError } from './ConflictError-CLoq37xH.mjs';
|
|
23
25
|
import { e as deleteGlobalShapeSnapshotsForConnection, g as readIdempotent, h as writeIdempotent, t as trimIdempotent, r as readClientWatermark, m as migrateClientWatermark, c as advanceClientWatermark, d as deleteGlobalShapeSnapshot, f as readGlobalShapeSnapshot, w as writeGlobalShapeSnapshot } from './ctx-db-idempotency-BdcNpvY4.mjs';
|
|
24
26
|
import { CDC_LOG_TABLE, readCdcChanges, readCdcCursor, readCdcEpoch, minCdcSeq, bumpCdcEpoch } from './CDC_LOG_TABLE-uwOJxJJZ.mjs';
|
|
25
|
-
import { stableStringify } from './stableStringify-mC40mZts.mjs';
|
|
26
27
|
import { a as selectShapeMemberIds, s as selectShapeRows } from './ctx-db-shapes-Cz9dHyh1.mjs';
|
|
27
28
|
|
|
28
29
|
const MAX_BATCH_ENTRIES = 500;
|
|
29
30
|
|
|
30
|
-
const BUFFER_LEVEL = {
|
|
31
|
-
debug: "debug",
|
|
32
|
-
error: "error",
|
|
33
|
-
fatal: "error",
|
|
34
|
-
info: "info",
|
|
35
|
-
log: "info",
|
|
36
|
-
trace: "debug",
|
|
37
|
-
warn: "warn"
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const HEX_ONLY = /^[0-9a-f]+$/;
|
|
41
|
-
const parseTraceparent = (header) => {
|
|
42
|
-
if (header === null || header === void 0) {
|
|
43
|
-
return void 0;
|
|
44
|
-
}
|
|
45
|
-
const parts = header.trim().toLowerCase().split("-");
|
|
46
|
-
const [version, traceId, parentSpanId, flags] = parts;
|
|
47
|
-
if (parts.length < 4 || version === void 0 || version.length !== 2 || !HEX_ONLY.test(version) || version === "ff" || // Version 00 forbids trailing fields; only a future version may carry them.
|
|
48
|
-
version === "00" && parts.length !== 4 || traceId === void 0 || parentSpanId === void 0 || flags === void 0 || flags.length !== 2 || !HEX_ONLY.test(flags) || traceId.length !== 32 || parentSpanId.length !== 16 || !HEX_ONLY.test(traceId) || !HEX_ONLY.test(parentSpanId) || traceId === "00000000000000000000000000000000" || parentSpanId === "0000000000000000") {
|
|
49
|
-
return void 0;
|
|
50
|
-
}
|
|
51
|
-
return { parentSpanId, traceId };
|
|
52
|
-
};
|
|
53
|
-
|
|
54
31
|
const evictOldestEntry = (map, capacity) => {
|
|
55
32
|
if (map.size < capacity) {
|
|
56
33
|
return;
|
|
@@ -116,12 +93,12 @@ const verifyWsAdminToken = async (secret, token, now = Date.now()) => {
|
|
|
116
93
|
|
|
117
94
|
const AUDIT_LOG_TABLE = "__lunora_audit__";
|
|
118
95
|
const AUDIT_LOG_RETENTION = 1e3;
|
|
119
|
-
const runSql$
|
|
96
|
+
const runSql$4 = (sql, query, ...params) => {
|
|
120
97
|
const runner = sql.exec;
|
|
121
98
|
return runner.call(sql, query, ...params);
|
|
122
99
|
};
|
|
123
100
|
const ensureAuditTable = (sql) => {
|
|
124
|
-
runSql$
|
|
101
|
+
runSql$4(
|
|
125
102
|
sql,
|
|
126
103
|
`CREATE TABLE IF NOT EXISTS "${AUDIT_LOG_TABLE}" (
|
|
127
104
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -135,7 +112,7 @@ const ensureAuditTable = (sql) => {
|
|
|
135
112
|
};
|
|
136
113
|
const appendAuditEntry = (sql, entry) => {
|
|
137
114
|
ensureAuditTable(sql);
|
|
138
|
-
runSql$
|
|
115
|
+
runSql$4(
|
|
139
116
|
sql,
|
|
140
117
|
`INSERT INTO "${AUDIT_LOG_TABLE}" (ts, op, "table", id, detail) VALUES (?, ?, ?, ?, ?)`,
|
|
141
118
|
entry.ts,
|
|
@@ -147,13 +124,13 @@ const appendAuditEntry = (sql, entry) => {
|
|
|
147
124
|
// eslint-disable-next-line unicorn/no-null -- SQL NULL is the correct value for an op with no associated table/id/detail.
|
|
148
125
|
entry.detail === void 0 ? null : JSON.stringify(entry.detail)
|
|
149
126
|
);
|
|
150
|
-
runSql$
|
|
127
|
+
runSql$4(sql, `DELETE FROM "${AUDIT_LOG_TABLE}" WHERE seq <= (SELECT MAX(seq) - ? FROM "${AUDIT_LOG_TABLE}")`, AUDIT_LOG_RETENTION);
|
|
151
128
|
};
|
|
152
129
|
const readAuditLog = (sql, options = {}) => {
|
|
153
130
|
ensureAuditTable(sql);
|
|
154
131
|
const sinceSeq = options.sinceSeq ?? 0;
|
|
155
132
|
const limit = Math.max(1, Math.min(options.limit ?? AUDIT_LOG_RETENTION, 1e4));
|
|
156
|
-
const rows = runSql$
|
|
133
|
+
const rows = runSql$4(
|
|
157
134
|
sql,
|
|
158
135
|
`SELECT seq, ts, op, "table", id, detail FROM "${AUDIT_LOG_TABLE}" WHERE seq > ? ORDER BY seq DESC LIMIT ?`,
|
|
159
136
|
sinceSeq,
|
|
@@ -206,6 +183,164 @@ const buildBatchEntryRequest = (batchRequest, entry) => {
|
|
|
206
183
|
});
|
|
207
184
|
};
|
|
208
185
|
|
|
186
|
+
const ISSUE_STATE_TABLE = "__lunora_issue_state__";
|
|
187
|
+
const HASH_QUERY_BATCH = 100;
|
|
188
|
+
const ISSUE_STATUSES = ["ignored", "open", "resolved"];
|
|
189
|
+
const ISSUE_SEVERITIES = ["critical", "high", "low", "medium"];
|
|
190
|
+
const runSql$3 = (sql, query, ...parameters) => {
|
|
191
|
+
const runner = sql.exec;
|
|
192
|
+
return runner.call(sql, query, ...parameters);
|
|
193
|
+
};
|
|
194
|
+
const SQL_NULL = null;
|
|
195
|
+
const orNull$1 = (value) => value ?? SQL_NULL;
|
|
196
|
+
const ensureIssueStateTable = (sql) => {
|
|
197
|
+
runSql$3(
|
|
198
|
+
sql,
|
|
199
|
+
`CREATE TABLE IF NOT EXISTS "${ISSUE_STATE_TABLE}" (
|
|
200
|
+
hash TEXT PRIMARY KEY,
|
|
201
|
+
status TEXT NOT NULL DEFAULT 'open',
|
|
202
|
+
assignee TEXT,
|
|
203
|
+
severity TEXT,
|
|
204
|
+
updated_at REAL NOT NULL,
|
|
205
|
+
updated_by TEXT
|
|
206
|
+
)`
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
const hydrate = (row) => {
|
|
210
|
+
return {
|
|
211
|
+
...row.assignee === null ? {} : { assignee: row.assignee },
|
|
212
|
+
hash: row.hash,
|
|
213
|
+
...row.severity === null ? {} : { severity: row.severity },
|
|
214
|
+
status: row.status,
|
|
215
|
+
updatedAt: row.updated_at,
|
|
216
|
+
...row.updated_by === null ? {} : { updatedBy: row.updated_by }
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
const readIssueStates = (sql, hashes) => {
|
|
220
|
+
const states = /* @__PURE__ */ new Map();
|
|
221
|
+
if (hashes.length === 0) {
|
|
222
|
+
return states;
|
|
223
|
+
}
|
|
224
|
+
ensureIssueStateTable(sql);
|
|
225
|
+
for (let start = 0; start < hashes.length; start += HASH_QUERY_BATCH) {
|
|
226
|
+
const batch = hashes.slice(start, start + HASH_QUERY_BATCH);
|
|
227
|
+
const placeholders = batch.map(() => "?").join(", ");
|
|
228
|
+
const rows = runSql$3(
|
|
229
|
+
sql,
|
|
230
|
+
`SELECT hash, status, assignee, severity, updated_at, updated_by FROM "${ISSUE_STATE_TABLE}" WHERE hash IN (${placeholders})`,
|
|
231
|
+
...batch
|
|
232
|
+
).toArray();
|
|
233
|
+
for (const row of rows) {
|
|
234
|
+
states.set(row.hash, hydrate(row));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return states;
|
|
238
|
+
};
|
|
239
|
+
const upsertIssueState = (sql, hash, patch, updatedAt, updatedBy) => {
|
|
240
|
+
ensureIssueStateTable(sql);
|
|
241
|
+
const status = orNull$1(patch.status);
|
|
242
|
+
const assignee = orNull$1(patch.assignee);
|
|
243
|
+
const clearAssignee = patch.assignee === null ? 1 : 0;
|
|
244
|
+
const severity = orNull$1(patch.severity);
|
|
245
|
+
const clearSeverity = patch.severity === null ? 1 : 0;
|
|
246
|
+
const actor = orNull$1(updatedBy);
|
|
247
|
+
runSql$3(
|
|
248
|
+
sql,
|
|
249
|
+
`INSERT INTO "${ISSUE_STATE_TABLE}" (hash, status, assignee, severity, updated_at, updated_by)
|
|
250
|
+
VALUES (?, COALESCE(?, 'open'), ?, ?, ?, ?)
|
|
251
|
+
ON CONFLICT(hash) DO UPDATE SET
|
|
252
|
+
status = COALESCE(?, status),
|
|
253
|
+
assignee = CASE WHEN ? = 1 THEN NULL ELSE COALESCE(?, assignee) END,
|
|
254
|
+
severity = CASE WHEN ? = 1 THEN NULL ELSE COALESCE(?, severity) END,
|
|
255
|
+
updated_at = ?,
|
|
256
|
+
updated_by = ?`,
|
|
257
|
+
hash,
|
|
258
|
+
status,
|
|
259
|
+
assignee,
|
|
260
|
+
severity,
|
|
261
|
+
updatedAt,
|
|
262
|
+
actor,
|
|
263
|
+
status,
|
|
264
|
+
clearAssignee,
|
|
265
|
+
assignee,
|
|
266
|
+
clearSeverity,
|
|
267
|
+
severity,
|
|
268
|
+
updatedAt,
|
|
269
|
+
actor
|
|
270
|
+
);
|
|
271
|
+
const [row] = runSql$3(
|
|
272
|
+
sql,
|
|
273
|
+
`SELECT hash, status, assignee, severity, updated_at, updated_by FROM "${ISSUE_STATE_TABLE}" WHERE hash = ?`,
|
|
274
|
+
hash
|
|
275
|
+
).toArray();
|
|
276
|
+
return row === void 0 ? { hash, status: status ?? "open", updatedAt, ...updatedBy === void 0 ? {} : { updatedBy } } : hydrate(row);
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const DEFAULT_CAPACITY$1 = 256;
|
|
280
|
+
const seriesKey = (event) => `${event.kind}\0${event.name}\0${stableStringify(event.attributes ?? {})}`;
|
|
281
|
+
class MetricBuffer {
|
|
282
|
+
capacity;
|
|
283
|
+
series = /* @__PURE__ */ new Map();
|
|
284
|
+
constructor(capacity = DEFAULT_CAPACITY$1) {
|
|
285
|
+
this.capacity = capacity > 0 ? Math.trunc(capacity) : DEFAULT_CAPACITY$1;
|
|
286
|
+
}
|
|
287
|
+
/** Number of distinct series currently aggregated. */
|
|
288
|
+
get size() {
|
|
289
|
+
return this.series.size;
|
|
290
|
+
}
|
|
291
|
+
/** Drop every aggregated series. */
|
|
292
|
+
clear() {
|
|
293
|
+
this.series.clear();
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Snapshot of the aggregated series, most-recently-updated first, each a fresh
|
|
297
|
+
* copy so a caller can't mutate the live aggregate. `series` is kept in
|
|
298
|
+
* update order (tail = newest), so one reverse yields newest-first.
|
|
299
|
+
*/
|
|
300
|
+
entries() {
|
|
301
|
+
return [...this.series.values()].toReversed().map((s) => {
|
|
302
|
+
return { ...s };
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
/** Fold one measurement into its series, creating or updating the aggregate. */
|
|
306
|
+
push(event) {
|
|
307
|
+
const key = seriesKey(event);
|
|
308
|
+
const existing = this.series.get(key);
|
|
309
|
+
if (existing === void 0) {
|
|
310
|
+
if (this.series.size >= this.capacity) {
|
|
311
|
+
const oldest = this.series.keys().next().value;
|
|
312
|
+
if (oldest !== void 0) {
|
|
313
|
+
this.series.delete(oldest);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
this.series.set(key, {
|
|
317
|
+
...event.attributes === void 0 ? {} : { attributes: event.attributes },
|
|
318
|
+
count: 1,
|
|
319
|
+
firstTs: event.ts,
|
|
320
|
+
functionPath: event.functionPath,
|
|
321
|
+
kind: event.kind,
|
|
322
|
+
last: event.value,
|
|
323
|
+
lastTs: event.ts,
|
|
324
|
+
max: event.value,
|
|
325
|
+
min: event.value,
|
|
326
|
+
name: event.name,
|
|
327
|
+
...event.shardKey === void 0 ? {} : { shardKey: event.shardKey },
|
|
328
|
+
sum: event.value
|
|
329
|
+
});
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
this.series.delete(key);
|
|
333
|
+
existing.count += 1;
|
|
334
|
+
existing.sum += event.value;
|
|
335
|
+
existing.min = Math.min(existing.min, event.value);
|
|
336
|
+
existing.max = Math.max(existing.max, event.value);
|
|
337
|
+
existing.last = event.value;
|
|
338
|
+
existing.lastTs = event.ts;
|
|
339
|
+
existing.functionPath = event.functionPath;
|
|
340
|
+
this.series.set(key, existing);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
209
344
|
const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
|
|
210
345
|
const QUERY_METRICS_MAX_SQL_LEN = 512;
|
|
211
346
|
const QUERY_METRICS_MAX_STATEMENTS = 500;
|
|
@@ -1182,35 +1317,6 @@ const createRelayLink = (host) => {
|
|
|
1182
1317
|
return parsed === void 0 ? new OwnerRelay(host, name) : new RelayMember(host, parsed.ownerKey, parsed.relayIndex);
|
|
1183
1318
|
};
|
|
1184
1319
|
|
|
1185
|
-
const stringifyFieldValue = (value) => {
|
|
1186
|
-
if (typeof value === "string") {
|
|
1187
|
-
return value;
|
|
1188
|
-
}
|
|
1189
|
-
try {
|
|
1190
|
-
return JSON.stringify(value) ?? String(value);
|
|
1191
|
-
} catch {
|
|
1192
|
-
return String(value);
|
|
1193
|
-
}
|
|
1194
|
-
};
|
|
1195
|
-
const coerceFieldValue = (value) => typeof value === "boolean" || typeof value === "number" || typeof value === "string" ? value : stringifyFieldValue(value);
|
|
1196
|
-
const normalizeLogFields = (perCall, bound) => {
|
|
1197
|
-
if (perCall === void 0 && bound === void 0) {
|
|
1198
|
-
return void 0;
|
|
1199
|
-
}
|
|
1200
|
-
const merged = {};
|
|
1201
|
-
if (bound !== void 0) {
|
|
1202
|
-
for (const [key, value] of Object.entries(bound)) {
|
|
1203
|
-
merged[key] = coerceFieldValue(value);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
if (perCall !== void 0) {
|
|
1207
|
-
for (const [key, value] of Object.entries(perCall)) {
|
|
1208
|
-
merged[key] = coerceFieldValue(value);
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
return Object.keys(merged).length === 0 ? void 0 : merged;
|
|
1212
|
-
};
|
|
1213
|
-
|
|
1214
1320
|
const REQUEST_LOG_TABLE = "__lunora_reqlog__";
|
|
1215
1321
|
const REQUEST_LOG_RETENTION = 1e3;
|
|
1216
1322
|
const REQUEST_LOG_EVENT_SOURCE = "lunora";
|
|
@@ -1439,6 +1545,23 @@ const readRequestLog = (sql, options = {}) => {
|
|
|
1439
1545
|
return base;
|
|
1440
1546
|
});
|
|
1441
1547
|
};
|
|
1548
|
+
const applyIssueStates = (sql, issues) => {
|
|
1549
|
+
const states = readIssueStates(sql, [...issues.keys()]);
|
|
1550
|
+
for (const issue of issues.values()) {
|
|
1551
|
+
const state = states.get(issue.hash);
|
|
1552
|
+
if (state === void 0) {
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
issue.stateUpdatedAt = state.updatedAt;
|
|
1556
|
+
if (state.assignee !== void 0) {
|
|
1557
|
+
issue.assignee = state.assignee;
|
|
1558
|
+
}
|
|
1559
|
+
if (state.severity !== void 0) {
|
|
1560
|
+
issue.severity = state.severity;
|
|
1561
|
+
}
|
|
1562
|
+
issue.status = state.status === "resolved" && issue.lastSeen > state.updatedAt ? "open" : state.status;
|
|
1563
|
+
}
|
|
1564
|
+
};
|
|
1442
1565
|
const readErrorIssues = (sql, options = {}) => {
|
|
1443
1566
|
ensureRequestLogTable(sql);
|
|
1444
1567
|
const limit = Math.max(1, Math.min(options.limit ?? REQUEST_LOG_RETENTION, 1e4));
|
|
@@ -1470,7 +1593,7 @@ const readErrorIssues = (sql, options = {}) => {
|
|
|
1470
1593
|
const { culprit, hash, title } = fingerprintError({ functionPath: row.function_path, message });
|
|
1471
1594
|
const existing = issues.get(hash);
|
|
1472
1595
|
if (existing === void 0) {
|
|
1473
|
-
issues.set(hash, { count: 1, culprit, firstSeen: row.ts, hash, lastSeen: row.ts, sampleMessage: message, title });
|
|
1596
|
+
issues.set(hash, { count: 1, culprit, firstSeen: row.ts, hash, lastSeen: row.ts, sampleMessage: message, status: "open", title });
|
|
1474
1597
|
sampleTs.set(hash, row.ts);
|
|
1475
1598
|
continue;
|
|
1476
1599
|
}
|
|
@@ -1483,7 +1606,10 @@ const readErrorIssues = (sql, options = {}) => {
|
|
|
1483
1606
|
existing.title = title;
|
|
1484
1607
|
}
|
|
1485
1608
|
}
|
|
1486
|
-
|
|
1609
|
+
applyIssueStates(sql, issues);
|
|
1610
|
+
const folded = [...issues.values()];
|
|
1611
|
+
const filtered = options.status === void 0 ? folded : folded.filter((issue) => issue.status === options.status);
|
|
1612
|
+
return filtered.toSorted((a, b) => b.lastSeen - a.lastSeen);
|
|
1487
1613
|
};
|
|
1488
1614
|
|
|
1489
1615
|
const runSocketPool = async (items, processOne, concurrency = 8) => {
|
|
@@ -1503,6 +1629,136 @@ const runSocketPool = async (items, processOne, concurrency = 8) => {
|
|
|
1503
1629
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
1504
1630
|
};
|
|
1505
1631
|
|
|
1632
|
+
const DEFAULT_CAPACITY = 500;
|
|
1633
|
+
class SpanBuffer {
|
|
1634
|
+
buffer = [];
|
|
1635
|
+
capacity;
|
|
1636
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
1637
|
+
this.capacity = capacity > 0 ? Math.trunc(capacity) : DEFAULT_CAPACITY;
|
|
1638
|
+
}
|
|
1639
|
+
/** Number of spans currently buffered. */
|
|
1640
|
+
get size() {
|
|
1641
|
+
return this.buffer.length;
|
|
1642
|
+
}
|
|
1643
|
+
/** Drop every buffered span. */
|
|
1644
|
+
clear() {
|
|
1645
|
+
this.buffer.length = 0;
|
|
1646
|
+
}
|
|
1647
|
+
/** Snapshot of the buffered spans in insertion order. Fresh array per call. */
|
|
1648
|
+
entries() {
|
|
1649
|
+
return [...this.buffer];
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Whether any buffered span belongs to `traceId`. A membership test rather
|
|
1653
|
+
* than `entries().some(...)` so the per-dispatch check that decides whether
|
|
1654
|
+
* to record a root span doesn't copy the whole ring on every request.
|
|
1655
|
+
*/
|
|
1656
|
+
hasTrace(traceId) {
|
|
1657
|
+
return this.buffer.some((span) => span.traceId === traceId);
|
|
1658
|
+
}
|
|
1659
|
+
/** Append a span, evicting the oldest when at capacity. */
|
|
1660
|
+
push(span) {
|
|
1661
|
+
this.buffer.push(span);
|
|
1662
|
+
if (this.buffer.length > this.capacity) {
|
|
1663
|
+
this.buffer.shift();
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const DEFAULT_TRACE_LIMIT = 50;
|
|
1668
|
+
const groupByTrace = (spans) => {
|
|
1669
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
1670
|
+
for (const span of spans) {
|
|
1671
|
+
const group = byTrace.get(span.traceId);
|
|
1672
|
+
if (group === void 0) {
|
|
1673
|
+
byTrace.set(span.traceId, [span]);
|
|
1674
|
+
} else {
|
|
1675
|
+
group.push(span);
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return byTrace;
|
|
1679
|
+
};
|
|
1680
|
+
const selectAnchor = (group, byId) => {
|
|
1681
|
+
const declaredRoot = group.find((span) => span.dispatch === true);
|
|
1682
|
+
if (declaredRoot !== void 0) {
|
|
1683
|
+
return declaredRoot;
|
|
1684
|
+
}
|
|
1685
|
+
const byStart = group.toSorted((a, b) => a.startTs - b.startTs);
|
|
1686
|
+
return byStart.find((span) => !byId.has(span.parentSpanId)) ?? byStart[0];
|
|
1687
|
+
};
|
|
1688
|
+
const depthResolver = (anchor, byId) => {
|
|
1689
|
+
const cache = /* @__PURE__ */ new Map([[anchor.spanId, 0]]);
|
|
1690
|
+
return (span) => {
|
|
1691
|
+
const chain = [];
|
|
1692
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1693
|
+
let current = span;
|
|
1694
|
+
let base = 0;
|
|
1695
|
+
for (; ; ) {
|
|
1696
|
+
const known = cache.get(current.spanId);
|
|
1697
|
+
if (known !== void 0) {
|
|
1698
|
+
base = known;
|
|
1699
|
+
break;
|
|
1700
|
+
}
|
|
1701
|
+
if (seen.has(current.spanId)) {
|
|
1702
|
+
break;
|
|
1703
|
+
}
|
|
1704
|
+
seen.add(current.spanId);
|
|
1705
|
+
chain.push(current);
|
|
1706
|
+
const parent = byId.get(current.parentSpanId);
|
|
1707
|
+
if (parent === void 0) {
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
current = parent;
|
|
1711
|
+
}
|
|
1712
|
+
for (const [index, entry] of chain.toReversed().entries()) {
|
|
1713
|
+
cache.set(entry.spanId, base + index + 1);
|
|
1714
|
+
}
|
|
1715
|
+
return cache.get(span.spanId) ?? base;
|
|
1716
|
+
};
|
|
1717
|
+
};
|
|
1718
|
+
const foldTraces = (spans, limit = DEFAULT_TRACE_LIMIT) => {
|
|
1719
|
+
const byTrace = groupByTrace(spans);
|
|
1720
|
+
const selected = [...byTrace.entries()].map(([traceId, group]) => {
|
|
1721
|
+
return { group, startTs: Math.min(...group.map((span) => span.startTs)), traceId };
|
|
1722
|
+
}).toSorted((a, b) => b.startTs - a.startTs).slice(0, limit);
|
|
1723
|
+
const summaries = [];
|
|
1724
|
+
for (const { group, traceId } of selected) {
|
|
1725
|
+
const byId = new Map(group.map((span) => [span.spanId, span]));
|
|
1726
|
+
const anchor = selectAnchor(group, byId);
|
|
1727
|
+
if (anchor === void 0) {
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
const depthOf = depthResolver(anchor, byId);
|
|
1731
|
+
const { startTs } = anchor;
|
|
1732
|
+
const endTs = Math.max(...group.map((span) => span.startTs + span.durationMs));
|
|
1733
|
+
const rows = group.map((span) => {
|
|
1734
|
+
return {
|
|
1735
|
+
...span.attributes === void 0 ? {} : { attributes: span.attributes },
|
|
1736
|
+
depth: depthOf(span),
|
|
1737
|
+
durationMs: span.durationMs,
|
|
1738
|
+
...span.error === void 0 ? {} : { error: span.error },
|
|
1739
|
+
name: span.name,
|
|
1740
|
+
// Clamped at 0: a child whose parent was evicted can predate the
|
|
1741
|
+
// anchor, and a negative offset would render as a bar off-canvas.
|
|
1742
|
+
offsetMs: Math.max(0, span.startTs - startTs),
|
|
1743
|
+
ok: span.ok,
|
|
1744
|
+
parentSpanId: span.parentSpanId,
|
|
1745
|
+
spanId: span.spanId
|
|
1746
|
+
};
|
|
1747
|
+
}).toSorted((a, b) => a.offsetMs - b.offsetMs || a.depth - b.depth);
|
|
1748
|
+
summaries.push({
|
|
1749
|
+
durationMs: endTs - startTs,
|
|
1750
|
+
functionPath: anchor.functionPath,
|
|
1751
|
+
ok: group.every((span) => span.ok),
|
|
1752
|
+
rootName: anchor.name,
|
|
1753
|
+
...anchor.shardKey === void 0 ? {} : { shardKey: anchor.shardKey },
|
|
1754
|
+
spans: rows,
|
|
1755
|
+
startTs,
|
|
1756
|
+
traceId
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
return { total: byTrace.size, traces: summaries.toSorted((a, b) => b.startTs - a.startTs) };
|
|
1760
|
+
};
|
|
1761
|
+
|
|
1506
1762
|
const DANGLING_SCAN_CAP = 5e3;
|
|
1507
1763
|
const DANGLING_RESULT_CAP = 500;
|
|
1508
1764
|
const DOC_COLUMN = "__doc__";
|
|
@@ -1618,6 +1874,36 @@ const parseWriteRowArgs = (args) => {
|
|
|
1618
1874
|
}
|
|
1619
1875
|
return { doc: record, id, op, table };
|
|
1620
1876
|
};
|
|
1877
|
+
const isIssueStatus = (value) => typeof value === "string" && ISSUE_STATUSES.includes(value);
|
|
1878
|
+
const isIssueSeverity = (value) => typeof value === "string" && ISSUE_SEVERITIES.includes(value);
|
|
1879
|
+
const parseIssueHash = (args) => {
|
|
1880
|
+
const hash = typeof args["hash"] === "string" ? args["hash"].trim() : "";
|
|
1881
|
+
if (hash === "") {
|
|
1882
|
+
throw new LunoraError("BAD_REQUEST", "issue triage: `hash` is required");
|
|
1883
|
+
}
|
|
1884
|
+
return hash;
|
|
1885
|
+
};
|
|
1886
|
+
const CLEAR = null;
|
|
1887
|
+
const parseAssigneeArgument = (args) => {
|
|
1888
|
+
const raw = args["assignee"];
|
|
1889
|
+
if (raw === null) {
|
|
1890
|
+
return CLEAR;
|
|
1891
|
+
}
|
|
1892
|
+
if (typeof raw === "string" && raw.trim() !== "") {
|
|
1893
|
+
return raw;
|
|
1894
|
+
}
|
|
1895
|
+
throw new LunoraError("BAD_REQUEST", "assignIssue: `assignee` must be a non-empty string (assign) or null (unassign)");
|
|
1896
|
+
};
|
|
1897
|
+
const parseSeverityArgument = (args) => {
|
|
1898
|
+
const raw = args["severity"];
|
|
1899
|
+
if (raw === null) {
|
|
1900
|
+
return CLEAR;
|
|
1901
|
+
}
|
|
1902
|
+
if (isIssueSeverity(raw)) {
|
|
1903
|
+
return raw;
|
|
1904
|
+
}
|
|
1905
|
+
throw new LunoraError("BAD_REQUEST", "setIssueSeverity: `severity` must be one of critical|high|medium|low, or null to clear");
|
|
1906
|
+
};
|
|
1621
1907
|
const parseCreateWorkflowInstanceArgs = (args) => {
|
|
1622
1908
|
const exportName = typeof args["exportName"] === "string" ? args["exportName"].trim() : "";
|
|
1623
1909
|
if (exportName === "") {
|
|
@@ -2239,6 +2525,14 @@ class ShardDO {
|
|
|
2239
2525
|
currentRequestIp;
|
|
2240
2526
|
/** W3C `traceparent` of the inbound RPC; forwarded onto outbound container fetches. */
|
|
2241
2527
|
currentRequestTraceparent;
|
|
2528
|
+
/**
|
|
2529
|
+
* Trace ids for the in-flight dispatch, resolved once at entry (see
|
|
2530
|
+
* {@link resolveTraceAnchor}). Shared by `ctx.trace` and by the synthetic
|
|
2531
|
+
* root span recorded on the way out so both agree even with no inbound
|
|
2532
|
+
* `traceparent`. Cleared in the same `finally` as the other per-request
|
|
2533
|
+
* fields.
|
|
2534
|
+
*/
|
|
2535
|
+
currentRequestTrace;
|
|
2242
2536
|
/**
|
|
2243
2537
|
* Client-issued idempotency key for the in-flight mutation, forwarded via the
|
|
2244
2538
|
* `x-lunora-mutation-id` header. When set, the dispatch path dedups the call
|
|
@@ -2436,6 +2730,19 @@ class ShardDO {
|
|
|
2436
2730
|
* "recent RPC errors on this instance" feed, not a general application log.
|
|
2437
2731
|
*/
|
|
2438
2732
|
logs = new LogBuffer();
|
|
2733
|
+
/**
|
|
2734
|
+
* Recent `ctx.trace` spans (plus the synthetic per-dispatch root), powering
|
|
2735
|
+
* the studio's Traces panel. In-memory and hibernation-volatile like
|
|
2736
|
+
* `logs` — production tracing ships to a collector via `otlpSink`.
|
|
2737
|
+
*/
|
|
2738
|
+
spans = new SpanBuffer();
|
|
2739
|
+
/**
|
|
2740
|
+
* Running aggregates of `ctx.metrics.*` measurements, powering the studio's
|
|
2741
|
+
* Metrics panel. In-memory and hibernation-volatile like `logs` and
|
|
2742
|
+
* `spans`, but folds samples into per-series totals rather than ringing
|
|
2743
|
+
* raw events — production aggregation ships to a collector via the sink.
|
|
2744
|
+
*/
|
|
2745
|
+
metricSeries = new MetricBuffer();
|
|
2439
2746
|
/**
|
|
2440
2747
|
* In-flight dependency tracker for the currently-executing query. Set by
|
|
2441
2748
|
* `runCachedQuery` so the ctx-db hooks (wired via `onRead`) can
|
|
@@ -2527,6 +2834,7 @@ class ShardDO {
|
|
|
2527
2834
|
* Worker-side fetch entry point. Handles WebSocket upgrades and the
|
|
2528
2835
|
* shard-local RPC endpoint forwarded by `@lunora/runtime`.
|
|
2529
2836
|
*/
|
|
2837
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- the DO's central request router; #149 added the trace-anchor capture and root-span record, tipping it to 16. Splitting the request lifecycle across helpers would hurt readability more than the +1 costs.
|
|
2530
2838
|
async fetch(request) {
|
|
2531
2839
|
const url = new URL(request.url);
|
|
2532
2840
|
this.shardBinding = request.headers.get("x-lunora-shard-binding") ?? this.shardBinding;
|
|
@@ -2558,6 +2866,8 @@ class ShardDO {
|
|
|
2558
2866
|
this.currentRequestIp = request.headers.get("x-lunora-client-ip") ?? void 0;
|
|
2559
2867
|
this.currentRequestSystem = request.headers.get("x-lunora-system") === "1";
|
|
2560
2868
|
this.currentRequestTraceparent = request.headers.get("traceparent") ?? void 0;
|
|
2869
|
+
this.currentRequestTrace = resolveTraceAnchor(this.currentRequestTraceparent);
|
|
2870
|
+
const dispatchTrace = this.currentRequestTrace;
|
|
2561
2871
|
this.currentRequestReadTables = void 0;
|
|
2562
2872
|
this.currentRequestCacheHit = void 0;
|
|
2563
2873
|
this.metrics.requests += 1;
|
|
@@ -2565,6 +2875,7 @@ class ShardDO {
|
|
|
2565
2875
|
this.currentScannedTables = /* @__PURE__ */ new Set();
|
|
2566
2876
|
this.currentIndexHits = /* @__PURE__ */ new Set();
|
|
2567
2877
|
this.currentStmtSamples = [];
|
|
2878
|
+
let dispatchError;
|
|
2568
2879
|
try {
|
|
2569
2880
|
if (payload.functionPath.startsWith(RELATION_FUNCTION_PREFIX)) {
|
|
2570
2881
|
const value = await this.runRelationFanoutRead(payload.functionPath, payload.args ?? {});
|
|
@@ -2596,6 +2907,7 @@ class ShardDO {
|
|
|
2596
2907
|
return response;
|
|
2597
2908
|
} catch (error) {
|
|
2598
2909
|
this.metrics.errors += 1;
|
|
2910
|
+
dispatchError = { thrown: error };
|
|
2599
2911
|
const durationMs = Date.now() - dispatchStartedAt;
|
|
2600
2912
|
const message = error instanceof Error ? error.message : String(error);
|
|
2601
2913
|
const conflicted = error instanceof ConflictError && error.kind === "occ";
|
|
@@ -2615,6 +2927,10 @@ class ShardDO {
|
|
|
2615
2927
|
await this.flushChangedTables();
|
|
2616
2928
|
return this.errorToResponse(error);
|
|
2617
2929
|
} finally {
|
|
2930
|
+
if (this.spans.hasTrace(dispatchTrace.traceId)) {
|
|
2931
|
+
this.recordDispatchRootSpan(payload.functionPath, dispatchStartedAt, dispatchError, dispatchTrace);
|
|
2932
|
+
}
|
|
2933
|
+
this.currentRequestTrace = void 0;
|
|
2618
2934
|
this.currentRequestBookmark = void 0;
|
|
2619
2935
|
this.currentResponseBookmark = void 0;
|
|
2620
2936
|
this.currentRequestUserId = void 0;
|
|
@@ -3084,6 +3400,14 @@ class ShardDO {
|
|
|
3084
3400
|
getCurrentTraceparent() {
|
|
3085
3401
|
return this.currentRequestTraceparent;
|
|
3086
3402
|
}
|
|
3403
|
+
/**
|
|
3404
|
+
* The in-flight dispatch's trace ids, for the generated `buildCtx` to hand to
|
|
3405
|
+
* {@link makeTracer}. `undefined` outside a dispatch (an alarm, a lifecycle
|
|
3406
|
+
* hook), where the tracer mints its own anchor.
|
|
3407
|
+
*/
|
|
3408
|
+
getCurrentTrace() {
|
|
3409
|
+
return this.currentRequestTrace;
|
|
3410
|
+
}
|
|
3087
3411
|
/**
|
|
3088
3412
|
* Identity claims (email, name, roles, …) forwarded by the runtime's
|
|
3089
3413
|
* `resolveIdentity` hook. Returns `undefined` for anonymous requests
|
|
@@ -4158,7 +4482,7 @@ class ShardDO {
|
|
|
4158
4482
|
* chose to log them, exactly like a raw `console.log`.
|
|
4159
4483
|
*/
|
|
4160
4484
|
recordUserLog(functionPath, level, args, message, fields, sink) {
|
|
4161
|
-
const trace =
|
|
4485
|
+
const trace = this.currentRequestTrace;
|
|
4162
4486
|
const event = {
|
|
4163
4487
|
args,
|
|
4164
4488
|
fields,
|
|
@@ -4166,12 +4490,12 @@ class ShardDO {
|
|
|
4166
4490
|
level,
|
|
4167
4491
|
message,
|
|
4168
4492
|
shardKey: this.state.id?.name,
|
|
4169
|
-
spanId: trace?.
|
|
4493
|
+
spanId: trace?.rootSpanId,
|
|
4170
4494
|
traceId: trace?.traceId,
|
|
4171
4495
|
ts: Date.now(),
|
|
4172
4496
|
userId: this.getCurrentUserId()
|
|
4173
4497
|
};
|
|
4174
|
-
this.logs.push({ fields, functionPath, level
|
|
4498
|
+
this.logs.push({ fields, functionPath, level, message, timestamp: event.ts });
|
|
4175
4499
|
try {
|
|
4176
4500
|
emitLogEvent(event);
|
|
4177
4501
|
} catch {
|
|
@@ -4196,16 +4520,138 @@ class ShardDO {
|
|
|
4196
4520
|
this.recordUserLog(functionPath, level, args, message, fields, sink);
|
|
4197
4521
|
};
|
|
4198
4522
|
return {
|
|
4199
|
-
debug: (...args) =>
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4523
|
+
debug: (...args) => {
|
|
4524
|
+
emit("debug", args);
|
|
4525
|
+
},
|
|
4526
|
+
error: (...args) => {
|
|
4527
|
+
emit("error", args);
|
|
4528
|
+
},
|
|
4529
|
+
fatal: (...args) => {
|
|
4530
|
+
emit("fatal", args);
|
|
4531
|
+
},
|
|
4532
|
+
info: (...args) => {
|
|
4533
|
+
emit("info", args);
|
|
4534
|
+
},
|
|
4535
|
+
log: (...args) => {
|
|
4536
|
+
emit("log", args);
|
|
4537
|
+
},
|
|
4538
|
+
trace: (...args) => {
|
|
4539
|
+
emit("trace", args);
|
|
4540
|
+
},
|
|
4541
|
+
warn: (...args) => {
|
|
4542
|
+
emit("warn", args);
|
|
4543
|
+
},
|
|
4206
4544
|
with: (fields) => this.makeLogger(functionPath, sink, boundFields ? { ...boundFields, ...fields } : fields)
|
|
4207
4545
|
};
|
|
4208
4546
|
}
|
|
4547
|
+
/**
|
|
4548
|
+
* Build the `ctx.trace` span factory for one dispatched function. The
|
|
4549
|
+
* generated `buildCtx` calls this once per dispatch and assigns the result to
|
|
4550
|
+
* `ctx.trace`.
|
|
4551
|
+
*
|
|
4552
|
+
* Thin wiring over {@link createTracer}, which owns the span semantics (see
|
|
4553
|
+
* there for why nesting is explicit rather than ambient). Everything it needs
|
|
4554
|
+
* from the shard is passed explicitly.
|
|
4555
|
+
*
|
|
4556
|
+
* `anchor` is the trace this ctx's spans belong to; omit it for a ctx with no
|
|
4557
|
+
* owning dispatch (an alarm, a subscription re-run) to mint a fresh anchor, so
|
|
4558
|
+
* `ctx.trace` still yields a coherent self-contained trace there.
|
|
4559
|
+
*/
|
|
4560
|
+
makeTracer(functionPath, sink, anchor) {
|
|
4561
|
+
return createTracer({
|
|
4562
|
+
anchor: anchor ?? resolveTraceAnchor(void 0),
|
|
4563
|
+
functionPath,
|
|
4564
|
+
record: (span) => {
|
|
4565
|
+
this.recordSpan(span, sink);
|
|
4566
|
+
},
|
|
4567
|
+
shardKey: this.state.id?.name,
|
|
4568
|
+
userId: () => this.getCurrentUserId()
|
|
4569
|
+
});
|
|
4570
|
+
}
|
|
4571
|
+
/**
|
|
4572
|
+
* Build the `ctx.metrics` recorder for one dispatched function. Thin wiring
|
|
4573
|
+
* over {@link createMetrics}, which owns the instrument semantics.
|
|
4574
|
+
*/
|
|
4575
|
+
makeMetrics(functionPath, sink) {
|
|
4576
|
+
return createMetrics({
|
|
4577
|
+
functionPath,
|
|
4578
|
+
record: (event) => {
|
|
4579
|
+
this.recordMetric(event, sink);
|
|
4580
|
+
},
|
|
4581
|
+
shardKey: this.state.id?.name
|
|
4582
|
+
});
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* Fold one measurement into the in-memory {@link metricSeries} readout and
|
|
4586
|
+
* hand it to the optional `sink.onMetric`. Best-effort like
|
|
4587
|
+
* {@link recordUserLog} and {@link recordSpan}: recording a measurement must
|
|
4588
|
+
* never break the handler that recorded it.
|
|
4589
|
+
*
|
|
4590
|
+
* The buffer folds rather than rings: a measurement's value is in its
|
|
4591
|
+
* aggregate over a window, which a bounded ring of raw samples can't represent
|
|
4592
|
+
* (it evicts the oldest samples, the ones a running total needs). So the
|
|
4593
|
+
* buffer keeps one running aggregate per series and resets on hibernation like
|
|
4594
|
+
* logs and spans — a "recent metrics on this instance" dev readout. Durable,
|
|
4595
|
+
* cross-instance aggregation is still the sink's job.
|
|
4596
|
+
*/
|
|
4597
|
+
recordMetric(event, sink) {
|
|
4598
|
+
try {
|
|
4599
|
+
this.metricSeries.push(event);
|
|
4600
|
+
} catch {
|
|
4601
|
+
}
|
|
4602
|
+
if (sink?.onMetric) {
|
|
4603
|
+
try {
|
|
4604
|
+
sink.onMetric(event, { waitUntil: this.state.waitUntil?.bind(this.state) });
|
|
4605
|
+
} catch {
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
}
|
|
4609
|
+
/**
|
|
4610
|
+
* Buffer the synthetic root span for a finished dispatch. The caller gates
|
|
4611
|
+
* this on the dispatch having actually produced spans (the `hasTrace` check at
|
|
4612
|
+
* the call site): every request minting a root would fill the bounded ring
|
|
4613
|
+
* with single-bar traces from uninstrumented handlers and evict the
|
|
4614
|
+
* instrumented ones the panel exists to show.
|
|
4615
|
+
*
|
|
4616
|
+
* `anchor` carries the dispatch's trace ids, captured at entry rather than
|
|
4617
|
+
* read from `this` here — this runs after the handler's awaits, where the
|
|
4618
|
+
* shared field may already belong to an interleaved dispatch.
|
|
4619
|
+
*/
|
|
4620
|
+
recordDispatchRootSpan(functionPath, startedAt, failure, anchor) {
|
|
4621
|
+
try {
|
|
4622
|
+
this.spans.push(
|
|
4623
|
+
dispatchRootSpan({
|
|
4624
|
+
anchor,
|
|
4625
|
+
durationMs: Date.now() - startedAt,
|
|
4626
|
+
failure,
|
|
4627
|
+
functionPath,
|
|
4628
|
+
shardKey: this.state.id?.name,
|
|
4629
|
+
startTs: startedAt,
|
|
4630
|
+
userId: this.getCurrentUserId()
|
|
4631
|
+
})
|
|
4632
|
+
);
|
|
4633
|
+
} catch {
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
/**
|
|
4637
|
+
* Buffer one span for the studio Traces panel and hand it to the optional
|
|
4638
|
+
* `sink.onSpan`. Best-effort throughout, exactly like {@link recordUserLog}:
|
|
4639
|
+
* a span is recorded *after* its body already settled, so letting a telemetry
|
|
4640
|
+
* failure escape here would turn a succeeded operation into a failed request.
|
|
4641
|
+
*/
|
|
4642
|
+
// eslint-disable-next-line @typescript-eslint/member-ordering -- kept in the span-recording cluster (next to recordDispatchRootSpan) for cohesion rather than hoisted above every private member
|
|
4643
|
+
recordSpan(span, sink) {
|
|
4644
|
+
try {
|
|
4645
|
+
this.spans.push(span);
|
|
4646
|
+
} catch {
|
|
4647
|
+
}
|
|
4648
|
+
if (sink?.onSpan) {
|
|
4649
|
+
try {
|
|
4650
|
+
sink.onSpan(span, { waitUntil: this.state.waitUntil?.bind(this.state) });
|
|
4651
|
+
} catch {
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
}
|
|
4209
4655
|
/**
|
|
4210
4656
|
* Assemble the per-socket {@link LifecycleDispatchInfo} from its attachment:
|
|
4211
4657
|
* the verified identity to replay and the {@link LifecycleEvent} the hooks
|
|
@@ -4652,8 +5098,63 @@ class ShardDO {
|
|
|
4652
5098
|
if (functionPath === ADMIN_FUNCTIONS.listFlags) {
|
|
4653
5099
|
return this.handleListFlags(args);
|
|
4654
5100
|
}
|
|
5101
|
+
const triaged = await this.handleIssueTriageOp(functionPath, args);
|
|
5102
|
+
if (triaged !== void 0) {
|
|
5103
|
+
return triaged;
|
|
5104
|
+
}
|
|
4655
5105
|
return this.handlePitrAdminOp(functionPath, args);
|
|
4656
5106
|
}
|
|
5107
|
+
/**
|
|
5108
|
+
* Serve the four Issue-triage admin writes — `resolveIssue` / `ignoreIssue`
|
|
5109
|
+
* (a status change), `assignIssue` (set/clear an owner), `setIssueSeverity`
|
|
5110
|
+
* (tag/clear severity). Each upserts one row in the reserved
|
|
5111
|
+
* `__lunora_issue_state__` side table keyed by the Issue's fingerprint
|
|
5112
|
+
* `hash`, then re-derives at read time in {@link readErrorIssues}. Returns
|
|
5113
|
+
* `undefined` for any path it doesn't own so `handleExtraAdminOp` falls
|
|
5114
|
+
* through. Admin-gated by `handleAdminRpc`'s caller (the `LUNORA_ADMIN_TOKEN`
|
|
5115
|
+
* bearer); a bad/missing `hash` (or a bad status/severity value) is a 400.
|
|
5116
|
+
*
|
|
5117
|
+
* The write lands through raw SQL the change-tracker can't observe, so it
|
|
5118
|
+
* marks {@link ISSUE_STATE_TABLE} changed and flushes — the live Issues
|
|
5119
|
+
* subscription is an admin-wildcard memo that re-runs whenever a flush finds
|
|
5120
|
+
* a changed table, so the triage shows up without an unrelated write.
|
|
5121
|
+
*/
|
|
5122
|
+
async handleIssueTriageOp(functionPath, args) {
|
|
5123
|
+
const patch = this.parseIssueTriagePatch(functionPath, args);
|
|
5124
|
+
if (patch === void 0) {
|
|
5125
|
+
return void 0;
|
|
5126
|
+
}
|
|
5127
|
+
const hash = parseIssueHash(args);
|
|
5128
|
+
const updatedBy = typeof args["updatedBy"] === "string" ? args["updatedBy"] : void 0;
|
|
5129
|
+
const sql = this.state.storage.sql;
|
|
5130
|
+
const state = upsertIssueState(sql, hash, patch, Date.now(), updatedBy);
|
|
5131
|
+
this.recordChangedTable(ISSUE_STATE_TABLE);
|
|
5132
|
+
await this.flushChangedTables();
|
|
5133
|
+
this.recordAudit(functionPath.slice(ADMIN_FUNCTION_PREFIX.length), { detail: { ...patch, hash } });
|
|
5134
|
+
return jsonResponse({ result: { state } }, 200);
|
|
5135
|
+
}
|
|
5136
|
+
/**
|
|
5137
|
+
* Map an Issue-triage admin path to the `IssueStatePatch` it applies, or
|
|
5138
|
+
* `undefined` when the path isn't a triage write. `assignIssue`/
|
|
5139
|
+
* `setIssueSeverity` accept an explicit `null` to CLEAR the field (unassign /
|
|
5140
|
+
* untag); a missing or malformed value is a 400 rather than a silent no-op.
|
|
5141
|
+
*/
|
|
5142
|
+
// eslint-disable-next-line class-methods-use-this -- kept an instance method for symmetry with the other `parse*`/handler seams
|
|
5143
|
+
parseIssueTriagePatch(functionPath, args) {
|
|
5144
|
+
if (functionPath === ADMIN_FUNCTIONS.resolveIssue) {
|
|
5145
|
+
return { status: "resolved" };
|
|
5146
|
+
}
|
|
5147
|
+
if (functionPath === ADMIN_FUNCTIONS.ignoreIssue) {
|
|
5148
|
+
return { status: "ignored" };
|
|
5149
|
+
}
|
|
5150
|
+
if (functionPath === ADMIN_FUNCTIONS.assignIssue) {
|
|
5151
|
+
return { assignee: parseAssigneeArgument(args), status: "open" };
|
|
5152
|
+
}
|
|
5153
|
+
if (functionPath === ADMIN_FUNCTIONS.setIssueSeverity) {
|
|
5154
|
+
return { severity: parseSeverityArgument(args) };
|
|
5155
|
+
}
|
|
5156
|
+
return void 0;
|
|
5157
|
+
}
|
|
4657
5158
|
/**
|
|
4658
5159
|
* Record one app-level auth attempt for the auth-failure SLO (PLAN3 §2.3).
|
|
4659
5160
|
* The worker calls this fire-and-forget (via `waitUntil`) after a top-level
|
|
@@ -5281,6 +5782,7 @@ class ShardDO {
|
|
|
5281
5782
|
* wraps each in the {@link ADMIN_WILDCARD} sentinel, keeping that one fact in
|
|
5282
5783
|
* a single place and `readAdminOp` under its complexity budget.
|
|
5283
5784
|
*/
|
|
5785
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- a flat admin-RPC dispatch chain (one trivial branch per function); #149 added the getTraces/getMetrics branches. A lookup table would obscure the 1:1 path→reader mapping this deliberately keeps.
|
|
5284
5786
|
readAdminWildcardOp(functionPath) {
|
|
5285
5787
|
if (functionPath === ADMIN_FUNCTIONS.listTables) {
|
|
5286
5788
|
return listTables(this.state.storage.sql);
|
|
@@ -5300,6 +5802,13 @@ class ShardDO {
|
|
|
5300
5802
|
if (functionPath === ADMIN_FUNCTIONS.getLogs) {
|
|
5301
5803
|
return { entries: this.logs.entries() };
|
|
5302
5804
|
}
|
|
5805
|
+
if (functionPath === ADMIN_FUNCTIONS.getTraces) {
|
|
5806
|
+
const folded = foldTraces(this.spans.entries());
|
|
5807
|
+
return { total: folded.total, traces: folded.traces };
|
|
5808
|
+
}
|
|
5809
|
+
if (functionPath === ADMIN_FUNCTIONS.getMetricSeries) {
|
|
5810
|
+
return { series: this.metricSeries.entries() };
|
|
5811
|
+
}
|
|
5303
5812
|
if (functionPath === ADMIN_FUNCTIONS.getSettings) {
|
|
5304
5813
|
return buildSettings(this.env);
|
|
5305
5814
|
}
|
|
@@ -5418,6 +5927,7 @@ class ShardDO {
|
|
|
5418
5927
|
functionPathPrefix: typeof args["functionPathPrefix"] === "string" ? args["functionPathPrefix"] : void 0,
|
|
5419
5928
|
limit: typeof args["limit"] === "number" ? args["limit"] : void 0,
|
|
5420
5929
|
shardKey: typeof args["shardKey"] === "string" ? args["shardKey"] : void 0,
|
|
5930
|
+
status: isIssueStatus(args["status"]) ? args["status"] : void 0,
|
|
5421
5931
|
userId: typeof args["userId"] === "string" ? args["userId"] : void 0
|
|
5422
5932
|
})
|
|
5423
5933
|
};
|