@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.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,10 +139,14 @@ 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
148
+ var REMOTE_EVENT = Symbol.for("@dudousxd/nestjs-durable:remote-event");
149
+ var isRemote = /* @__PURE__ */ __name((event) => event[REMOTE_EVENT] === true, "isRemote");
142
150
  var DurableTelescopeWatcher = class {
143
151
  static {
144
152
  __name(this, "DurableTelescopeWatcher");
@@ -155,7 +163,9 @@ var DurableTelescopeWatcher = class {
155
163
  if (typeof engine?.subscribe !== "function") return;
156
164
  const tracer = trace3.getTracer("@dudousxd/nestjs-durable-telescope");
157
165
  engine.subscribe((event) => {
166
+ if (isRemote(event)) return;
158
167
  const family = runEventFamily(event.type);
168
+ const source = lifecycleSourceOfEvent(event);
159
169
  const record = /* @__PURE__ */ __name(() => ctx.record({
160
170
  type: this.type,
161
171
  ...family !== void 0 ? {
@@ -179,7 +189,13 @@ var DurableTelescopeWatcher = class {
179
189
  output: event.output,
180
190
  error: event.error,
181
191
  durationMs: event.durationMs,
182
- 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
+ } : {}
183
199
  },
184
200
  tags: this.tags(event)
185
201
  }), "record");
@@ -203,6 +219,21 @@ var DurableTelescopeWatcher = class {
203
219
  }
204
220
  context3.with(trace3.setSpan(base, step), record);
205
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();
206
237
  } else {
207
238
  context3.with(base, record);
208
239
  }
@@ -233,9 +264,28 @@ var DurableTelescopeWatcher = class {
233
264
  tags.push(event.type === "run.routed" ? "routed" : "unrouted");
234
265
  tags.push(`onError:${event.error?.code ?? "*"}`);
235
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
+ }
236
278
  return tags;
237
279
  }
238
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");
239
289
 
240
290
  // src/durable-telescope.extension.ts
241
291
  import { defineTelescopeExtension } from "@dudousxd/nestjs-telescope";
@@ -357,6 +407,66 @@ function durableDashboard(opts = {}) {
357
407
  }
358
408
  ]
359
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
+ },
360
470
  {
361
471
  kind: "table",
362
472
  // Lists ALL worker groups (starved ones sorted first); the Status column
@@ -384,6 +494,52 @@ function durableDashboard(opts = {}) {
384
494
  label: "Status"
385
495
  }
386
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
387
543
  }
388
544
  ]
389
545
  },
@@ -502,7 +658,7 @@ function durableDashboard(opts = {}) {
502
658
  __name(durableDashboard, "durableDashboard");
503
659
 
504
660
  // src/durable-data-providers.ts
505
- 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";
506
662
  import { TELESCOPE_STORAGE } from "@dudousxd/nestjs-telescope";
507
663
  function percentile(sorted, p) {
508
664
  if (sorted.length === 0) return 0;
@@ -1032,6 +1188,148 @@ function durableStateBreakdownProvider() {
1032
1188
  };
1033
1189
  }
1034
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");
1035
1333
 
1036
1334
  // src/durable-telescope.extension.ts
1037
1335
  function durableTelescopeExtension(opts = {}) {
@@ -1060,7 +1358,10 @@ function durableTelescopeExtension(opts = {}) {
1060
1358
  durableRunsOverTimeProvider(),
1061
1359
  durableSuccessRateProvider(),
1062
1360
  durableThroughputProvider(),
1063
- durableStateBreakdownProvider()
1361
+ durableStateBreakdownProvider(),
1362
+ durableFailedTriggersProvider(),
1363
+ durableOwedLifecycleProvider(),
1364
+ durableLifecycleTriggersProvider()
1064
1365
  ], "dataProviders")
1065
1366
  });
1066
1367
  }
@@ -1071,6 +1372,9 @@ export {
1071
1372
  durableDashboard,
1072
1373
  durableDurationProvider,
1073
1374
  durableExecutionScope,
1375
+ durableFailedTriggersProvider,
1376
+ durableLifecycleTriggersProvider,
1377
+ durableOwedLifecycleProvider,
1074
1378
  durableRecentFailuresProvider,
1075
1379
  durableRunsOverTimeProvider,
1076
1380
  durableStateBreakdownProvider,
@@ -1082,6 +1386,7 @@ export {
1082
1386
  durableTraceContext,
1083
1387
  durableWorkerHealthProvider,
1084
1388
  durableWorkerStatusProvider,
1389
+ runSpanContext,
1085
1390
  runTraceContext,
1086
1391
  runTraceId
1087
1392
  };