@lunora/do 1.0.0-alpha.34 → 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 +380 -69
- package/dist/index.d.ts +380 -69
- 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-B5c6kQIN.mjs → ROOT_DO_SIZE_WARN_BYTES-QSOI2hpe.mjs} +376 -65
- 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 +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-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,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;
|
|
@@ -206,6 +183,71 @@ const buildBatchEntryRequest = (batchRequest, entry) => {
|
|
|
206
183
|
});
|
|
207
184
|
};
|
|
208
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
|
+
|
|
209
251
|
const QUERY_METRICS_TABLE = "__lunora_metrics_queries";
|
|
210
252
|
const QUERY_METRICS_MAX_SQL_LEN = 512;
|
|
211
253
|
const QUERY_METRICS_MAX_STATEMENTS = 500;
|
|
@@ -1182,35 +1224,6 @@ const createRelayLink = (host) => {
|
|
|
1182
1224
|
return parsed === void 0 ? new OwnerRelay(host, name) : new RelayMember(host, parsed.ownerKey, parsed.relayIndex);
|
|
1183
1225
|
};
|
|
1184
1226
|
|
|
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
1227
|
const REQUEST_LOG_TABLE = "__lunora_reqlog__";
|
|
1215
1228
|
const REQUEST_LOG_RETENTION = 1e3;
|
|
1216
1229
|
const REQUEST_LOG_EVENT_SOURCE = "lunora";
|
|
@@ -1503,6 +1516,136 @@ const runSocketPool = async (items, processOne, concurrency = 8) => {
|
|
|
1503
1516
|
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
1504
1517
|
};
|
|
1505
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
|
+
|
|
1506
1649
|
const DANGLING_SCAN_CAP = 5e3;
|
|
1507
1650
|
const DANGLING_RESULT_CAP = 500;
|
|
1508
1651
|
const DOC_COLUMN = "__doc__";
|
|
@@ -2239,6 +2382,14 @@ class ShardDO {
|
|
|
2239
2382
|
currentRequestIp;
|
|
2240
2383
|
/** W3C `traceparent` of the inbound RPC; forwarded onto outbound container fetches. */
|
|
2241
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;
|
|
2242
2393
|
/**
|
|
2243
2394
|
* Client-issued idempotency key for the in-flight mutation, forwarded via the
|
|
2244
2395
|
* `x-lunora-mutation-id` header. When set, the dispatch path dedups the call
|
|
@@ -2436,6 +2587,19 @@ class ShardDO {
|
|
|
2436
2587
|
* "recent RPC errors on this instance" feed, not a general application log.
|
|
2437
2588
|
*/
|
|
2438
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();
|
|
2439
2603
|
/**
|
|
2440
2604
|
* In-flight dependency tracker for the currently-executing query. Set by
|
|
2441
2605
|
* `runCachedQuery` so the ctx-db hooks (wired via `onRead`) can
|
|
@@ -2527,6 +2691,7 @@ class ShardDO {
|
|
|
2527
2691
|
* Worker-side fetch entry point. Handles WebSocket upgrades and the
|
|
2528
2692
|
* shard-local RPC endpoint forwarded by `@lunora/runtime`.
|
|
2529
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.
|
|
2530
2695
|
async fetch(request) {
|
|
2531
2696
|
const url = new URL(request.url);
|
|
2532
2697
|
this.shardBinding = request.headers.get("x-lunora-shard-binding") ?? this.shardBinding;
|
|
@@ -2558,6 +2723,8 @@ class ShardDO {
|
|
|
2558
2723
|
this.currentRequestIp = request.headers.get("x-lunora-client-ip") ?? void 0;
|
|
2559
2724
|
this.currentRequestSystem = request.headers.get("x-lunora-system") === "1";
|
|
2560
2725
|
this.currentRequestTraceparent = request.headers.get("traceparent") ?? void 0;
|
|
2726
|
+
this.currentRequestTrace = resolveTraceAnchor(this.currentRequestTraceparent);
|
|
2727
|
+
const dispatchTrace = this.currentRequestTrace;
|
|
2561
2728
|
this.currentRequestReadTables = void 0;
|
|
2562
2729
|
this.currentRequestCacheHit = void 0;
|
|
2563
2730
|
this.metrics.requests += 1;
|
|
@@ -2565,6 +2732,7 @@ class ShardDO {
|
|
|
2565
2732
|
this.currentScannedTables = /* @__PURE__ */ new Set();
|
|
2566
2733
|
this.currentIndexHits = /* @__PURE__ */ new Set();
|
|
2567
2734
|
this.currentStmtSamples = [];
|
|
2735
|
+
let dispatchError;
|
|
2568
2736
|
try {
|
|
2569
2737
|
if (payload.functionPath.startsWith(RELATION_FUNCTION_PREFIX)) {
|
|
2570
2738
|
const value = await this.runRelationFanoutRead(payload.functionPath, payload.args ?? {});
|
|
@@ -2596,6 +2764,7 @@ class ShardDO {
|
|
|
2596
2764
|
return response;
|
|
2597
2765
|
} catch (error) {
|
|
2598
2766
|
this.metrics.errors += 1;
|
|
2767
|
+
dispatchError = { thrown: error };
|
|
2599
2768
|
const durationMs = Date.now() - dispatchStartedAt;
|
|
2600
2769
|
const message = error instanceof Error ? error.message : String(error);
|
|
2601
2770
|
const conflicted = error instanceof ConflictError && error.kind === "occ";
|
|
@@ -2615,6 +2784,10 @@ class ShardDO {
|
|
|
2615
2784
|
await this.flushChangedTables();
|
|
2616
2785
|
return this.errorToResponse(error);
|
|
2617
2786
|
} finally {
|
|
2787
|
+
if (this.spans.hasTrace(dispatchTrace.traceId)) {
|
|
2788
|
+
this.recordDispatchRootSpan(payload.functionPath, dispatchStartedAt, dispatchError, dispatchTrace);
|
|
2789
|
+
}
|
|
2790
|
+
this.currentRequestTrace = void 0;
|
|
2618
2791
|
this.currentRequestBookmark = void 0;
|
|
2619
2792
|
this.currentResponseBookmark = void 0;
|
|
2620
2793
|
this.currentRequestUserId = void 0;
|
|
@@ -3084,6 +3257,14 @@ class ShardDO {
|
|
|
3084
3257
|
getCurrentTraceparent() {
|
|
3085
3258
|
return this.currentRequestTraceparent;
|
|
3086
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
|
+
}
|
|
3087
3268
|
/**
|
|
3088
3269
|
* Identity claims (email, name, roles, …) forwarded by the runtime's
|
|
3089
3270
|
* `resolveIdentity` hook. Returns `undefined` for anonymous requests
|
|
@@ -4158,7 +4339,7 @@ class ShardDO {
|
|
|
4158
4339
|
* chose to log them, exactly like a raw `console.log`.
|
|
4159
4340
|
*/
|
|
4160
4341
|
recordUserLog(functionPath, level, args, message, fields, sink) {
|
|
4161
|
-
const trace =
|
|
4342
|
+
const trace = this.currentRequestTrace;
|
|
4162
4343
|
const event = {
|
|
4163
4344
|
args,
|
|
4164
4345
|
fields,
|
|
@@ -4166,12 +4347,12 @@ class ShardDO {
|
|
|
4166
4347
|
level,
|
|
4167
4348
|
message,
|
|
4168
4349
|
shardKey: this.state.id?.name,
|
|
4169
|
-
spanId: trace?.
|
|
4350
|
+
spanId: trace?.rootSpanId,
|
|
4170
4351
|
traceId: trace?.traceId,
|
|
4171
4352
|
ts: Date.now(),
|
|
4172
4353
|
userId: this.getCurrentUserId()
|
|
4173
4354
|
};
|
|
4174
|
-
this.logs.push({ fields, functionPath, level
|
|
4355
|
+
this.logs.push({ fields, functionPath, level, message, timestamp: event.ts });
|
|
4175
4356
|
try {
|
|
4176
4357
|
emitLogEvent(event);
|
|
4177
4358
|
} catch {
|
|
@@ -4196,16 +4377,138 @@ class ShardDO {
|
|
|
4196
4377
|
this.recordUserLog(functionPath, level, args, message, fields, sink);
|
|
4197
4378
|
};
|
|
4198
4379
|
return {
|
|
4199
|
-
debug: (...args) =>
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
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
|
+
},
|
|
4206
4401
|
with: (fields) => this.makeLogger(functionPath, sink, boundFields ? { ...boundFields, ...fields } : fields)
|
|
4207
4402
|
};
|
|
4208
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) });
|
|
4508
|
+
} catch {
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4209
4512
|
/**
|
|
4210
4513
|
* Assemble the per-socket {@link LifecycleDispatchInfo} from its attachment:
|
|
4211
4514
|
* the verified identity to replay and the {@link LifecycleEvent} the hooks
|
|
@@ -5281,6 +5584,7 @@ class ShardDO {
|
|
|
5281
5584
|
* wraps each in the {@link ADMIN_WILDCARD} sentinel, keeping that one fact in
|
|
5282
5585
|
* a single place and `readAdminOp` under its complexity budget.
|
|
5283
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.
|
|
5284
5588
|
readAdminWildcardOp(functionPath) {
|
|
5285
5589
|
if (functionPath === ADMIN_FUNCTIONS.listTables) {
|
|
5286
5590
|
return listTables(this.state.storage.sql);
|
|
@@ -5300,6 +5604,13 @@ class ShardDO {
|
|
|
5300
5604
|
if (functionPath === ADMIN_FUNCTIONS.getLogs) {
|
|
5301
5605
|
return { entries: this.logs.entries() };
|
|
5302
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
|
+
}
|
|
5303
5614
|
if (functionPath === ADMIN_FUNCTIONS.getSettings) {
|
|
5304
5615
|
return buildSettings(this.env);
|
|
5305
5616
|
}
|