@dudousxd/nestjs-durable-telescope 0.9.2 → 0.10.0

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.cjs CHANGED
@@ -26,6 +26,9 @@ __export(index_exports, {
26
26
  durableDashboard: () => durableDashboard,
27
27
  durableDurationProvider: () => durableDurationProvider,
28
28
  durableExecutionScope: () => durableExecutionScope,
29
+ durableFailedTriggersProvider: () => durableFailedTriggersProvider,
30
+ durableLifecycleTriggersProvider: () => durableLifecycleTriggersProvider,
31
+ durableOwedLifecycleProvider: () => durableOwedLifecycleProvider,
29
32
  durableRecentFailuresProvider: () => durableRecentFailuresProvider,
30
33
  durableRunsOverTimeProvider: () => durableRunsOverTimeProvider,
31
34
  durableStateBreakdownProvider: () => durableStateBreakdownProvider,
@@ -37,6 +40,7 @@ __export(index_exports, {
37
40
  durableTraceContext: () => durableTraceContext,
38
41
  durableWorkerHealthProvider: () => durableWorkerHealthProvider,
39
42
  durableWorkerStatusProvider: () => durableWorkerStatusProvider,
43
+ runSpanContext: () => runSpanContext,
40
44
  runTraceContext: () => runTraceContext,
41
45
  runTraceId: () => runTraceId
42
46
  });
@@ -82,14 +86,18 @@ function runIds(runId) {
82
86
  }
83
87
  __name(runIds, "runIds");
84
88
  function runTraceContext(runId, parent = import_api.context.active()) {
85
- return import_api.trace.setSpanContext(parent, {
89
+ return import_api.trace.setSpanContext(parent, runSpanContext(runId));
90
+ }
91
+ __name(runTraceContext, "runTraceContext");
92
+ function runSpanContext(runId) {
93
+ return {
86
94
  traceId: runTraceId(runId),
87
95
  spanId: runSpanId(runId),
88
96
  traceFlags: import_api.TraceFlags.SAMPLED,
89
97
  isRemote: true
90
- });
98
+ };
91
99
  }
92
- __name(runTraceContext, "runTraceContext");
100
+ __name(runSpanContext, "runSpanContext");
93
101
 
94
102
  // src/durable-execution-scope.ts
95
103
  var activeScope = new import_node_async_hooks.AsyncLocalStorage();
@@ -176,10 +184,14 @@ var RUN_EVENT_TYPES = /* @__PURE__ */ new Set([
176
184
  "run.started",
177
185
  "run.completed",
178
186
  "run.failed",
179
- "run.suspended"
187
+ "run.suspended",
188
+ "run.triggered",
189
+ "run.trigger_failed"
180
190
  ]);
181
191
 
182
192
  // src/durable-telescope.watcher.ts
193
+ var REMOTE_EVENT = Symbol.for("@dudousxd/nestjs-durable:remote-event");
194
+ var isRemote = /* @__PURE__ */ __name((event) => event[REMOTE_EVENT] === true, "isRemote");
183
195
  var DurableTelescopeWatcher = class {
184
196
  static {
185
197
  __name(this, "DurableTelescopeWatcher");
@@ -196,7 +208,9 @@ var DurableTelescopeWatcher = class {
196
208
  if (typeof engine?.subscribe !== "function") return;
197
209
  const tracer = import_api3.trace.getTracer("@dudousxd/nestjs-durable-telescope");
198
210
  engine.subscribe((event) => {
211
+ if (isRemote(event)) return;
199
212
  const family = runEventFamily(event.type);
213
+ const source = lifecycleSourceOfEvent(event);
200
214
  const record = /* @__PURE__ */ __name(() => ctx.record({
201
215
  type: this.type,
202
216
  ...family !== void 0 ? {
@@ -220,7 +234,13 @@ var DurableTelescopeWatcher = class {
220
234
  output: event.output,
221
235
  error: event.error,
222
236
  durationMs: event.durationMs,
223
- reason: event.reason
237
+ reason: event.reason,
238
+ triggeredRunId: event.triggeredRunId,
239
+ ...source ? {
240
+ lifecycleSourceRunId: source.runId,
241
+ lifecycleKind: source.kind,
242
+ lifecycleSourceTraceId: runTraceId(source.runId)
243
+ } : {}
224
244
  },
225
245
  tags: this.tags(event)
226
246
  }), "record");
@@ -244,6 +264,21 @@ var DurableTelescopeWatcher = class {
244
264
  }
245
265
  import_api3.context.with(import_api3.trace.setSpan(base, step), record);
246
266
  step.end();
267
+ } else if (source) {
268
+ const started = tracer.startSpan(`run ${event.workflow ?? event.runId}`, {
269
+ attributes: {
270
+ "durable.run_id": event.runId,
271
+ "durable.lifecycle.source_run_id": source.runId,
272
+ "durable.lifecycle.kind": source.kind
273
+ },
274
+ links: [
275
+ {
276
+ context: runSpanContext(source.runId)
277
+ }
278
+ ]
279
+ }, base);
280
+ import_api3.context.with(import_api3.trace.setSpan(base, started), record);
281
+ started.end();
247
282
  } else {
248
283
  import_api3.context.with(base, record);
249
284
  }
@@ -274,9 +309,28 @@ var DurableTelescopeWatcher = class {
274
309
  tags.push(event.type === "run.routed" ? "routed" : "unrouted");
275
310
  tags.push(`onError:${event.error?.code ?? "*"}`);
276
311
  }
312
+ if (event.type === "run.triggered") {
313
+ tags.push("triggered");
314
+ if (event.triggeredRunId) tags.push(`run:${event.triggeredRunId}`);
315
+ }
316
+ if (event.type === "run.trigger_failed") tags.push("trigger_failed", `subscriber:${event.name}`);
317
+ const source = lifecycleSourceOfEvent(event);
318
+ if (source) {
319
+ tags.push("lifecycle", `source:${source.runId}`);
320
+ const traceId = (0, import_nestjs_durable_core.traceIdOfTraceparent)(event.sourceTraceparent);
321
+ if (traceId) tags.push(`sourceTrace:${traceId}`);
322
+ }
277
323
  return tags;
278
324
  }
279
325
  };
326
+ function lifecycleSourceOfEvent(event) {
327
+ if (event.type !== "run.started" || !event.workflow) return void 0;
328
+ return (0, import_nestjs_durable_core.lifecycleSourceOf)({
329
+ id: event.runId,
330
+ workflow: event.workflow
331
+ });
332
+ }
333
+ __name(lifecycleSourceOfEvent, "lifecycleSourceOfEvent");
280
334
 
281
335
  // src/durable-telescope.extension.ts
282
336
  var import_nestjs_telescope2 = require("@dudousxd/nestjs-telescope");
@@ -398,6 +452,66 @@ function durableDashboard(opts = {}) {
398
452
  }
399
453
  ]
400
454
  },
455
+ {
456
+ kind: "table",
457
+ title: "Failed triggers",
458
+ data: {
459
+ provider: "durable.failedTriggers"
460
+ },
461
+ columns: [
462
+ {
463
+ key: "at",
464
+ label: "Time"
465
+ },
466
+ {
467
+ key: "workflow",
468
+ label: "Source workflow"
469
+ },
470
+ {
471
+ key: "runId",
472
+ label: "Source run",
473
+ link: {
474
+ href: runHref
475
+ }
476
+ },
477
+ {
478
+ key: "subscriber",
479
+ label: "Subscriber"
480
+ },
481
+ {
482
+ key: "error",
483
+ label: "Error"
484
+ }
485
+ ]
486
+ },
487
+ {
488
+ kind: "table",
489
+ title: "Owed lifecycle events",
490
+ data: {
491
+ provider: "durable.owedLifecycle"
492
+ },
493
+ columns: [
494
+ {
495
+ key: "settledAt",
496
+ label: "Settled"
497
+ },
498
+ {
499
+ key: "workflow",
500
+ label: "Workflow"
501
+ },
502
+ {
503
+ key: "runId",
504
+ label: "Run",
505
+ link: {
506
+ href: runHref
507
+ }
508
+ },
509
+ {
510
+ key: "owed",
511
+ label: "Owed to"
512
+ }
513
+ ]
514
+ },
401
515
  {
402
516
  kind: "table",
403
517
  // Lists ALL worker groups (starved ones sorted first); the Status column
@@ -425,6 +539,52 @@ function durableDashboard(opts = {}) {
425
539
  label: "Status"
426
540
  }
427
541
  ]
542
+ },
543
+ {
544
+ kind: "stat",
545
+ title: "Owed runs",
546
+ data: {
547
+ provider: "durable.owedLifecycle",
548
+ query: {
549
+ metric: "count"
550
+ }
551
+ },
552
+ spark: false,
553
+ thresholds: {
554
+ warn: 1,
555
+ bad: 50,
556
+ direction: "up-bad"
557
+ }
558
+ }
559
+ ]
560
+ },
561
+ {
562
+ // Workflows started when another one settles (`run.completed:<wf>` / `run.failed:<wf>`).
563
+ title: "Lifecycle",
564
+ cols: 2,
565
+ panels: [
566
+ {
567
+ kind: "timeseries",
568
+ title: "Lifecycle triggers over time",
569
+ data: {
570
+ provider: "durable.lifecycleTriggers"
571
+ },
572
+ series: [
573
+ "triggered",
574
+ "failed"
575
+ ],
576
+ style: "stacked"
577
+ },
578
+ {
579
+ kind: "topN",
580
+ title: "Triggered by subscriber",
581
+ data: {
582
+ provider: "durable.lifecycleTriggers",
583
+ query: {
584
+ metric: "bySubscriber"
585
+ }
586
+ },
587
+ limit: 10
428
588
  }
429
589
  ]
430
590
  },
