@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.js CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
3
 
4
4
  // src/durable-telescope.watcher.ts
5
- import { WorkflowEngine, parentRunIdOf, useDurableExecution } from "@dudousxd/nestjs-durable-core";
5
+ import { WorkflowEngine, lifecycleSourceOf, parentRunIdOf, traceIdOfTraceparent, useDurableExecution } from "@dudousxd/nestjs-durable-core";
6
6
  import { SpanStatusCode as SpanStatusCode2, context as context3, trace as trace3 } from "@opentelemetry/api";
7
7
 
8
8
  // src/durable-execution-scope.ts
@@ -41,14 +41,18 @@ function runIds(runId) {
41
41
  }
42
42
  __name(runIds, "runIds");
43
43
  function runTraceContext(runId, parent = context.active()) {
44
- return trace.setSpanContext(parent, {
44
+ return trace.setSpanContext(parent, runSpanContext(runId));
45
+ }
46
+ __name(runTraceContext, "runTraceContext");
47
+ function runSpanContext(runId) {
48
+ return {
45
49
  traceId: runTraceId(runId),
46
50
  spanId: runSpanId(runId),
47
51
  traceFlags: TraceFlags.SAMPLED,
48
52
  isRemote: true
49
- });
53
+ };
50
54
  }
51
- __name(runTraceContext, "runTraceContext");
55
+ __name(runSpanContext, "runSpanContext");
52
56
 
53
57
  // src/durable-execution-scope.ts
54
58
  var activeScope = new AsyncLocalStorage();
@@ -135,7 +139,9 @@ var RUN_EVENT_TYPES = /* @__PURE__ */ new Set([
135
139
  "run.started",
136
140
  "run.completed",
137
141
  "run.failed",
138
- "run.suspended"
142
+ "run.suspended",
143
+ "run.triggered",
144
+ "run.trigger_failed"
139
145
  ]);
140
146
 
141
147
  // src/durable-telescope.watcher.ts
@@ -159,6 +165,7 @@ var DurableTelescopeWatcher = class {
159
165
  engine.subscribe((event) => {
160
166
  if (isRemote(event)) return;
161
167
  const family = runEventFamily(event.type);
168
+ const source = lifecycleSourceOfEvent(event);
162
169
  const record = /* @__PURE__ */ __name(() => ctx.record({
163
170
  type: this.type,
164
171
  ...family !== void 0 ? {
@@ -182,7 +189,13 @@ var DurableTelescopeWatcher = class {
182
189
  output: event.output,
183
190
  error: event.error,
184
191
  durationMs: event.durationMs,
185
- reason: event.reason
192
+ reason: event.reason,
193
+ triggeredRunId: event.triggeredRunId,
194
+ ...source ? {
195
+ lifecycleSourceRunId: source.runId,
196
+ lifecycleKind: source.kind,
197
+ lifecycleSourceTraceId: runTraceId(source.runId)
198
+ } : {}
186
199
  },
187
200
  tags: this.tags(event)
188
201
  }), "record");
@@ -206,6 +219,21 @@ var DurableTelescopeWatcher = class {
206
219
  }
207
220
  context3.with(trace3.setSpan(base, step), record);
208
221
  step.end();
222
+ } else if (source) {
223
+ const started = tracer.startSpan(`run ${event.workflow ?? event.runId}`, {
224
+ attributes: {
225
+ "durable.run_id": event.runId,
226
+ "durable.lifecycle.source_run_id": source.runId,
227
+ "durable.lifecycle.kind": source.kind
228
+ },
229
+ links: [
230
+ {
231
+ context: runSpanContext(source.runId)
232
+ }
233
+ ]
234
+ }, base);
235
+ context3.with(trace3.setSpan(base, started), record);
236
+ started.end();
209
237
  } else {
210
238
  context3.with(base, record);
211
239
  }
@@ -236,9 +264,28 @@ var DurableTelescopeWatcher = class {
236
264
  tags.push(event.type === "run.routed" ? "routed" : "unrouted");
237
265
  tags.push(`onError:${event.error?.code ?? "*"}`);
238
266
  }
267
+ if (event.type === "run.triggered") {
268
+ tags.push("triggered");
269
+ if (event.triggeredRunId) tags.push(`run:${event.triggeredRunId}`);
270
+ }
271
+ if (event.type === "run.trigger_failed") tags.push("trigger_failed", `subscriber:${event.name}`);
272
+ const source = lifecycleSourceOfEvent(event);
273
+ if (source) {
274
+ tags.push("lifecycle", `source:${source.runId}`);
275
+ const traceId = traceIdOfTraceparent(event.sourceTraceparent);
276
+ if (traceId) tags.push(`sourceTrace:${traceId}`);
277
+ }
239
278
  return tags;
240
279
  }
241
280
  };
281
+ function lifecycleSourceOfEvent(event) {
282
+ if (event.type !== "run.started" || !event.workflow) return void 0;
283
+ return lifecycleSourceOf({
284
+ id: event.runId,
285
+ workflow: event.workflow
286
+ });
287
+ }
288
+ __name(lifecycleSourceOfEvent, "lifecycleSourceOfEvent");
242
289
 
243
290
  // src/durable-telescope.extension.ts
244
291
  import { defineTelescopeExtension } from "@dudousxd/nestjs-telescope";
@@ -360,6 +407,66 @@ function durableDashboard(opts = {}) {
360
407
  }
361
408
  ]
