@dudousxd/nestjs-durable-telescope 0.7.2 → 0.8.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
@@ -22,8 +22,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  DurableTelescopeWatcher: () => DurableTelescopeWatcher,
25
+ currentDurableScope: () => currentDurableScope,
25
26
  durableDashboard: () => durableDashboard,
26
27
  durableDurationProvider: () => durableDurationProvider,
28
+ durableExecutionScope: () => durableExecutionScope,
27
29
  durableRecentFailuresProvider: () => durableRecentFailuresProvider,
28
30
  durableRunsOverTimeProvider: () => durableRunsOverTimeProvider,
29
31
  durableStateBreakdownProvider: () => durableStateBreakdownProvider,
@@ -32,30 +34,146 @@ __export(index_exports, {
32
34
  durableTelescopeExtension: () => durableTelescopeExtension,
33
35
  durableThroughputProvider: () => durableThroughputProvider,
34
36
  durableTimeseriesProvider: () => durableTimeseriesProvider,
37
+ durableTraceContext: () => durableTraceContext,
35
38
  durableWorkerHealthProvider: () => durableWorkerHealthProvider,
36
- durableWorkerStatusProvider: () => durableWorkerStatusProvider
39
+ durableWorkerStatusProvider: () => durableWorkerStatusProvider,
40
+ runTraceContext: () => runTraceContext,
41
+ runTraceId: () => runTraceId
37
42
  });
38
43
  module.exports = __toCommonJS(index_exports);
39
44
 
40
45
  // src/durable-telescope.watcher.ts
41
46
  var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
47
+ var import_api3 = require("@opentelemetry/api");
48
+
49
+ // src/durable-execution-scope.ts
50
+ var import_node_async_hooks = require("async_hooks");
51
+ var import_node_crypto2 = require("crypto");
52
+ var import_api2 = require("@opentelemetry/api");
53
+
54
+ // src/durable-run-trace.ts
55
+ var import_node_crypto = require("crypto");
42
56
  var import_api = require("@opentelemetry/api");