@@ -1073,6 +1233,148 @@ function durableStateBreakdownProvider() {
1073
1233
  };
1074
1234
  }
1075
1235
  __name(durableStateBreakdownProvider, "durableStateBreakdownProvider");
1236
+ function stamp(at) {
1237
+ return at ? `${new Date(at).toISOString().replace("T", " ").slice(0, 16)}Z` : "";
1238
+ }
1239
+ __name(stamp, "stamp");
1240
+ async function triggerFailures(ctx, limit) {
1241
+ const seen = /* @__PURE__ */ new Set();
1242
+ const out = [];
1243
+ for (const entry of await fetchRunEntries(ctx, [
1244
+ "run.trigger_failed"
1245
+ ], limit)) {
1246
+ const c = entry.content ?? {};
1247
+ if (c.event !== "run.trigger_failed") continue;
1248
+ const key = `${c.runId}\0${c.name}`;
1249
+ if (seen.has(key)) continue;
1250
+ seen.add(key);
1251
+ out.push(entry);
1252
+ }
1253
+ return out;
1254
+ }
1255
+ __name(triggerFailures, "triggerFailures");
1256
+ function durableFailedTriggersProvider() {
1257
+ return {
1258
+ name: "durable.failedTriggers",
1259
+ async resolve(query, ctx) {
1260
+ const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));
1261
+ const windowMs = query?.windowMs === void 0 ? 24 * 60 * 60 * 1e3 : Number(query.windowMs);
1262
+ const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;
1263
+ const rows = (await triggerFailures(ctx, RUN_EVENTS_LIMIT)).filter((entry) => entryTime(entry) >= cutoff).slice(0, limit).map((entry) => {
1264
+ const c = entry.content ?? {};
1265
+ return {
1266
+ at: stamp(entry.createdAt),
1267
+ workflow: c.workflow ?? "",
1268
+ runId: c.runId ?? "",
1269
+ subscriber: c.name ?? "",
1270
+ error: c.error?.message ?? ""
1271
+ };
1272
+ });
1273
+ return {
1274
+ rows
1275
+ };
1276
+ }
1277
+ };
1278
+ }
1279
+ __name(durableFailedTriggersProvider, "durableFailedTriggersProvider");
1280
+ var LIFECYCLE_GRACE_MS = 3e4;
1281
+ function durableOwedLifecycleProvider() {
1282
+ return {
1283
+ name: "durable.owedLifecycle",
1284
+ async resolve(query, ctx) {
1285
+ const store = ctx.moduleRef.get(import_nestjs_durable_core2.STATE_STORE_CANONICAL, {
1286
+ strict: false
1287
+ });
1288
+ const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));
1289
+ const cutoff = Date.now() - LIFECYCLE_GRACE_MS;
1290
+ const runs = await store.listRuns({
1291
+ tag: import_nestjs_durable_core2.LIFECYCLE_PENDING_TAG,
1292
+ statuses: [
1293
+ "completed",
1294
+ "failed",
1295
+ "cancelled",
1296
+ "dead"
1297
+ ],
1298
+ limit: 1e3
1299
+ });
1300
+ const owed = runs.filter((run) => +new Date(run.updatedAt) <= cutoff);
1301
+ if (query?.metric === "count") return {
1302
+ value: owed.length
1303
+ };
1304
+ const rows = owed.sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt)).slice(0, limit).map((run) => ({
1305
+ settledAt: stamp(run.updatedAt),
1306
+ workflow: run.workflow,
1307
+ runId: run.id,
1308
+ owed: (0, import_nestjs_durable_core2.owedSubscribersOf)(run.tags).join(", ") || "every subscriber"
1309
+ }));
1310
+ return {
1311
+ rows
1312
+ };
1313
+ }
1314
+ };
1315
+ }
1316
+ __name(durableOwedLifecycleProvider, "durableOwedLifecycleProvider");
1317
+ function durableLifecycleTriggersProvider() {
1318
+ return {
1319
+ name: "durable.lifecycleTriggers",
1320
+ async resolve(query, ctx) {
1321
+ const seen = /* @__PURE__ */ new Set();
1322
+ const entries = (await fetchRunEntries(ctx, [
1323
+ "run.triggered",
1324
+ "run.trigger_failed"
1325
+ ])).filter((entry) => {
1326
+ const c = entry.content ?? {};
1327
+ if (c.event !== "run.triggered" && c.event !== "run.trigger_failed") return false;
1328
+ const key = `${c.event}\0${c.runId}\0${c.name}`;
1329
+ if (seen.has(key)) return false;
1330
+ seen.add(key);
1331
+ return true;
1332
+ });
1333
+ if (query?.metric === "bySubscriber") {
1334
+ const limit = Math.max(1, Number(query?.limit ?? 10));
1335
+ const counts = /* @__PURE__ */ new Map();
1336
+ for (const entry of entries) {
1337
+ const c = entry.content ?? {};
1338
+ if (c.event !== "run.triggered") continue;
1339
+ const label = `${c.workflow ?? "unknown"} \u2192 ${c.name ?? "unknown"}`;
1340
+ counts.set(label, (counts.get(label) ?? 0) + 1);
1341
+ }
1342
+ const items = [
1343
+ ...counts
1344
+ ].map(([label, value]) => ({
1345
+ label,
1346
+ value
1347
+ })).sort((a, b) => b.value - a.value || a.label.localeCompare(b.label)).slice(0, limit);
1348
+ return {
1349
+ items
1350
+ };
1351
+ }
1352
+ const n = Math.max(1, Number(query?.buckets ?? 24));
1353
+ const now = Date.now();
1354
+ let minT = now;
1355
+ for (const entry of entries) if (entry.createdAt) minT = Math.min(minT, entryTime(entry));
1356
+ const bucketSize = Math.max(now - minT, 1) / n;
1357
+ const rows = Array.from({
1358
+ length: n
1359
+ }, (_, i) => ({
1360
+ label: new Date(minT + i * bucketSize).toISOString().slice(11, 16),
1361
+ triggered: 0,
1362
+ failed: 0
1363
+ }));
1364
+ for (const entry of entries) {
1365
+ const index = Math.floor((entryTime(entry) - minT) / bucketSize);
1366
+ const row = rows[Math.min(n - 1, Math.max(0, index))];
1367
+ if (!row) continue;
1368
+ if (entry.content.event === "run.triggered") row.triggered += 1;
1369
+ else row.failed += 1;
1370
+ }
1371
+ return {
1372
+ rows
1373
+ };
1374
+ }
1375
+ };
1376
+ }
1377
+ __name(durableLifecycleTriggersProvider, "durableLifecycleTriggersProvider");
1076
1378
 