362
409
  },
410
+ {
411
+ kind: "table",
412
+ title: "Failed triggers",
413
+ data: {
414
+ provider: "durable.failedTriggers"
415
+ },
416
+ columns: [
417
+ {
418
+ key: "at",
419
+ label: "Time"
420
+ },
421
+ {
422
+ key: "workflow",
423
+ label: "Source workflow"
424
+ },
425
+ {
426
+ key: "runId",
427
+ label: "Source run",
428
+ link: {
429
+ href: runHref
430
+ }
431
+ },
432
+ {
433
+ key: "subscriber",
434
+ label: "Subscriber"
435
+ },
436
+ {
437
+ key: "error",
438
+ label: "Error"
439
+ }
440
+ ]
441
+ },
442
+ {
443
+ kind: "table",
444
+ title: "Owed lifecycle events",
445
+ data: {
446
+ provider: "durable.owedLifecycle"
447
+ },
448
+ columns: [
449
+ {
450
+ key: "settledAt",
451
+ label: "Settled"
452
+ },
453
+ {
454
+ key: "workflow",
455
+ label: "Workflow"
456
+ },
457
+ {
458
+ key: "runId",
459
+ label: "Run",
460
+ link: {
461
+ href: runHref
462
+ }
463
+ },
464
+ {
465
+ key: "owed",
466
+ label: "Owed to"
467
+ }
468
+ ]
469
+ },
363
470
  {
364
471
  kind: "table",
365
472
  // Lists ALL worker groups (starved ones sorted first); the Status column
@@ -387,6 +494,52 @@ function durableDashboard(opts = {}) {
387
494
  label: "Status"
388
495
  }
389
496
  ]
497
+ },
498
+ {
499
+ kind: "stat",
500
+ title: "Owed runs",
501
+ data: {
502
+ provider: "durable.owedLifecycle",
503
+ query: {
504
+ metric: "count"
505
+ }
506
+ },
507
+ spark: false,
508
+ thresholds: {
509
+ warn: 1,
510
+ bad: 50,
511
+ direction: "up-bad"
512
+ }
513
+ }
514
+ ]
515
+ },
516
+ {
517
+ // Workflows started when another one settles (`run.completed:<wf>` / `run.failed:<wf>`).
518
+ title: "Lifecycle",
519
+ cols: 2,
520
+ panels: [
521
+ {
522
+ kind: "timeseries",
523
+ title: "Lifecycle triggers over time",
524
+ data: {
525
+ provider: "durable.lifecycleTriggers"
526
+ },
527
+ series: [
528
+ "triggered",
529
+ "failed"
530
+ ],
531
+ style: "stacked"
532
+ },
533
+ {
534
+ kind: "topN",
535
+ title: "Triggered by subscriber",
536
+ data: {
537
+ provider: "durable.lifecycleTriggers",
538
+ query: {
539
+ metric: "bySubscriber"
540
+ }
541
+ },
542
+ limit: 10
390
543
  }
391
544
  ]
392
545
  },
