@dudousxd/nestjs-durable-telescope 0.9.3 → 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,7 +184,9 @@ 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
@@ -200,6 +210,7 @@ var DurableTelescopeWatcher = class {
200
210
  engine.subscribe((event) => {
201
211
  if (isRemote(event)) return;
202
212
  const family = runEventFamily(event.type);
213
+ const source = lifecycleSourceOfEvent(event);
203
214
  const record = /* @__PURE__ */ __name(() => ctx.record({
204
215
  type: this.type,
205
216
  ...family !== void 0 ? {
@@ -223,7 +234,13 @@ var DurableTelescopeWatcher = class {
223
234
  output: event.output,
224
235
  error: event.error,
225
236
  durationMs: event.durationMs,
226
- 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
+ } : {}
227
244
  },
228
245
  tags: this.tags(event)
229
246
  }), "record");
@@ -247,6 +264,21 @@ var DurableTelescopeWatcher = class {
247
264
  }
248
265
  import_api3.context.with(import_api3.trace.setSpan(base, step), record);
249
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();
250
282
  } else {
251
283
  import_api3.context.with(base, record);
252
284
  }
@@ -277,9 +309,28 @@ var DurableTelescopeWatcher = class {
277
309
  tags.push(event.type === "run.routed" ? "routed" : "unrouted");
278
310
  tags.push(`onError:${event.error?.code ?? "*"}`);
279
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
+ }
280
323
  return tags;
281
324
  }
282
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");
283
334
 
284
335
  // src/durable-telescope.extension.ts
285
336
  var import_nestjs_telescope2 = require("@dudousxd/nestjs-telescope");
@@ -401,6 +452,66 @@ function durableDashboard(opts = {}) {
401
452
  }
402
453
  ]
403
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
+ },
404
515
  {
405
516
  kind: "table",
406
517
  // Lists ALL worker groups (starved ones sorted first); the Status column
@@ -428,6 +539,52 @@ function durableDashboard(opts = {}) {
428
539
  label: "Status"
429
540
  }
430
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
431
588
  }
432
589
  ]
433
590
  },
@@ -1076,6 +1233,148 @@ function durableStateBreakdownProvider() {
1076
1233
  };
1077
1234
  }
1078
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");
1079
1378
 
1080
1379
  // src/durable-telescope.extension.ts
1081
1380
  function durableTelescopeExtension(opts = {}) {
@@ -1104,7 +1403,10 @@ function durableTelescopeExtension(opts = {}) {
1104
1403
  durableRunsOverTimeProvider(),
1105
1404
  durableSuccessRateProvider(),
1106
1405
  durableThroughputProvider(),
1107
- durableStateBreakdownProvider()
1406
+ durableStateBreakdownProvider(),
1407
+ durableFailedTriggersProvider(),
1408
+ durableOwedLifecycleProvider(),
1409
+ durableLifecycleTriggersProvider()
1108
1410
  ], "dataProviders")
1109
1411
  });
1110
1412
  }
@@ -1116,6 +1418,9 @@ __name(durableTelescopeExtension, "durableTelescopeExtension");
1116
1418
  durableDashboard,
1117
1419
  durableDurationProvider,
1118
1420
  durableExecutionScope,
1421
+ durableFailedTriggersProvider,
1422
+ durableLifecycleTriggersProvider,
1423
+ durableOwedLifecycleProvider,
1119
1424
  durableRecentFailuresProvider,
1120
1425
  durableRunsOverTimeProvider,
1121
1426
  durableStateBreakdownProvider,
@@ -1127,6 +1432,7 @@ __name(durableTelescopeExtension, "durableTelescopeExtension");
1127
1432
  durableTraceContext,
1128
1433
  durableWorkerHealthProvider,
1129
1434
  durableWorkerStatusProvider,
1435
+ runSpanContext,
1130
1436
  runTraceContext,
1131
1437
  runTraceId
1132
1438
  });