1077
1379
  // src/durable-telescope.extension.ts
1078
1380
  function durableTelescopeExtension(opts = {}) {
@@ -1101,7 +1403,10 @@ function durableTelescopeExtension(opts = {}) {
1101
1403
  durableRunsOverTimeProvider(),
1102
1404
  durableSuccessRateProvider(),
1103
1405
  durableThroughputProvider(),
1104
- durableStateBreakdownProvider()
1406
+ durableStateBreakdownProvider(),
1407
+ durableFailedTriggersProvider(),
1408
+ durableOwedLifecycleProvider(),
1409
+ durableLifecycleTriggersProvider()
1105
1410
  ], "dataProviders")
1106
1411
  });
1107
1412
  }
@@ -1113,6 +1418,9 @@ __name(durableTelescopeExtension, "durableTelescopeExtension");
1113
1418
  durableDashboard,
1114
1419
  durableDurationProvider,
1115
1420
  durableExecutionScope,
1421
+ durableFailedTriggersProvider,
1422
+ durableLifecycleTriggersProvider,
1423
+ durableOwedLifecycleProvider,
1116
1424
  durableRecentFailuresProvider,
1117
1425
  durableRunsOverTimeProvider,
1118
1426
  durableStateBreakdownProvider,
@@ -1124,6 +1432,7 @@ __name(durableTelescopeExtension, "durableTelescopeExtension");
1124
1432
  durableTraceContext,
1125
1433
  durableWorkerHealthProvider,
1126
1434
  durableWorkerStatusProvider,
1435
+ runSpanContext,
1127
1436
  runTraceContext,
1128
1437
  runTraceId
1129
1438
  });