@lunora/do 1.0.0-alpha.33 → 1.0.0-alpha.35
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 +4471 -4246
- package/dist/index.d.ts +4471 -4246
- package/dist/index.mjs +4 -3
- package/dist/packem_shared/{ADMIN_FUNCTIONS-CAHLZMj8.mjs → ADMIN_FUNCTIONS-CqVmUVNs.mjs} +2 -0
- package/dist/packem_shared/{ROOT_DO_SIZE_WARN_BYTES-afQHUhyA.mjs → ROOT_DO_SIZE_WARN_BYTES-QSOI2hpe.mjs} +422 -9
- 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-Yiyx2OQB.mjs} +1 -1
- package/package.json +2 -2
package/dist/index.mjs
CHANGED
|
@@ -3,6 +3,7 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
|
|
|
3
3
|
export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
|
|
4
4
|
export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
|
|
5
5
|
export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
|
|
6
|
+
export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-DWfYDxCS.mjs';
|
|
6
7
|
export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-Ca21uDuU.mjs';
|
|
7
8
|
export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
|
|
8
9
|
export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
|
|
@@ -11,7 +12,7 @@ export { diffExternalSource } from './packem_shared/diffExternalSource-CovHfdyo.
|
|
|
11
12
|
export { materializeExternalRows, materializeExternalRowsIncremental, readExternalSourceBaseline, runExternalSourceTick } from './packem_shared/materializeExternalRows-DlQWMlw_.mjs';
|
|
12
13
|
export { isSoftDeleted, isSourceDue, liftSourceId, pullExternalSourceIncrementalTick, pullExternalSourceTick } from './packem_shared/isSoftDeleted-YLKR6JYw.mjs';
|
|
13
14
|
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-
|
|
15
|
+
export { ADMIN_FUNCTIONS, ADMIN_FUNCTION_PREFIX, FLAGS_FUNCTION_PREFIX, RELATION_FUNCTION_PREFIX, facetColumn, listTables, readTablePage, selectMatchingIds } from './packem_shared/ADMIN_FUNCTIONS-CqVmUVNs.mjs';
|
|
15
16
|
export { LogBuffer } from './packem_shared/LogBuffer-B_Ezju_N.mjs';
|
|
16
17
|
export { MAIL_RETENTION, MAIL_TABLE, clearCapturedMail, ensureMailTable, readCapturedMail, recordCapturedMail } from './packem_shared/MAIL_RETENTION-CPpgl-dX.mjs';
|
|
17
18
|
export { default as NotFoundError } from './packem_shared/NotFoundError-C70b9hLw.mjs';
|
|
@@ -19,14 +20,14 @@ export { armRestore, readBookmark } from './packem_shared/armRestore-4Px61hHS.mj
|
|
|
19
20
|
export { applySelect, buildSeekWhere, decodeCursor, encodeCursor, normalizeOrderKeys, softDeleteScope } from './packem_shared/applySelect-WQY8m62C.mjs';
|
|
20
21
|
export { RANK_TIEBREAK, encodePartitionKey, matchesRankStaticWhere, rankTableName, resolveRankPartition, sortColumnName } from './packem_shared/RANK_TIEBREAK-CXhdcA1o.mjs';
|
|
21
22
|
export { ReactiveCache, reactiveCacheKey } from './packem_shared/ReactiveCache-DnSvbjil.mjs';
|
|
22
|
-
export { serveRelationFanout } from './packem_shared/serveRelationFanout-
|
|
23
|
+
export { serveRelationFanout } from './packem_shared/serveRelationFanout-Yiyx2OQB.mjs';
|
|
23
24
|
export { DEFAULT_MAX_RELATION_KEYS, assertFlatPredicate, assertShapeShardable, containsRelationPredicate, isRelationPredicate, resolveRelationPredicates } from './packem_shared/DEFAULT_MAX_RELATION_KEYS-BEan1CRD.mjs';
|
|
24
25
|
export { applyOnDelete, fanOutScalarCounts, resolveWith, runRowValidators } from './packem_shared/applyOnDelete-BXSq3S70.mjs';
|
|
25
26
|
export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_shared/RLS_UNWRAP_SYMBOL-DTvHvRzY.mjs';
|
|
26
27
|
export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
|
|
27
28
|
export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
|
|
28
29
|
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-
|
|
30
|
+
export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-QSOI2hpe.mjs';
|
|
30
31
|
export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
|
|
31
32
|
export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs';
|
|
32
33
|
export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
|
|
@@ -24,6 +24,7 @@ const ADMIN_FUNCTIONS = {
|
|
|
24
24
|
getFanoutMetrics: "__lunora_admin__:getFanoutMetrics",
|
|
25
25
|
getFunctionStats: "__lunora_admin__:getFunctionStats",
|
|
26
26
|
getIssues: "__lunora_admin__:getIssues",
|
|
27
|
+
getMetricSeries: "__lunora_admin__:getMetricSeries",
|
|
27
28
|
listSubscriptions: "__lunora_admin__:listSubscriptions",
|
|
28
29
|
listTableIndexes: "__lunora_admin__:listTableIndexes",
|
|
29
30
|
getLogs: "__lunora_admin__:getLogs",
|
|
@@ -33,6 +34,7 @@ const ADMIN_FUNCTIONS = {
|
|
|
33
34
|
getRequestLog: "__lunora_admin__:getRequestLog",
|
|
34
35
|
getSecurityAudit: "__lunora_admin__:getSecurityAudit",
|
|
35
36
|
getSettings: "__lunora_admin__:getSettings",
|
|
37
|
+
getTraces: "__lunora_admin__:getTraces",
|
|
36
38
|
// eslint-disable-next-line no-secrets/no-secrets -- reserved admin RPC path constant, not a credential
|
|
37
39
|
getWorkflowInstanceStatus: "__lunora_admin__:getWorkflowInstanceStatus",
|
|
38
40
|
importShard: "__lunora_admin__:importShard",
|
|
@@ -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-CqVmUVNs.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,7 +24,6 @@ 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;
|
|
@@ -182,6 +183,71 @@ const buildBatchEntryRequest = (batchRequest, entry) => {
|
|
|
182
183
|
});
|
|
183
184
|
};
|
|
184
185
|
|
|
186
|
+
const DEFAULT_CAPACITY$1 = 256;
|
|
187
|
+
const seriesKey = (event) => `${event.kind}\0${event.name}\0${stableStringify(event.attributes ?? {})}`;
|
|
188
|
+
class MetricBuffer {
|
|
189
|
+
capacity;
|
|
190
|
+
series = /* @__PURE__ */ new Map();
|
|
191
|
+
constructor(capacity = DEFAULT_CAPACITY$1) {
|
|
192
|
+
this.capacity = capacity > 0 ? Math.trunc(capacity) : DEFAULT_CAPACITY$1;
|
|
193
|
+
}
|
|
194
|
+
/** Number of distinct series currently aggregated. */
|
|
195
|
+
get size() {
|
|
196
|
+
return this.series.size;
|
|
197
|
+
}
|
|
198
|
+
/** Drop every aggregated series. */
|
|
199
|
+
clear() {
|
|
200
|
+
this.series.clear();
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Snapshot of the aggregated series, most-recently-updated first, each a fresh
|
|
204
|
+
* copy so a caller can't mutate the live aggregate. `series` is kept in
|
|
205
|
+
* update order (tail = newest), so one reverse yields newest-first.
|
|
206
|
+
*/
|
|
207
|
+
entries() {
|
|
208
|
+
return [...this.series.values()].toReversed().map((s) => {
|
|
209
|
+
return { ...s };
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** Fold one measurement into its series, creating or updating the aggregate. */
|
|
213
|
+
push(event) {
|
|
214
|
+
const key = seriesKey(event);
|
|
215
|
+
const existing = this.series.get(key);
|
|
216
|
+
if (existing === void 0) {
|
|
217
|
+
if (this.series.size >= this.capacity) {
|
|
218
|
+
const oldest = this.series.keys().next().value;
|
|
219
|
+
if (oldest !== void 0) {
|
|
220
|
+
this.series.delete(oldest);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
this.series.set(key, {
|
|
224
|
+
...event.attributes === void 0 ? {} : { attributes: event.attributes },
|
|
225
|
+
count: 1,
|
|
226
|
+
firstTs: event.ts,
|
|
227
|
+
functionPath: event.functionPath,
|
|
228
|
+
kind: event.kind,
|
|
229
|
+
last: event.value,
|
|
230
|
+
lastTs: event.ts,
|
|
231
|
+
max: event.value,
|
|
232
|
+
min: event.value,
|
|
233
|
+
name: event.name,
|
|
234
|
+
...event.shardKey === void 0 ? {} : { shardKey: event.shardKey },
|
|
235
|
+
sum: event.value
|
|
236
|
+
});
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.series.delete(key);
|
|
240
|
+
existing.count += 1;
|
|
241
|
+
existing.sum += event.value;
|
|
242
|
+
existing.min = Math.min(existing.min, event.value);
|
|
243
|
+
existing.max = Math.max(existing.max, event.value);
|
|
244
|
+
existing.last = event.value;
|
|
245
|
+
existing.lastTs = event.ts;
|
|
246
|
+
existing.functionPath = event.functionPath;
|
|
247
|
+
this.series.set(key, existing);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
185
251
|
const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
|
|
186
252
|
const QUERY_METRICS_MAX_SQL_LEN = 512;
|
|
187
253
|
const QUERY_METRICS_MAX_STATEMENTS = 500;
|
|
@@ -1271,18 +1337,40 @@ const renderLogMessage = (args) => args.map((value) => {
|
|
|
1271
1337
|
return String(value);
|
|
1272
1338
|
}
|
|
1273
1339
|
}).join(" ");
|
|
1340
|
+
const isLogFields = (value) => {
|
|
1341
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1342
|
+
return false;
|
|
1343
|
+
}
|
|
1344
|
+
const proto = Object.getPrototypeOf(value);
|
|
1345
|
+
return proto === Object.prototype || proto === null;
|
|
1346
|
+
};
|
|
1347
|
+
const parseLogArgs = (args, boundFields) => {
|
|
1348
|
+
if (args.length === 2 && typeof args[0] === "string" && isLogFields(args[1])) {
|
|
1349
|
+
return { fields: normalizeLogFields(args[1], boundFields), message: args[0] };
|
|
1350
|
+
}
|
|
1351
|
+
return { fields: normalizeLogFields(void 0, boundFields), message: renderLogMessage(args) };
|
|
1352
|
+
};
|
|
1274
1353
|
const emitLogEvent = (input) => {
|
|
1275
|
-
const
|
|
1354
|
+
const payload = {
|
|
1355
|
+
fields: input.fields,
|
|
1276
1356
|
function: input.functionPath,
|
|
1277
1357
|
level: input.level,
|
|
1278
1358
|
message: input.message,
|
|
1279
1359
|
shard: input.shardKey,
|
|
1280
1360
|
source: REQUEST_LOG_EVENT_SOURCE,
|
|
1361
|
+
spanId: input.spanId,
|
|
1362
|
+
traceId: input.traceId,
|
|
1281
1363
|
ts: input.ts,
|
|
1282
1364
|
type: LOG_EVENT_TYPE,
|
|
1283
1365
|
userId: input.userId
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1366
|
+
};
|
|
1367
|
+
let line;
|
|
1368
|
+
try {
|
|
1369
|
+
line = JSON.stringify(payload);
|
|
1370
|
+
} catch {
|
|
1371
|
+
line = JSON.stringify({ ...payload, fields: void 0 });
|
|
1372
|
+
}
|
|
1373
|
+
if (input.level === "error" || input.level === "fatal") {
|
|
1286
1374
|
console.error(line);
|
|
1287
1375
|
} else if (input.level === "warn") {
|
|
1288
1376
|
console.warn(line);
|
|
@@ -1428,6 +1516,136 @@ const runSocketPool = async (items, processOne, concurrency = 8) => {
|
|
|
1428
1516
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
1429
1517
|
};
|
|
1430
1518
|
|
|
1519
|
+
const DEFAULT_CAPACITY = 500;
|
|
1520
|
+
class SpanBuffer {
|
|
1521
|
+
buffer = [];
|
|
1522
|
+
capacity;
|
|
1523
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
1524
|
+
this.capacity = capacity > 0 ? Math.trunc(capacity) : DEFAULT_CAPACITY;
|
|
1525
|
+
}
|
|
1526
|
+
/** Number of spans currently buffered. */
|
|
1527
|
+
get size() {
|
|
1528
|
+
return this.buffer.length;
|
|
1529
|
+
}
|
|
1530
|
+
/** Drop every buffered span. */
|
|
1531
|
+
clear() {
|
|
1532
|
+
this.buffer.length = 0;
|
|
1533
|
+
}
|
|
1534
|
+
/** Snapshot of the buffered spans in insertion order. Fresh array per call. */
|
|
1535
|
+
entries() {
|
|
1536
|
+
return [...this.buffer];
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Whether any buffered span belongs to `traceId`. A membership test rather
|
|
1540
|
+
* than `entries().some(...)` so the per-dispatch check that decides whether
|
|
1541
|
+
* to record a root span doesn't copy the whole ring on every request.
|
|
1542
|
+
*/
|
|
1543
|
+
hasTrace(traceId) {
|
|
1544
|
+
return this.buffer.some((span) => span.traceId === traceId);
|
|
1545
|
+
}
|
|
1546
|
+
/** Append a span, evicting the oldest when at capacity. */
|
|
1547
|
+
push(span) {
|
|
1548
|
+
this.buffer.push(span);
|
|
1549
|
+
if (this.buffer.length > this.capacity) {
|
|
1550
|
+
this.buffer.shift();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
const DEFAULT_TRACE_LIMIT = 50;
|
|
1555
|
+
const groupByTrace = (spans) => {
|
|
1556
|
+
const byTrace = /* @__PURE__ */ new Map();
|
|
1557
|
+
for (const span of spans) {
|
|
1558
|
+
const group = byTrace.get(span.traceId);
|
|
1559
|
+
if (group === void 0) {
|
|
1560
|
+
byTrace.set(span.traceId, [span]);
|
|
1561
|
+
} else {
|
|
1562
|
+
group.push(span);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
return byTrace;
|
|
1566
|
+
};
|
|
1567
|
+
const selectAnchor = (group, byId) => {
|
|
1568
|
+
const declaredRoot = group.find((span) => span.dispatch === true);
|
|
1569
|
+
if (declaredRoot !== void 0) {
|
|
1570
|
+
return declaredRoot;
|
|
1571
|
+
}
|
|
1572
|
+
const byStart = group.toSorted((a, b) => a.startTs - b.startTs);
|
|
1573
|
+
return byStart.find((span) => !byId.has(span.parentSpanId)) ?? byStart[0];
|
|
1574
|
+
};
|
|
1575
|
+
const depthResolver = (anchor, byId) => {
|
|
1576
|
+
const cache = /* @__PURE__ */ new Map([[anchor.spanId, 0]]);
|
|
1577
|
+
return (span) => {
|
|
1578
|
+
const chain = [];
|
|
1579
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1580
|
+
let current = span;
|
|
1581
|
+
let base = 0;
|
|
1582
|
+
for (; ; ) {
|
|
1583
|
+
const known = cache.get(current.spanId);
|
|
1584
|
+
if (known !== void 0) {
|
|
1585
|
+
base = known;
|
|
1586
|
+
break;
|
|
1587
|
+
}
|
|
1588
|
+
if (seen.has(current.spanId)) {
|
|
1589
|
+
break;
|
|
1590
|
+
}
|
|
1591
|
+
seen.add(current.spanId);
|
|
1592
|
+
chain.push(current);
|
|
1593
|
+
const parent = byId.get(current.parentSpanId);
|
|
1594
|
+
if (parent === void 0) {
|
|
1595
|
+
break;
|
|
1596
|
+
}
|
|
1597
|
+
current = parent;
|
|
1598
|
+
}
|
|
1599
|
+
for (const [index, entry] of chain.toReversed().entries()) {
|
|
1600
|
+
cache.set(entry.spanId, base + index + 1);
|
|
1601
|
+
}
|
|
1602
|
+
return cache.get(span.spanId) ?? base;
|
|
1603
|
+
};
|
|
1604
|
+
};
|
|
1605
|
+
const foldTraces = (spans, limit = DEFAULT_TRACE_LIMIT) => {
|
|
1606
|
+
const byTrace = groupByTrace(spans);
|
|
1607
|
+
const selected = [...byTrace.entries()].map(([traceId, group]) => {
|
|
1608
|
+
return { group, startTs: Math.min(...group.map((span) => span.startTs)), traceId };
|
|
1609
|
+
}).toSorted((a, b) => b.startTs - a.startTs).slice(0, limit);
|
|
1610
|
+
const summaries = [];
|
|
1611
|
+
for (const { group, traceId } of selected) {
|
|
1612
|
+
const byId = new Map(group.map((span) => [span.spanId, span]));
|
|
1613
|
+
const anchor = selectAnchor(group, byId);
|
|
1614
|
+
if (anchor === void 0) {
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const depthOf = depthResolver(anchor, byId);
|
|
1618
|
+
const { startTs } = anchor;
|
|
1619
|
+
const endTs = Math.max(...group.map((span) => span.startTs + span.durationMs));
|
|
1620
|
+
const rows = group.map((span) => {
|
|
1621
|
+
return {
|
|
1622
|
+
...span.attributes === void 0 ? {} : { attributes: span.attributes },
|
|
1623
|
+
depth: depthOf(span),
|
|
1624
|
+
durationMs: span.durationMs,
|
|
1625
|
+
...span.error === void 0 ? {} : { error: span.error },
|
|
1626
|
+
name: span.name,
|
|
1627
|
+
// Clamped at 0: a child whose parent was evicted can predate the
|
|
1628
|
+
// anchor, and a negative offset would render as a bar off-canvas.
|
|
1629
|
+
offsetMs: Math.max(0, span.startTs - startTs),
|
|
1630
|
+
ok: span.ok,
|
|
1631
|
+
parentSpanId: span.parentSpanId,
|
|
1632
|
+
spanId: span.spanId
|
|
1633
|
+
};
|
|
1634
|
+
}).toSorted((a, b) => a.offsetMs - b.offsetMs || a.depth - b.depth);
|
|
1635
|
+
summaries.push({
|
|
1636
|
+
durationMs: endTs - startTs,
|
|
1637
|
+
functionPath: anchor.functionPath,
|
|
1638
|
+
ok: group.every((span) => span.ok),
|
|
1639
|
+
rootName: anchor.name,
|
|
1640
|
+
...anchor.shardKey === void 0 ? {} : { shardKey: anchor.shardKey },
|
|
1641
|
+
spans: rows,
|
|
1642
|
+
startTs,
|
|
1643
|
+
traceId
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
return { total: byTrace.size, traces: summaries.toSorted((a, b) => b.startTs - a.startTs) };
|
|
1647
|
+
};
|
|
1648
|
+
|
|
1431
1649
|
const DANGLING_SCAN_CAP = 5e3;
|
|
1432
1650
|
const DANGLING_RESULT_CAP = 500;
|
|
1433
1651
|
const DOC_COLUMN = "__doc__";
|
|
@@ -2164,6 +2382,14 @@ class ShardDO {
|
|
|
2164
2382
|
currentRequestIp;
|
|
2165
2383
|
/** W3C `traceparent` of the inbound RPC; forwarded onto outbound container fetches. */
|
|
2166
2384
|
currentRequestTraceparent;
|
|
2385
|
+
/**
|
|
2386
|
+
* Trace ids for the in-flight dispatch, resolved once at entry (see
|
|
2387
|
+
* {@link resolveTraceAnchor}). Shared by `ctx.trace` and by the synthetic
|
|
2388
|
+
* root span recorded on the way out so both agree even with no inbound
|
|
2389
|
+
* `traceparent`. Cleared in the same `finally` as the other per-request
|
|
2390
|
+
* fields.
|
|
2391
|
+
*/
|
|
2392
|
+
currentRequestTrace;
|
|
2167
2393
|
/**
|
|
2168
2394
|
* Client-issued idempotency key for the in-flight mutation, forwarded via the
|
|
2169
2395
|
* `x-lunora-mutation-id` header. When set, the dispatch path dedups the call
|
|
@@ -2361,6 +2587,19 @@ class ShardDO {
|
|
|
2361
2587
|
* "recent RPC errors on this instance" feed, not a general application log.
|
|
2362
2588
|
*/
|
|
2363
2589
|
logs = new LogBuffer();
|
|
2590
|
+
/**
|
|
2591
|
+
* Recent `ctx.trace` spans (plus the synthetic per-dispatch root), powering
|
|
2592
|
+
* the studio's Traces panel. In-memory and hibernation-volatile like
|
|
2593
|
+
* `logs` — production tracing ships to a collector via `otlpSink`.
|
|
2594
|
+
*/
|
|
2595
|
+
spans = new SpanBuffer();
|
|
2596
|
+
/**
|
|
2597
|
+
* Running aggregates of `ctx.metrics.*` measurements, powering the studio's
|
|
2598
|
+
* Metrics panel. In-memory and hibernation-volatile like `logs` and
|
|
2599
|
+
* `spans`, but folds samples into per-series totals rather than ringing
|
|
2600
|
+
* raw events — production aggregation ships to a collector via the sink.
|
|
2601
|
+
*/
|
|
2602
|
+
metricSeries = new MetricBuffer();
|
|
2364
2603
|
/**
|
|
2365
2604
|
* In-flight dependency tracker for the currently-executing query. Set by
|
|
2366
2605
|
* `runCachedQuery` so the ctx-db hooks (wired via `onRead`) can
|
|
@@ -2452,6 +2691,7 @@ class ShardDO {
|
|
|
2452
2691
|
* Worker-side fetch entry point. Handles WebSocket upgrades and the
|
|
2453
2692
|
* shard-local RPC endpoint forwarded by `@lunora/runtime`.
|
|
2454
2693
|
*/
|
|
2694
|
+
// 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.
|
|
2455
2695
|
async fetch(request) {
|
|
2456
2696
|
const url = new URL(request.url);
|
|
2457
2697
|
this.shardBinding = request.headers.get("x-lunora-shard-binding") ?? this.shardBinding;
|
|
@@ -2483,6 +2723,8 @@ class ShardDO {
|
|
|
2483
2723
|
this.currentRequestIp = request.headers.get("x-lunora-client-ip") ?? void 0;
|
|
2484
2724
|
this.currentRequestSystem = request.headers.get("x-lunora-system") === "1";
|
|
2485
2725
|
this.currentRequestTraceparent = request.headers.get("traceparent") ?? void 0;
|
|
2726
|
+
this.currentRequestTrace = resolveTraceAnchor(this.currentRequestTraceparent);
|
|
2727
|
+
const dispatchTrace = this.currentRequestTrace;
|
|
2486
2728
|
this.currentRequestReadTables = void 0;
|
|
2487
2729
|
this.currentRequestCacheHit = void 0;
|
|
2488
2730
|
this.metrics.requests += 1;
|
|
@@ -2490,6 +2732,7 @@ class ShardDO {
|
|
|
2490
2732
|
this.currentScannedTables = /* @__PURE__ */ new Set();
|
|
2491
2733
|
this.currentIndexHits = /* @__PURE__ */ new Set();
|
|
2492
2734
|
this.currentStmtSamples = [];
|
|
2735
|
+
let dispatchError;
|
|
2493
2736
|
try {
|
|
2494
2737
|
if (payload.functionPath.startsWith(RELATION_FUNCTION_PREFIX)) {
|
|
2495
2738
|
const value = await this.runRelationFanoutRead(payload.functionPath, payload.args ?? {});
|
|
@@ -2521,6 +2764,7 @@ class ShardDO {
|
|
|
2521
2764
|
return response;
|
|
2522
2765
|
} catch (error) {
|
|
2523
2766
|
this.metrics.errors += 1;
|
|
2767
|
+
dispatchError = { thrown: error };
|
|
2524
2768
|
const durationMs = Date.now() - dispatchStartedAt;
|
|
2525
2769
|
const message = error instanceof Error ? error.message : String(error);
|
|
2526
2770
|
const conflicted = error instanceof ConflictError && error.kind === "occ";
|
|
@@ -2540,6 +2784,10 @@ class ShardDO {
|
|
|
2540
2784
|
await this.flushChangedTables();
|
|
2541
2785
|
return this.errorToResponse(error);
|
|
2542
2786
|
} finally {
|
|
2787
|
+
if (this.spans.hasTrace(dispatchTrace.traceId)) {
|
|
2788
|
+
this.recordDispatchRootSpan(payload.functionPath, dispatchStartedAt, dispatchError, dispatchTrace);
|
|
2789
|
+
}
|
|
2790
|
+
this.currentRequestTrace = void 0;
|
|
2543
2791
|
this.currentRequestBookmark = void 0;
|
|
2544
2792
|
this.currentResponseBookmark = void 0;
|
|
2545
2793
|
this.currentRequestUserId = void 0;
|
|
@@ -3009,6 +3257,14 @@ class ShardDO {
|
|
|
3009
3257
|
getCurrentTraceparent() {
|
|
3010
3258
|
return this.currentRequestTraceparent;
|
|
3011
3259
|
}
|
|
3260
|
+
/**
|
|
3261
|
+
* The in-flight dispatch's trace ids, for the generated `buildCtx` to hand to
|
|
3262
|
+
* {@link makeTracer}. `undefined` outside a dispatch (an alarm, a lifecycle
|
|
3263
|
+
* hook), where the tracer mints its own anchor.
|
|
3264
|
+
*/
|
|
3265
|
+
getCurrentTrace() {
|
|
3266
|
+
return this.currentRequestTrace;
|
|
3267
|
+
}
|
|
3012
3268
|
/**
|
|
3013
3269
|
* Identity claims (email, name, roles, …) forwarded by the runtime's
|
|
3014
3270
|
* `resolveIdentity` hook. Returns `undefined` for anonymous requests
|
|
@@ -4082,24 +4338,173 @@ class ShardDO {
|
|
|
4082
4338
|
* Unlike request-log args, `ctx.log` args are NOT redacted: the developer
|
|
4083
4339
|
* chose to log them, exactly like a raw `console.log`.
|
|
4084
4340
|
*/
|
|
4085
|
-
recordUserLog(functionPath, level, args, sink) {
|
|
4341
|
+
recordUserLog(functionPath, level, args, message, fields, sink) {
|
|
4342
|
+
const trace = this.currentRequestTrace;
|
|
4086
4343
|
const event = {
|
|
4087
4344
|
args,
|
|
4345
|
+
fields,
|
|
4088
4346
|
functionPath,
|
|
4089
4347
|
level,
|
|
4090
|
-
message
|
|
4348
|
+
message,
|
|
4091
4349
|
shardKey: this.state.id?.name,
|
|
4350
|
+
spanId: trace?.rootSpanId,
|
|
4351
|
+
traceId: trace?.traceId,
|
|
4092
4352
|
ts: Date.now(),
|
|
4093
4353
|
userId: this.getCurrentUserId()
|
|
4094
4354
|
};
|
|
4095
|
-
this.logs.push({ functionPath, level
|
|
4355
|
+
this.logs.push({ fields, functionPath, level, message, timestamp: event.ts });
|
|
4096
4356
|
try {
|
|
4097
4357
|
emitLogEvent(event);
|
|
4098
4358
|
} catch {
|
|
4099
4359
|
}
|
|
4100
4360
|
if (sink?.onLog) {
|
|
4101
4361
|
try {
|
|
4102
|
-
sink.onLog(event);
|
|
4362
|
+
sink.onLog(event, { waitUntil: this.state.waitUntil?.bind(this.state) });
|
|
4363
|
+
} catch {
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
/**
|
|
4368
|
+
* Build the `ctx.log` logger for one dispatched function. Each severity method
|
|
4369
|
+
* accepts either the structured form (`(message, fields)`) or console-style
|
|
4370
|
+
* varargs (see {@link parseLogArgs}); `with(fields)` returns a child that
|
|
4371
|
+
* stamps `fields` onto every line. The generated `buildCtx` calls this once
|
|
4372
|
+
* per dispatch and assigns the result to `ctx.log`.
|
|
4373
|
+
*/
|
|
4374
|
+
makeLogger(functionPath, sink, boundFields) {
|
|
4375
|
+
const emit = (level, args) => {
|
|
4376
|
+
const { fields, message } = parseLogArgs(args, boundFields);
|
|
4377
|
+
this.recordUserLog(functionPath, level, args, message, fields, sink);
|
|
4378
|
+
};
|
|
4379
|
+
return {
|
|
4380
|
+
debug: (...args) => {
|
|
4381
|
+
emit("debug", args);
|
|
4382
|
+
},
|
|
4383
|
+
error: (...args) => {
|
|
4384
|
+
emit("error", args);
|
|
4385
|
+
},
|
|
4386
|
+
fatal: (...args) => {
|
|
4387
|
+
emit("fatal", args);
|
|
4388
|
+
},
|
|
4389
|
+
info: (...args) => {
|
|
4390
|
+
emit("info", args);
|
|
4391
|
+
},
|
|
4392
|
+
log: (...args) => {
|
|
4393
|
+
emit("log", args);
|
|
4394
|
+
},
|
|
4395
|
+
trace: (...args) => {
|
|
4396
|
+
emit("trace", args);
|
|
4397
|
+
},
|
|
4398
|
+
warn: (...args) => {
|
|
4399
|
+
emit("warn", args);
|
|
4400
|
+
},
|
|
4401
|
+
with: (fields) => this.makeLogger(functionPath, sink, boundFields ? { ...boundFields, ...fields } : fields)
|
|
4402
|
+
};
|
|
4403
|
+
}
|
|
4404
|
+
/**
|
|
4405
|
+
* Build the `ctx.trace` span factory for one dispatched function. The
|
|
4406
|
+
* generated `buildCtx` calls this once per dispatch and assigns the result to
|
|
4407
|
+
* `ctx.trace`.
|
|
4408
|
+
*
|
|
4409
|
+
* Thin wiring over {@link createTracer}, which owns the span semantics (see
|
|
4410
|
+
* there for why nesting is explicit rather than ambient). Everything it needs
|
|
4411
|
+
* from the shard is passed explicitly.
|
|
4412
|
+
*
|
|
4413
|
+
* `anchor` is the trace this ctx's spans belong to; omit it for a ctx with no
|
|
4414
|
+
* owning dispatch (an alarm, a subscription re-run) to mint a fresh anchor, so
|
|
4415
|
+
* `ctx.trace` still yields a coherent self-contained trace there.
|
|
4416
|
+
*/
|
|
4417
|
+
makeTracer(functionPath, sink, anchor) {
|
|
4418
|
+
return createTracer({
|
|
4419
|
+
anchor: anchor ?? resolveTraceAnchor(void 0),
|
|
4420
|
+
functionPath,
|
|
4421
|
+
record: (span) => {
|
|
4422
|
+
this.recordSpan(span, sink);
|
|
4423
|
+
},
|
|
4424
|
+
shardKey: this.state.id?.name,
|
|
4425
|
+
userId: () => this.getCurrentUserId()
|
|
4426
|
+
});
|
|
4427
|
+
}
|
|
4428
|
+
/**
|
|
4429
|
+
* Build the `ctx.metrics` recorder for one dispatched function. Thin wiring
|
|
4430
|
+
* over {@link createMetrics}, which owns the instrument semantics.
|
|
4431
|
+
*/
|
|
4432
|
+
makeMetrics(functionPath, sink) {
|
|
4433
|
+
return createMetrics({
|
|
4434
|
+
functionPath,
|
|
4435
|
+
record: (event) => {
|
|
4436
|
+
this.recordMetric(event, sink);
|
|
4437
|
+
},
|
|
4438
|
+
shardKey: this.state.id?.name
|
|
4439
|
+
});
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Fold one measurement into the in-memory {@link metricSeries} readout and
|
|
4443
|
+
* hand it to the optional `sink.onMetric`. Best-effort like
|
|
4444
|
+
* {@link recordUserLog} and {@link recordSpan}: recording a measurement must
|
|
4445
|
+
* never break the handler that recorded it.
|
|
4446
|
+
*
|
|
4447
|
+
* The buffer folds rather than rings: a measurement's value is in its
|
|
4448
|
+
* aggregate over a window, which a bounded ring of raw samples can't represent
|
|
4449
|
+
* (it evicts the oldest samples, the ones a running total needs). So the
|
|
4450
|
+
* buffer keeps one running aggregate per series and resets on hibernation like
|
|
4451
|
+
* logs and spans — a "recent metrics on this instance" dev readout. Durable,
|
|
4452
|
+
* cross-instance aggregation is still the sink's job.
|
|
4453
|
+
*/
|
|
4454
|
+
recordMetric(event, sink) {
|
|
4455
|
+
try {
|
|
4456
|
+
this.metricSeries.push(event);
|
|
4457
|
+
} catch {
|
|
4458
|
+
}
|
|
4459
|
+
if (sink?.onMetric) {
|
|
4460
|
+
try {
|
|
4461
|
+
sink.onMetric(event, { waitUntil: this.state.waitUntil?.bind(this.state) });
|
|
4462
|
+
} catch {
|
|
4463
|
+
}
|
|
4464
|
+
}
|
|
4465
|
+
}
|
|
4466
|
+
/**
|
|
4467
|
+
* Buffer the synthetic root span for a finished dispatch. The caller gates
|
|
4468
|
+
* this on the dispatch having actually produced spans (the `hasTrace` check at
|
|
4469
|
+
* the call site): every request minting a root would fill the bounded ring
|
|
4470
|
+
* with single-bar traces from uninstrumented handlers and evict the
|
|
4471
|
+
* instrumented ones the panel exists to show.
|
|
4472
|
+
*
|
|
4473
|
+
* `anchor` carries the dispatch's trace ids, captured at entry rather than
|
|
4474
|
+
* read from `this` here — this runs after the handler's awaits, where the
|
|
4475
|
+
* shared field may already belong to an interleaved dispatch.
|
|
4476
|
+
*/
|
|
4477
|
+
recordDispatchRootSpan(functionPath, startedAt, failure, anchor) {
|
|
4478
|
+
try {
|
|
4479
|
+
this.spans.push(
|
|
4480
|
+
dispatchRootSpan({
|
|
4481
|
+
anchor,
|
|
4482
|
+
durationMs: Date.now() - startedAt,
|
|
4483
|
+
failure,
|
|
4484
|
+
functionPath,
|
|
4485
|
+
shardKey: this.state.id?.name,
|
|
4486
|
+
startTs: startedAt,
|
|
4487
|
+
userId: this.getCurrentUserId()
|
|
4488
|
+
})
|
|
4489
|
+
);
|
|
4490
|
+
} catch {
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
/**
|
|
4494
|
+
* Buffer one span for the studio Traces panel and hand it to the optional
|
|
4495
|
+
* `sink.onSpan`. Best-effort throughout, exactly like {@link recordUserLog}:
|
|
4496
|
+
* a span is recorded *after* its body already settled, so letting a telemetry
|
|
4497
|
+
* failure escape here would turn a succeeded operation into a failed request.
|
|
4498
|
+
*/
|
|
4499
|
+
// 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
|
|
4500
|
+
recordSpan(span, sink) {
|
|
4501
|
+
try {
|
|
4502
|
+
this.spans.push(span);
|
|
4503
|
+
} catch {
|
|
4504
|
+
}
|
|
4505
|
+
if (sink?.onSpan) {
|
|
4506
|
+
try {
|
|
4507
|
+
sink.onSpan(span, { waitUntil: this.state.waitUntil?.bind(this.state) });
|
|
4103
4508
|
} catch {
|
|
4104
4509
|
}
|
|
4105
4510
|
}
|
|
@@ -5179,6 +5584,7 @@ class ShardDO {
|
|
|
5179
5584
|
* wraps each in the {@link ADMIN_WILDCARD} sentinel, keeping that one fact in
|
|
5180
5585
|
* a single place and `readAdminOp` under its complexity budget.
|
|
5181
5586
|
*/
|
|
5587
|
+
// 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.
|
|
5182
5588
|
readAdminWildcardOp(functionPath) {
|
|
5183
5589
|
if (functionPath === ADMIN_FUNCTIONS.listTables) {
|
|
5184
5590
|
return listTables(this.state.storage.sql);
|
|
@@ -5198,6 +5604,13 @@ class ShardDO {
|
|
|
5198
5604
|
if (functionPath === ADMIN_FUNCTIONS.getLogs) {
|
|
5199
5605
|
return { entries: this.logs.entries() };
|
|
5200
5606
|
}
|
|
5607
|
+
if (functionPath === ADMIN_FUNCTIONS.getTraces) {
|
|
5608
|
+
const folded = foldTraces(this.spans.entries());
|
|
5609
|
+
return { total: folded.total, traces: folded.traces };
|
|
5610
|
+
}
|
|
5611
|
+
if (functionPath === ADMIN_FUNCTIONS.getMetricSeries) {
|
|
5612
|
+
return { series: this.metricSeries.entries() };
|
|
5613
|
+
}
|
|
5201
5614
|
if (functionPath === ADMIN_FUNCTIONS.getSettings) {
|
|
5202
5615
|
return buildSettings(this.env);
|
|
5203
5616
|
}
|