@@ -505,7 +658,7 @@ function durableDashboard(opts = {}) {
505
658
  __name(durableDashboard, "durableDashboard");
506
659
 
507
660
  // src/durable-data-providers.ts
508
- import { STATE_STORE_CANONICAL, WorkflowEngine as WorkflowEngine2 } from "@dudousxd/nestjs-durable-core";
661
+ import { LIFECYCLE_PENDING_TAG, STATE_STORE_CANONICAL, WorkflowEngine as WorkflowEngine2, owedSubscribersOf } from "@dudousxd/nestjs-durable-core";
509
662
  import { TELESCOPE_STORAGE } from "@dudousxd/nestjs-telescope";
510
663
  function percentile(sorted, p) {
511
664
  if (sorted.length === 0) return 0;
@@ -1035,6 +1188,148 @@ function durableStateBreakdownProvider() {
1035
1188
  };
1036
1189
  }
1037
1190
  __name(durableStateBreakdownProvider, "durableStateBreakdownProvider");
1191
+ function stamp(at) {
1192
+ return at ? `${new Date(at).toISOString().replace("T", " ").slice(0, 16)}Z` : "";
1193
+ }
1194
+ __name(stamp, "stamp");
1195
+ async function triggerFailures(ctx, limit) {
1196
+ const seen = /* @__PURE__ */ new Set();
1197
+ const out = [];
1198
+ for (const entry of await fetchRunEntries(ctx, [
1199
+ "run.trigger_failed"
1200
+ ], limit)) {
1201
+ const c = entry.content ?? {};
1202
+ if (c.event !== "run.trigger_failed") continue;
1203
+ const key = `${c.runId}\0${c.name}`;
1204
+ if (seen.has(key)) continue;
1205
+ seen.add(key);
1206
+ out.push(entry);
1207
+ }
1208
+ return out;
1209
+ }
1210
+ __name(triggerFailures, "triggerFailures");
1211
+ function durableFailedTriggersProvider() {
1212
+ return {
1213
+ name: "durable.failedTriggers",
1214
+ async resolve(query, ctx) {
1215
+ const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));
1216
+ const windowMs = query?.windowMs === void 0 ? 24 * 60 * 60 * 1e3 : Number(query.windowMs);
1217
+ const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;
1218
+ const rows = (await triggerFailures(ctx, RUN_EVENTS_LIMIT)).filter((entry) => entryTime(entry) >= cutoff).slice(0, limit).map((entry) => {
1219
+ const c = entry.content ?? {};
1220
+ return {
1221
+ at: stamp(entry.createdAt),
1222
+ workflow: c.workflow ?? "",
1223
+ runId: c.runId ?? "",
1224
+ subscriber: c.name ?? "",
1225
+ error: c.error?.message ?? ""
1226
+ };
1227
+ });
1228
+ return {
1229
+ rows
1230
+ };
1231
+ }
1232
+ };
1233
+ }
1234
+ __name(durableFailedTriggersProvider, "durableFailedTriggersProvider");
1235
+ var LIFECYCLE_GRACE_MS = 3e4;
1236
+ function durableOwedLifecycleProvider() {
1237
+ return {
1238
+ name: "durable.owedLifecycle",
1239
+ async resolve(query, ctx) {
1240
+ const store = ctx.moduleRef.get(STATE_STORE_CANONICAL, {
1241
+ strict: false
1242
+ });
1243
+ const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));
1244
+ const cutoff = Date.now() - LIFECYCLE_GRACE_MS;
1245
+ const runs = await store.listRuns({
1246
+ tag: LIFECYCLE_PENDING_TAG,
1247
+ statuses: [
1248
+ "completed",
1249
+ "failed",
1250
+ "cancelled",
1251
+ "dead"
1252
+ ],
1253
+ limit: 1e3
1254
+ });
1255
+ const owed = runs.filter((run) => +new Date(run.updatedAt) <= cutoff);
1256
+ if (query?.metric === "count") return {
1257
+ value: owed.length
1258
+ };
1259
+ const rows = owed.sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt)).slice(0, limit).map((run) => ({
1260
+ settledAt: stamp(run.updatedAt),
1261
+ workflow: run.workflow,
1262
+ runId: run.id,
1263
+ owed: owedSubscribersOf(run.tags).join(", ") || "every subscriber"
1264
+ }));
1265
+ return {
1266
+ rows
1267
+ };
1268
+ }
1269
+ };
1270
+ }
1271
+ __name(durableOwedLifecycleProvider, "durableOwedLifecycleProvider");
1272
+ function durableLifecycleTriggersProvider() {
1273
+ return {
1274
+ name: "durable.lifecycleTriggers",
1275
+ async resolve(query, ctx) {
1276
+ const seen = /* @__PURE__ */ new Set();
1277
+ const entries = (await fetchRunEntries(ctx, [
1278
+ "run.triggered",
1279
+ "run.trigger_failed"
1280
+ ])).filter((entry) => {
1281
+ const c = entry.content ?? {};
1282
+ if (c.event !== "run.triggered" && c.event !== "run.trigger_failed") return false;
1283
+ const key = `${c.event}\0${c.runId}\0${c.name}`;
1284
+ if (seen.has(key)) return false;
1285
+ seen.add(key);
1286
+ return true;
1287
+ });
1288
+ if (query?.metric === "bySubscriber") {
1289
+ const limit = Math.max(1, Number(query?.limit ?? 10));
1290
+ const counts = /* @__PURE__ */ new Map();
1291
+ for (const entry of entries) {
1292
+ const c = entry.content ?? {};
1293
+ if (c.event !== "run.triggered") continue;
1294
+ const label = `${c.workflow ?? "unknown"} \u2192 ${c.name ?? "unknown"}`;
1295
+ counts.set(label, (counts.get(label) ?? 0) + 1);
1296
+ }
1297
+ const items = [
1298
+ ...counts
1299
+ ].map(([label, value]) => ({
1300
+ label,
1301
+ value
1302
+ })).sort((a, b) => b.value - a.value || a.label.localeCompare(b.label)).slice(0, limit);
1303
+ return {
1304
+ items
1305
+ };
1306
+ }
1307
+ const n = Math.max(1, Number(query?.buckets ?? 24));
1308
+ const now = Date.now();
1309
+ let minT = now;
1310
+ for (const entry of entries) if (entry.createdAt) minT = Math.min(minT, entryTime(entry));
1311
+ const bucketSize = Math.max(now - minT, 1) / n;
1312
+ const rows = Array.from({
1313
+ length: n
1314
+ }, (_, i) => ({
1315
+ label: new Date(minT + i * bucketSize).toISOString().slice(11, 16),
1316
+ triggered: 0,
1317
+ failed: 0
1318
+ }));
1319
+ for (const entry of entries) {
1320
+ const index = Math.floor((entryTime(entry) - minT) / bucketSize);
1321
+ const row = rows[Math.min(n - 1, Math.max(0, index))];
1322
+ if (!row) continue;
1323
+ if (entry.content.event === "run.triggered") row.triggered += 1;
1324
+ else row.failed += 1;
1325
+ }
1326
+ return {
1327
+ rows
1328
+ };
1329
+ }
1330
+ };
1331
+ }
1332
+ __name(durableLifecycleTriggersProvider, "durableLifecycleTriggersProvider");
1038
1333
 