57
+ function runTraceId(runId) {
58
+ return (0, import_node_crypto.createHash)("sha256").update(`durable:run:${runId}`).digest("hex").slice(0, 32);
59
+ }
60
+ __name(runTraceId, "runTraceId");
61
+ function runSpanId(runId) {
62
+ return (0, import_node_crypto.createHash)("sha256").update(`durable:run-span:${runId}`).digest("hex").slice(0, 16);
63
+ }
64
+ __name(runSpanId, "runSpanId");
65
+ function runTraceContext(runId, parent = import_api.context.active()) {
66
+ return import_api.trace.setSpanContext(parent, {
67
+ traceId: runTraceId(runId),
68
+ spanId: runSpanId(runId),
69
+ traceFlags: import_api.TraceFlags.SAMPLED,
70
+ isRemote: true
71
+ });
72
+ }
73
+ __name(runTraceContext, "runTraceContext");
74
+
75
+ // src/durable-execution-scope.ts
76
+ var activeScope = new import_node_async_hooks.AsyncLocalStorage();
77
+ function currentDurableScope() {
78
+ return activeScope.getStore();
79
+ }
80
+ __name(currentDurableScope, "currentDurableScope");
81
+ var DURABLE_BATCH_ORIGIN = "queue";
82
+ function spanName(execution) {
83
+ if (execution.unit === "step") return `step ${execution.name ?? "?"}`;
84
+ return `workflow ${execution.workflow ?? "?"}`;
85
+ }
86
+ __name(spanName, "spanName");
87
+ function spanAttributes(execution) {
88
+ const attributes = {
89
+ "durable.run_id": execution.runId
90
+ };
91
+ if (execution.workflow !== void 0) attributes["durable.workflow"] = execution.workflow;
92
+ if (execution.name !== void 0) attributes["durable.step.name"] = execution.name;
93
+ if (execution.seq !== void 0) attributes["durable.step.seq"] = execution.seq;
94
+ if (execution.attempt !== void 0) attributes["durable.step.attempt"] = execution.attempt;
95
+ return attributes;
96
+ }
97
+ __name(spanAttributes, "spanAttributes");
98
+ function durableExecutionScope(ctx) {
99
+ const tracer = import_api2.trace.getTracer("@dudousxd/nestjs-durable-telescope");
100
+ const owner = {};
101
+ return (execution, next) => {
102
+ const open = activeScope.getStore();
103
+ if (open?.owner === owner && open.runId === execution.runId) return next();
104
+ if (ctx.config.enabled === false) return next();
105
+ try {
106
+ const parent = runTraceContext(execution.runId);
107
+ const span = tracer.startSpan(spanName(execution), {
108
+ attributes: spanAttributes(execution)
109
+ }, parent);
110
+ const spanContext = span.spanContext();
111
+ const scope = {
112
+ runId: execution.runId,
113
+ traceId: runTraceId(execution.runId),
114
+ spanId: span.isRecording() ? spanContext.spanId : (0, import_node_crypto2.randomBytes)(8).toString("hex"),
115
+ owner
116
+ };
117
+ return import_api2.context.with(import_api2.trace.setSpan(parent, span), () => activeScope.run(scope, () => ctx.runInBatch(DURABLE_BATCH_ORIGIN, next).then((value) => {
118
+ span.end();
119
+ return value;
120
+ }, (error) => {
121
+ span.setStatus({
122
+ code: import_api2.SpanStatusCode.ERROR,
123
+ ...error instanceof Error ? {
124
+ message: error.message
125
+ } : {}
126
+ });
127
+ span.end();
128
+ throw error;
129
+ })));
130
+ } catch {
131
+ return next();
132
+ }
133
+ };
134
+ }
135
+ __name(durableExecutionScope, "durableExecutionScope");
136
+ function durableTraceContext(delegate) {
137
+ return {
138
+ current() {
139
+ const fromDelegate = delegate?.current() ?? null;
140
+ if (fromDelegate) return fromDelegate;
141
+ const scope = activeScope.getStore();
142
+ return scope ? {
143
+ traceId: scope.traceId,
144
+ spanId: scope.spanId
145
+ } : null;
146
+ }
147
+ };
148
+ }
149
+ __name(durableTraceContext, "durableTraceContext");
150
+
151
+ // src/durable-telescope.watcher.ts
43
152
  var DurableTelescopeWatcher = class {
44
153
  static {
45
154
  __name(this, "DurableTelescopeWatcher");
46
155
  }
47
156
  type = "durable";
48
- /** Open root span per run, so step entries nest under it and share its traceId. */
49
- roots = /* @__PURE__ */ new Map();
157
+ /** Removes the execution wrapper from the engine's process-level registry. */
158
+ disposeScope;
50
159
  register(ctx) {
160
+ this.disposeScope?.();
161
+ this.disposeScope = (0, import_nestjs_durable_core.useDurableExecution)(durableExecutionScope(ctx));
51
162
  const engine = ctx.moduleRef.get(import_nestjs_durable_core.WorkflowEngine, {
52
163
  strict: false
53
164
  });
54
165
  if (typeof engine?.subscribe !== "function") return;
55
- const tracer = import_api.trace.getTracer("@dudousxd/nestjs-durable-telescope");
166
+ const tracer = import_api3.trace.getTracer("@dudousxd/nestjs-durable-telescope");
56
167
  engine.subscribe((event) => {
57
168
  const record = /* @__PURE__ */ __name(() => ctx.record({
58
169
  type: this.type,
170
+ // Stated explicitly rather than left to the ambient span, and the Recorder honours an
171
+ // explicit `traceId` above everything else. A lifecycle event is very often emitted
172
+ // OUTSIDE any execution scope — a remote step's result lands in a transport callback, a
173
+ // recovery sweep emits for a run this process never executed — and those entries are
174
+ // exactly the ones an operator follows into the trace. We know the run, so we know the
175
+ // trace; there is no reason to let it depend on where the emit happened to land.
176
+ traceId: runTraceId(event.runId),
59
177
  content: {
60
178
  event: event.type,
61
179
  workflow: event.workflow,
@@ -69,16 +187,7 @@ var DurableTelescopeWatcher = class {
69
187
  },
70
188
  tags: this.tags(event)
71
189
  }), "record");
72
- if (event.type === "run.started") {
73
- this.roots.set(event.runId, tracer.startSpan(`workflow ${event.workflow ?? "?"}`, {
74
- attributes: {
75
- "durable.run_id": event.runId,
76
- "durable.workflow": event.workflow
77
- }
78
- }));
79
- }
80
- const root = this.roots.get(event.runId);
81
- const base = root ? import_api.trace.setSpan(import_api.context.active(), root) : import_api.context.active();
190
+ const base = runTraceContext(event.runId);
82
191
  if (event.name != null && event.seq != null) {
83
192
  const step = tracer.startSpan(`step ${event.name}`, {
84
193
  attributes: {
@@ -87,27 +196,32 @@ var DurableTelescopeWatcher = class {
87
196
  "durable.step.kind": event.kind
88
197
  }
89
198
  }, base);
90
- import_api.context.with(import_api.trace.setSpan(base, step), record);
199
+ if (event.type === "step.failed") {
200
+ const message = event.error?.message;
201
+ step.setStatus(message !== void 0 ? {
202
+ code: import_api3.SpanStatusCode.ERROR,
203
+ message
204
+ } : {
205
+ code: import_api3.SpanStatusCode.ERROR
206
+ });
207
+ }
208
+ import_api3.context.with(import_api3.trace.setSpan(base, step), record);
91
209
  step.end();
92
210
  } else {
93
- import_api.context.with(base, record);
94
- }
95
- if (event.type === "run.completed" || event.type === "run.suspended") {
96
- root?.end();
97
- this.roots.delete(event.runId);
98
- } else if (event.type === "run.failed") {
99
- const message = event.error?.message;
100
- root?.setStatus(message !== void 0 ? {
101
- code: import_api.SpanStatusCode.ERROR,
102
- message
103
- } : {
104
- code: import_api.SpanStatusCode.ERROR
105
- });
106
- root?.end();
107
- this.roots.delete(event.runId);
211
+ import_api3.context.with(base, record);
108
212
  }
109
213
  });
110
214
  }
215
+ /**
216
+ * Detach the execution wrapper. The `Watcher` SPI has no teardown hook, so nothing calls this in a
217
+ * running app — where the wrapper should live exactly as long as the process does. It exists so a
218
+ * test that registers a watcher does not leave a wrapper behind in the process-level registry for
219
+ * the next test to trip over.
220
+ */
221
+ unregister() {
222
+ this.disposeScope?.();
223
+ this.disposeScope = void 0;
224
+ }
111
225
  tags(event) {
112
226
  const tags = [
113
227
  "durable",
@@ -284,7 +398,10 @@ function durableDashboard(opts = {}) {
284
398
  // One row per LIVE worker (flattened from each group's heartbeats), exposing the live
285
399
  // WorkerStatus the heartbeat carries: fixed vs. adaptive limit, in-flight saturation,
286
400
  // RAM%/CPU%, throughput, p95, and why the adaptive controller last moved. A worker from
287
- // an older SDK with no status still lists, with '—' in the measured columns. The
401
+ // an older SDK with no status still lists, reading 'n/a' — distinct from the '—' of a
402
+ // worker that reports fine but has nothing to measure yet, which is what an idle
403
+ // deployment looks like (throughput and p95 come off a rolling window of completions,
404
+ // so they are empty until a step finishes inside it). The
288
405
  // adaptive min–max range rides the In-flight column's row value (`minMax` key) for those
289
406
  // that want it; the visible columns stay readable.
290
407
  title: "Workers",
@@ -598,17 +715,23 @@ function relAgo(atMs) {
598
715
  return `${Math.round(h / 24)}d ago`;
599
716
  }
600
717
  __name(relAgo, "relAgo");
601
- function fmtLastAdjust(a) {
602
- if (!a) return "\u2014";
718
+ var NOT_REPORTED = "n/a";
719
+ var NOT_YET_MEASURED = "\u2014";
720
+ function fmtLastAdjust(status) {
721
+ if (!status) return NOT_REPORTED;
722
+ const a = status.lastAdjust;
723
+ if (!a) return NOT_YET_MEASURED;
603
724
  return `${a.reason} ${a.from}\u2192${a.to} ${relAgo(a.at)}`;
604
725
  }
605
726
  __name(fmtLastAdjust, "fmtLastAdjust");
606
- function fmtPct(n) {
607
- return typeof n === "number" ? Math.round(n) : "\u2014";
727
+ function fmtPct(n, status) {
728
+ if (typeof n === "number") return Math.round(n);
729
+ return status ? NOT_YET_MEASURED : NOT_REPORTED;
608
730
  }
609
731
  __name(fmtPct, "fmtPct");
610
- function fmtNum(n) {
611
- return typeof n === "number" ? Math.round(n) : "\u2014";
732
+ function fmtNum(n, status) {
733
+ if (typeof n === "number") return Math.round(n);
734
+ return status ? NOT_YET_MEASURED : NOT_REPORTED;
612
735
  }
613
736
  __name(fmtNum, "fmtNum");
614
737
  function durableWorkerStatusProvider() {
@@ -622,11 +745,11 @@ function durableWorkerStatusProvider() {
622
745
  const rows = health.slice().sort((a, b) => Number(b.depth > 0) - Number(a.depth > 0) || a.group.localeCompare(b.group)).flatMap((group) => group.liveWorkers.slice().sort((a, b) => a.instanceId.localeCompare(b.instanceId)).map((worker) => {
623
746
  const status = worker.status;
624
747
  const concurrency = status?.concurrency;
625
- const mode = concurrency?.mode ?? "\u2014";
626
- const limit = concurrency?.limit ?? "\u2014";
627
- const inFlight = status?.inFlight ?? "\u2014";
628
- const minMax = concurrency?.mode === "adaptive" ? `${concurrency.min ?? 1}\u2013${concurrency.max ?? 32}` : "\u2014";
629
- const saturation = concurrency && status ? `${status.inFlight}/${concurrency.limit}` : "\u2014";
748
+ const mode = concurrency?.mode ?? NOT_REPORTED;
749
+ const limit = concurrency?.limit ?? NOT_REPORTED;
750
+ const inFlight = status?.inFlight ?? NOT_REPORTED;
751
+ const minMax = concurrency?.mode === "adaptive" ? `${concurrency.min ?? 1}\u2013${concurrency.max ?? 32}` : NOT_REPORTED;
752
+ const saturation = concurrency && status ? `${status.inFlight}/${concurrency.limit}` : NOT_REPORTED;
630
753
  return {
631
754
  group: group.group,
632
755
  instanceId: worker.instanceId,
@@ -636,11 +759,11 @@ function durableWorkerStatusProvider() {
636
759
  inFlight,
637
760
  saturation,
638
761
  queued: group.depth,
639
- rssPct: fmtPct(status?.rssPct),
640
- cpuPct: fmtPct(status?.cpuPct),
641
- throughputPerMin: fmtNum(status?.throughputPerMin),
642
- p95Ms: fmtNum(status?.p95Ms),
643
- lastAdjust: fmtLastAdjust(status?.lastAdjust)
762
+ rssPct: fmtPct(status?.rssPct, status),
763
+ cpuPct: fmtPct(status?.cpuPct, status),
764
+ throughputPerMin: fmtNum(status?.throughputPerMin, status),
765
+ p95Ms: fmtNum(status?.p95Ms, status),
766
+ lastAdjust: fmtLastAdjust(status)
644
767
  };
645
768
  }));
646
769
  return {
@@ -906,8 +1029,10 @@ __name(durableTelescopeExtension, "durableTelescopeExtension");
906
1029
  // Annotate the CommonJS export names for ESM import in node:
907
1030
  0 && (module.exports = {
908
1031
  DurableTelescopeWatcher,
1032
+ currentDurableScope,
909
1033
  durableDashboard,
910
1034
  durableDurationProvider,
1035
+ durableExecutionScope,
911
1036
  durableRecentFailuresProvider,
912
1037
  durableRunsOverTimeProvider,
913
1038
  durableStateBreakdownProvider,
@@ -916,7 +1041,10 @@ __name(durableTelescopeExtension, "durableTelescopeExtension");
916
1041
  durableTelescopeExtension,
917
1042
  durableThroughputProvider,
918
1043
  durableTimeseriesProvider,
1044
+ durableTraceContext,
919
1045
  durableWorkerHealthProvider,
920
- durableWorkerStatusProvider
1046
+ durableWorkerStatusProvider,
1047
+ runTraceContext,
1048
+ runTraceId
921
1049
  });
922
1050
  //# sourceMappingURL=index.cjs.map