1039
1334
  // src/durable-telescope.extension.ts
1040
1335
  function durableTelescopeExtension(opts = {}) {
@@ -1063,7 +1358,10 @@ function durableTelescopeExtension(opts = {}) {
1063
1358
  durableRunsOverTimeProvider(),
1064
1359
  durableSuccessRateProvider(),
1065
1360
  durableThroughputProvider(),
1066
- durableStateBreakdownProvider()
1361
+ durableStateBreakdownProvider(),
1362
+ durableFailedTriggersProvider(),
1363
+ durableOwedLifecycleProvider(),
1364
+ durableLifecycleTriggersProvider()
1067
1365
  ], "dataProviders")
1068
1366
  });
1069
1367
  }
@@ -1074,6 +1372,9 @@ export {
1074
1372
  durableDashboard,
1075
1373
  durableDurationProvider,
1076
1374
  durableExecutionScope,
1375
+ durableFailedTriggersProvider,
1376
+ durableLifecycleTriggersProvider,
1377
+ durableOwedLifecycleProvider,
1077
1378
  durableRecentFailuresProvider,
1078
1379
  durableRunsOverTimeProvider,
1079
1380
  durableStateBreakdownProvider,
@@ -1085,6 +1386,7 @@ export {
1085
1386
  durableTraceContext,
1086
1387
  durableWorkerHealthProvider,
1087
1388
  durableWorkerStatusProvider,
1389
+ runSpanContext,
1088
1390
  runTraceContext,
1089
1391
  runTraceId
1090
1392
  };