@dudousxd/nestjs-durable-telescope 0.4.0 → 0.5.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/LICENSE +21 -0
- package/dist/index.cjs +763 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.ts +98 -5
- package/dist/index.js +725 -19
- package/dist/index.js.map +1 -1
- package/package.json +21 -7
- package/dist/durable-dashboard.spec-data.d.ts +0 -12
- package/dist/durable-dashboard.spec-data.d.ts.map +0 -1
- package/dist/durable-dashboard.spec-data.js +0 -88
- package/dist/durable-dashboard.spec-data.js.map +0 -1
- package/dist/durable-data-providers.d.ts +0 -25
- package/dist/durable-data-providers.d.ts.map +0 -1
- package/dist/durable-data-providers.js +0 -131
- package/dist/durable-data-providers.js.map +0 -1
- package/dist/durable-telescope.extension.d.ts +0 -6
- package/dist/durable-telescope.extension.d.ts.map +0 -1
- package/dist/durable-telescope.extension.js +0 -23
- package/dist/durable-telescope.extension.js.map +0 -1
- package/dist/durable-telescope.watcher.d.ts +0 -23
- package/dist/durable-telescope.watcher.d.ts.map +0 -1
- package/dist/durable-telescope.watcher.js +0 -91
- package/dist/durable-telescope.watcher.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,763 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
DurableTelescopeWatcher: () => DurableTelescopeWatcher,
|
|
25
|
+
durableDashboard: () => durableDashboard,
|
|
26
|
+
durableDurationProvider: () => durableDurationProvider,
|
|
27
|
+
durableRecentFailuresProvider: () => durableRecentFailuresProvider,
|
|
28
|
+
durableRunsOverTimeProvider: () => durableRunsOverTimeProvider,
|
|
29
|
+
durableStateBreakdownProvider: () => durableStateBreakdownProvider,
|
|
30
|
+
durableStateProvider: () => durableStateProvider,
|
|
31
|
+
durableSuccessRateProvider: () => durableSuccessRateProvider,
|
|
32
|
+
durableTelescopeExtension: () => durableTelescopeExtension,
|
|
33
|
+
durableThroughputProvider: () => durableThroughputProvider,
|
|
34
|
+
durableTimeseriesProvider: () => durableTimeseriesProvider,
|
|
35
|
+
durableWorkerHealthProvider: () => durableWorkerHealthProvider
|
|
36
|
+
});
|
|
37
|
+
module.exports = __toCommonJS(index_exports);
|
|
38
|
+
|
|
39
|
+
// src/durable-telescope.watcher.ts
|
|
40
|
+
var import_nestjs_durable_core = require("@dudousxd/nestjs-durable-core");
|
|
41
|
+
var import_api = require("@opentelemetry/api");
|
|
42
|
+
var DurableTelescopeWatcher = class {
|
|
43
|
+
static {
|
|
44
|
+
__name(this, "DurableTelescopeWatcher");
|
|
45
|
+
}
|
|
46
|
+
type = "durable";
|
|
47
|
+
/** Open root span per run, so step entries nest under it and share its traceId. */
|
|
48
|
+
roots = /* @__PURE__ */ new Map();
|
|
49
|
+
register(ctx) {
|
|
50
|
+
const engine = ctx.moduleRef.get(import_nestjs_durable_core.WorkflowEngine, {
|
|
51
|
+
strict: false
|
|
52
|
+
});
|
|
53
|
+
const tracer = import_api.trace.getTracer("@dudousxd/nestjs-durable-telescope");
|
|
54
|
+
engine.subscribe((event) => {
|
|
55
|
+
const record = /* @__PURE__ */ __name(() => ctx.record({
|
|
56
|
+
type: this.type,
|
|
57
|
+
content: {
|
|
58
|
+
event: event.type,
|
|
59
|
+
workflow: event.workflow,
|
|
60
|
+
runId: event.runId,
|
|
61
|
+
seq: event.seq,
|
|
62
|
+
name: event.name,
|
|
63
|
+
kind: event.kind,
|
|
64
|
+
output: event.output,
|
|
65
|
+
error: event.error,
|
|
66
|
+
durationMs: event.durationMs
|
|
67
|
+
},
|
|
68
|
+
tags: this.tags(event)
|
|
69
|
+
}), "record");
|
|
70
|
+
if (event.type === "run.started") {
|
|
71
|
+
this.roots.set(event.runId, tracer.startSpan(`workflow ${event.workflow ?? "?"}`, {
|
|
72
|
+
attributes: {
|
|
73
|
+
"durable.run_id": event.runId,
|
|
74
|
+
"durable.workflow": event.workflow
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
const root = this.roots.get(event.runId);
|
|
79
|
+
const base = root ? import_api.trace.setSpan(import_api.context.active(), root) : import_api.context.active();
|
|
80
|
+
if (event.name != null && event.seq != null) {
|
|
81
|
+
const step = tracer.startSpan(`step ${event.name}`, {
|
|
82
|
+
attributes: {
|
|
83
|
+
"durable.run_id": event.runId,
|
|
84
|
+
"durable.step.seq": event.seq,
|
|
85
|
+
"durable.step.kind": event.kind
|
|
86
|
+
}
|
|
87
|
+
}, base);
|
|
88
|
+
import_api.context.with(import_api.trace.setSpan(base, step), record);
|
|
89
|
+
step.end();
|
|
90
|
+
} else {
|
|
91
|
+
import_api.context.with(base, record);
|
|
92
|
+
}
|
|
93
|
+
if (event.type === "run.completed" || event.type === "run.suspended") {
|
|
94
|
+
root?.end();
|
|
95
|
+
this.roots.delete(event.runId);
|
|
96
|
+
} else if (event.type === "run.failed") {
|
|
97
|
+
const message = event.error?.message;
|
|
98
|
+
root?.setStatus(message !== void 0 ? {
|
|
99
|
+
code: import_api.SpanStatusCode.ERROR,
|
|
100
|
+
message
|
|
101
|
+
} : {
|
|
102
|
+
code: import_api.SpanStatusCode.ERROR
|
|
103
|
+
});
|
|
104
|
+
root?.end();
|
|
105
|
+
this.roots.delete(event.runId);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
tags(event) {
|
|
110
|
+
const tags = [
|
|
111
|
+
"durable",
|
|
112
|
+
event.type,
|
|
113
|
+
`run:${event.runId}`
|
|
114
|
+
];
|
|
115
|
+
if (event.workflow) tags.push(`workflow:${event.workflow}`);
|
|
116
|
+
if (event.kind) tags.push(`kind:${event.kind}`);
|
|
117
|
+
if (event.type === "run.failed") tags.push("failed");
|
|
118
|
+
return tags;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// src/durable-telescope.extension.ts
|
|
123
|
+
var import_nestjs_telescope2 = require("@dudousxd/nestjs-telescope");
|
|
124
|
+
|
|
125
|
+
// src/durable-dashboard.spec-data.ts
|
|
126
|
+
function durableDashboard(opts = {}) {
|
|
127
|
+
const runHref = opts.runHref ?? "/durable/runs/{runId}";
|
|
128
|
+
const windowMs = opts.recentFailuresWindowMs ?? 24 * 60 * 60 * 1e3;
|
|
129
|
+
return {
|
|
130
|
+
id: "durable.workflows",
|
|
131
|
+
label: "Workflows",
|
|
132
|
+
panels: [],
|
|
133
|
+
sections: [
|
|
134
|
+
{
|
|
135
|
+
title: "Health",
|
|
136
|
+
cols: 4,
|
|
137
|
+
panels: [
|
|
138
|
+
{
|
|
139
|
+
kind: "gauge",
|
|
140
|
+
title: "Success rate",
|
|
141
|
+
data: {
|
|
142
|
+
provider: "durable.successRate"
|
|
143
|
+
},
|
|
144
|
+
max: 1,
|
|
145
|
+
format: "percent",
|
|
146
|
+
thresholds: {
|
|
147
|
+
warn: 0.98,
|
|
148
|
+
bad: 0.95,
|
|
149
|
+
direction: "down-bad"
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
kind: "stat",
|
|
154
|
+
title: "Duration p95",
|
|
155
|
+
data: {
|
|
156
|
+
provider: "durable.duration",
|
|
157
|
+
query: {
|
|
158
|
+
metric: "p95"
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
format: "duration",
|
|
162
|
+
spark: false,
|
|
163
|
+
thresholds: {
|
|
164
|
+
warn: 2e3,
|
|
165
|
+
bad: 5e3,
|
|
166
|
+
direction: "up-bad"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
kind: "stat",
|
|
171
|
+
title: "Backlog",
|
|
172
|
+
data: {
|
|
173
|
+
provider: "durable.state",
|
|
174
|
+
query: {
|
|
175
|
+
status: "pending"
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
spark: false,
|
|
179
|
+
thresholds: {
|
|
180
|
+
warn: 50,
|
|
181
|
+
bad: 200,
|
|
182
|
+
direction: "up-bad"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
kind: "stat",
|
|
187
|
+
title: "Throughput",
|
|
188
|
+
data: {
|
|
189
|
+
provider: "durable.throughput"
|
|
190
|
+
},
|
|
191
|
+
format: "rate",
|
|
192
|
+
spark: true
|
|
193
|
+
}
|
|
194
|
+
]
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
title: "Needs attention",
|
|
198
|
+
cols: 3,
|
|
199
|
+
panels: [
|
|
200
|
+
{
|
|
201
|
+
kind: "topN",
|
|
202
|
+
title: "Top failing workflows",
|
|
203
|
+
data: {
|
|
204
|
+
provider: "durable.timeseries",
|
|
205
|
+
query: {
|
|
206
|
+
metric: "topFailures"
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
limit: 8
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
kind: "table",
|
|
213
|
+
title: "Stuck runs",
|
|
214
|
+
data: {
|
|
215
|
+
provider: "durable.recentFailures",
|
|
216
|
+
query: {
|
|
217
|
+
windowMs
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
columns: [
|
|
221
|
+
{
|
|
222
|
+
key: "updatedAt",
|
|
223
|
+
label: "Updated"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: "workflow",
|
|
227
|
+
label: "Workflow"
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
key: "runId",
|
|
231
|
+
label: "Run",
|
|
232
|
+
link: {
|
|
233
|
+
href: runHref
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: "error",
|
|
238
|
+
label: "Error"
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
kind: "table",
|
|
244
|
+
title: "Starved worker groups",
|
|
245
|
+
data: {
|
|
246
|
+
provider: "durable.workerHealth"
|
|
247
|
+
},
|
|
248
|
+
columns: [
|
|
249
|
+
{
|
|
250
|
+
key: "group",
|
|
251
|
+
label: "Group"
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
key: "queued",
|
|
255
|
+
label: "Queued"
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: "liveWorkers",
|
|
259
|
+
label: "Workers"
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
key: "status",
|
|
263
|
+
label: "Status"
|
|
264
|
+
}
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
]
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
title: "Trends",
|
|
271
|
+
cols: 3,
|
|
272
|
+
panels: [
|
|
273
|
+
{
|
|
274
|
+
kind: "timeseries",
|
|
275
|
+
title: "Runs over time",
|
|
276
|
+
data: {
|
|
277
|
+
provider: "durable.runsOverTime"
|
|
278
|
+
},
|
|
279
|
+
series: [
|
|
280
|
+
"done",
|
|
281
|
+
"failed"
|
|
282
|
+
],
|
|
283
|
+
style: "stacked"
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
kind: "distribution",
|
|
287
|
+
title: "Duration distribution",
|
|
288
|
+
data: {
|
|
289
|
+
provider: "durable.duration"
|
|
290
|
+
},
|
|
291
|
+
markers: [
|
|
292
|
+
"p50",
|
|
293
|
+
"p95",
|
|
294
|
+
"p99"
|
|
295
|
+
],
|
|
296
|
+
format: "duration"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
kind: "breakdown",
|
|
300
|
+
title: "Runs by state",
|
|
301
|
+
data: {
|
|
302
|
+
provider: "durable.stateBreakdown"
|
|
303
|
+
},
|
|
304
|
+
style: "donut"
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
__name(durableDashboard, "durableDashboard");
|
|
312
|
+
|
|
313
|
+
// src/durable-data-providers.ts
|
|
314
|
+
var import_nestjs_durable_core2 = require("@dudousxd/nestjs-durable-core");
|
|
315
|
+
var import_nestjs_telescope = require("@dudousxd/nestjs-telescope");
|
|
316
|
+
function percentile(sorted, p) {
|
|
317
|
+
if (sorted.length === 0) return 0;
|
|
318
|
+
const idx = Math.min(sorted.length - 1, Math.ceil(p / 100 * sorted.length) - 1);
|
|
319
|
+
return sorted[Math.max(0, idx)] ?? 0;
|
|
320
|
+
}
|
|
321
|
+
__name(percentile, "percentile");
|
|
322
|
+
async function fetchEntries(ctx) {
|
|
323
|
+
const storage = ctx.moduleRef.get(import_nestjs_telescope.TELESCOPE_STORAGE, {
|
|
324
|
+
strict: false
|
|
325
|
+
});
|
|
326
|
+
const page = await storage.get({
|
|
327
|
+
type: "durable",
|
|
328
|
+
limit: 5e3
|
|
329
|
+
});
|
|
330
|
+
return page.data;
|
|
331
|
+
}
|
|
332
|
+
__name(fetchEntries, "fetchEntries");
|
|
333
|
+
function splitWindows(entries, windowMs, now) {
|
|
334
|
+
const start = now - windowMs;
|
|
335
|
+
const prevStart = start - windowMs;
|
|
336
|
+
return {
|
|
337
|
+
current: entries.filter((e) => {
|
|
338
|
+
const t = e.createdAt ? +new Date(e.createdAt) : 0;
|
|
339
|
+
return t > start && t <= now;
|
|
340
|
+
}),
|
|
341
|
+
previous: entries.filter((e) => {
|
|
342
|
+
const t = e.createdAt ? +new Date(e.createdAt) : 0;
|
|
343
|
+
return t > prevStart && t <= start;
|
|
344
|
+
})
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
__name(splitWindows, "splitWindows");
|
|
348
|
+
function successRateOf(entries) {
|
|
349
|
+
let completed = 0;
|
|
350
|
+
let failed = 0;
|
|
351
|
+
for (const e of entries) {
|
|
352
|
+
const c = e.content ?? {};
|
|
353
|
+
if (c.event === "run.completed") completed += 1;
|
|
354
|
+
else if (c.event === "run.failed") failed += 1;
|
|
355
|
+
}
|
|
356
|
+
const total = completed + failed;
|
|
357
|
+
return total === 0 ? 1 : completed / total;
|
|
358
|
+
}
|
|
359
|
+
__name(successRateOf, "successRateOf");
|
|
360
|
+
var STATE_CAP = 1e4;
|
|
361
|
+
function durableStateProvider() {
|
|
362
|
+
return {
|
|
363
|
+
name: "durable.state",
|
|
364
|
+
async resolve(query, ctx) {
|
|
365
|
+
const store = ctx.moduleRef.get(import_nestjs_durable_core2.STATE_STORE, {
|
|
366
|
+
strict: false
|
|
367
|
+
});
|
|
368
|
+
const status = query?.status ?? "dead";
|
|
369
|
+
const runs = await store.listRuns({
|
|
370
|
+
status,
|
|
371
|
+
limit: STATE_CAP
|
|
372
|
+
});
|
|
373
|
+
return {
|
|
374
|
+
value: runs.length
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
__name(durableStateProvider, "durableStateProvider");
|
|
380
|
+
function durableTimeseriesProvider() {
|
|
381
|
+
return {
|
|
382
|
+
name: "durable.timeseries",
|
|
383
|
+
async resolve(query, ctx) {
|
|
384
|
+
const storage = ctx.moduleRef.get(import_nestjs_telescope.TELESCOPE_STORAGE, {
|
|
385
|
+
strict: false
|
|
386
|
+
});
|
|
387
|
+
const limit = Math.min(5e3, Math.max(100, Number(query?.limit ?? 2e3)));
|
|
388
|
+
const page = await storage.get({
|
|
389
|
+
type: "durable",
|
|
390
|
+
limit
|
|
391
|
+
});
|
|
392
|
+
let completed = 0;
|
|
393
|
+
let failed = 0;
|
|
394
|
+
const failByWorkflow = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const e of page.data) {
|
|
396
|
+
const c = e.content ?? {};
|
|
397
|
+
if (c.event === "run.completed") completed += 1;
|
|
398
|
+
else if (c.event === "run.failed") {
|
|
399
|
+
failed += 1;
|
|
400
|
+
const wf = c.workflow ?? "unknown";
|
|
401
|
+
failByWorkflow.set(wf, (failByWorkflow.get(wf) ?? 0) + 1);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const total = completed + failed;
|
|
405
|
+
const metric = query?.metric ?? "successRate";
|
|
406
|
+
if (metric === "successRate") return {
|
|
407
|
+
value: total === 0 ? 1 : completed / total
|
|
408
|
+
};
|
|
409
|
+
if (metric === "failed") return {
|
|
410
|
+
value: failed
|
|
411
|
+
};
|
|
412
|
+
if (metric === "total") return {
|
|
413
|
+
value: total
|
|
414
|
+
};
|
|
415
|
+
if (metric === "topFailures") {
|
|
416
|
+
const items = [
|
|
417
|
+
...failByWorkflow.entries()
|
|
418
|
+
].map(([label, value]) => ({
|
|
419
|
+
label,
|
|
420
|
+
value
|
|
421
|
+
})).sort((a, b) => b.value - a.value);
|
|
422
|
+
return {
|
|
423
|
+
items
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
value: total
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
__name(durableTimeseriesProvider, "durableTimeseriesProvider");
|
|
433
|
+
function durableRecentFailuresProvider() {
|
|
434
|
+
return {
|
|
435
|
+
name: "durable.recentFailures",
|
|
436
|
+
async resolve(query, ctx) {
|
|
437
|
+
const store = ctx.moduleRef.get(import_nestjs_durable_core2.STATE_STORE, {
|
|
438
|
+
strict: false
|
|
439
|
+
});
|
|
440
|
+
const limit = Math.min(200, Math.max(10, Number(query?.limit ?? 50)));
|
|
441
|
+
const windowMs = query?.windowMs === void 0 ? 24 * 60 * 60 * 1e3 : Number(query.windowMs);
|
|
442
|
+
const cutoff = windowMs > 0 ? Date.now() - windowMs : 0;
|
|
443
|
+
const [failed, dead] = await Promise.all([
|
|
444
|
+
store.listRuns({
|
|
445
|
+
status: "failed",
|
|
446
|
+
limit
|
|
447
|
+
}),
|
|
448
|
+
store.listRuns({
|
|
449
|
+
status: "dead",
|
|
450
|
+
limit
|
|
451
|
+
})
|
|
452
|
+
]);
|
|
453
|
+
const rows = [
|
|
454
|
+
...failed,
|
|
455
|
+
...dead
|
|
456
|
+
].filter((r) => +new Date(r.updatedAt) >= cutoff).sort((a, b) => +new Date(b.updatedAt) - +new Date(a.updatedAt)).slice(0, limit).map((r) => ({
|
|
457
|
+
// Compact UTC stamp ("YYYY-MM-DD HH:mm Z"); the generic table renderer
|
|
458
|
+
// prints column values as-is, so we format here.
|
|
459
|
+
updatedAt: `${new Date(r.updatedAt).toISOString().replace("T", " ").slice(0, 16)}Z`,
|
|
460
|
+
workflow: r.workflow,
|
|
461
|
+
runId: r.id,
|
|
462
|
+
error: r.error?.message ?? ""
|
|
463
|
+
}));
|
|
464
|
+
return {
|
|
465
|
+
rows
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
__name(durableRecentFailuresProvider, "durableRecentFailuresProvider");
|
|
471
|
+
function durableWorkerHealthProvider() {
|
|
472
|
+
return {
|
|
473
|
+
name: "durable.workerHealth",
|
|
474
|
+
async resolve(query, ctx) {
|
|
475
|
+
const engine = ctx.moduleRef.get(import_nestjs_durable_core2.WorkflowEngine, {
|
|
476
|
+
strict: false
|
|
477
|
+
});
|
|
478
|
+
const health = await engine.workerHealth();
|
|
479
|
+
const isStarved = /* @__PURE__ */ __name((g) => g.depth > 0 && g.liveWorkers.length === 0, "isStarved");
|
|
480
|
+
if (query?.metric === "starvedCount") {
|
|
481
|
+
return {
|
|
482
|
+
value: health.filter(isStarved).length
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const rows = health.slice().sort((a, b) => Number(isStarved(b)) - Number(isStarved(a)) || b.depth - a.depth).map((g) => ({
|
|
486
|
+
group: g.group,
|
|
487
|
+
queued: g.depth,
|
|
488
|
+
liveWorkers: g.liveWorkers.length,
|
|
489
|
+
status: isStarved(g) ? "STARVED" : "ok"
|
|
490
|
+
}));
|
|
491
|
+
return {
|
|
492
|
+
rows
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
__name(durableWorkerHealthProvider, "durableWorkerHealthProvider");
|
|
498
|
+
function durableDurationProvider() {
|
|
499
|
+
return {
|
|
500
|
+
name: "durable.duration",
|
|
501
|
+
async resolve(query, ctx) {
|
|
502
|
+
const entries = await fetchEntries(ctx);
|
|
503
|
+
const startedAt = /* @__PURE__ */ new Map();
|
|
504
|
+
for (const e of entries) {
|
|
505
|
+
const c = e.content ?? {};
|
|
506
|
+
if (c.event === "run.started" && c.runId && e.createdAt) {
|
|
507
|
+
startedAt.set(c.runId, +new Date(e.createdAt));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const durs = [];
|
|
511
|
+
for (const e of entries) {
|
|
512
|
+
const c = e.content ?? {};
|
|
513
|
+
if (c.event === "run.completed" || c.event === "run.failed") {
|
|
514
|
+
if (typeof c.durationMs === "number") {
|
|
515
|
+
durs.push(c.durationMs);
|
|
516
|
+
} else if (c.runId) {
|
|
517
|
+
const start = startedAt.get(c.runId);
|
|
518
|
+
const end = e.createdAt ? +new Date(e.createdAt) : void 0;
|
|
519
|
+
if (start !== void 0 && end !== void 0 && end >= start) {
|
|
520
|
+
durs.push(end - start);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
durs.sort((a, b) => a - b);
|
|
526
|
+
const p50 = percentile(durs, 50);
|
|
527
|
+
const p95 = percentile(durs, 95);
|
|
528
|
+
const p99 = percentile(durs, 99);
|
|
529
|
+
const metric = query?.metric;
|
|
530
|
+
if (metric === "p50") return {
|
|
531
|
+
value: p50
|
|
532
|
+
};
|
|
533
|
+
if (metric === "p95") return {
|
|
534
|
+
value: p95
|
|
535
|
+
};
|
|
536
|
+
if (metric === "p99") return {
|
|
537
|
+
value: p99
|
|
538
|
+
};
|
|
539
|
+
const max = durs.at(-1) ?? 0;
|
|
540
|
+
const bucketCount = 8;
|
|
541
|
+
const size = Math.max(1, Math.ceil((max + 1) / bucketCount));
|
|
542
|
+
const buckets = Array.from({
|
|
543
|
+
length: bucketCount
|
|
544
|
+
}, (_, i) => ({
|
|
545
|
+
label: `${Math.round(i * size / 100) / 10}s`,
|
|
546
|
+
count: 0
|
|
547
|
+
}));
|
|
548
|
+
for (const d of durs) {
|
|
549
|
+
const bi = Math.min(bucketCount - 1, Math.floor(d / size));
|
|
550
|
+
const bucket = buckets[bi];
|
|
551
|
+
if (bucket) bucket.count += 1;
|
|
552
|
+
}
|
|
553
|
+
return {
|
|
554
|
+
buckets,
|
|
555
|
+
p50,
|
|
556
|
+
p95,
|
|
557
|
+
p99
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
__name(durableDurationProvider, "durableDurationProvider");
|
|
563
|
+
function durableRunsOverTimeProvider() {
|
|
564
|
+
return {
|
|
565
|
+
name: "durable.runsOverTime",
|
|
566
|
+
async resolve(query, ctx) {
|
|
567
|
+
const entries = await fetchEntries(ctx);
|
|
568
|
+
const n = Number(query?.buckets ?? 24);
|
|
569
|
+
const now = Date.now();
|
|
570
|
+
let minT = now;
|
|
571
|
+
for (const e of entries) {
|
|
572
|
+
if (e.createdAt) {
|
|
573
|
+
const t = +new Date(e.createdAt);
|
|
574
|
+
if (t < minT) minT = t;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const span = Math.max(now - minT, 1);
|
|
578
|
+
const bucketSize = span / n;
|
|
579
|
+
const rows = Array.from({
|
|
580
|
+
length: n
|
|
581
|
+
}, (_, i) => {
|
|
582
|
+
const bucketStart = new Date(minT + i * bucketSize);
|
|
583
|
+
const label = bucketStart.toISOString().slice(11, 16);
|
|
584
|
+
return {
|
|
585
|
+
label,
|
|
586
|
+
done: 0,
|
|
587
|
+
failed: 0
|
|
588
|
+
};
|
|
589
|
+
});
|
|
590
|
+
for (const e of entries) {
|
|
591
|
+
const c = e.content ?? {};
|
|
592
|
+
if (c.event !== "run.completed" && c.event !== "run.failed") continue;
|
|
593
|
+
const t = e.createdAt ? +new Date(e.createdAt) : 0;
|
|
594
|
+
const idx = Math.min(n - 1, Math.floor((t - minT) / bucketSize));
|
|
595
|
+
const row = rows[idx];
|
|
596
|
+
if (row) {
|
|
597
|
+
if (c.event === "run.completed") row.done += 1;
|
|
598
|
+
else row.failed += 1;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
rows
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
__name(durableRunsOverTimeProvider, "durableRunsOverTimeProvider");
|
|
608
|
+
function durableSuccessRateProvider() {
|
|
609
|
+
return {
|
|
610
|
+
name: "durable.successRate",
|
|
611
|
+
async resolve(query, ctx) {
|
|
612
|
+
const entries = await fetchEntries(ctx);
|
|
613
|
+
const windowMs = Number(query?.windowMs ?? 24 * 60 * 60 * 1e3);
|
|
614
|
+
const now = Date.now();
|
|
615
|
+
const { current, previous } = splitWindows(entries, windowMs, now);
|
|
616
|
+
const value = successRateOf(current);
|
|
617
|
+
const prevRate = successRateOf(previous);
|
|
618
|
+
const delta = previous.length > 0 ? value - prevRate : void 0;
|
|
619
|
+
const sparkBuckets = 8;
|
|
620
|
+
const bucketSize = windowMs / sparkBuckets;
|
|
621
|
+
const sparkStart = now - windowMs;
|
|
622
|
+
const spark = Array.from({
|
|
623
|
+
length: sparkBuckets
|
|
624
|
+
}, (_, i) => {
|
|
625
|
+
const bStart = sparkStart + i * bucketSize;
|
|
626
|
+
const bEnd = bStart + bucketSize;
|
|
627
|
+
const bEntries = current.filter((e) => {
|
|
628
|
+
const t = e.createdAt ? +new Date(e.createdAt) : 0;
|
|
629
|
+
return t > bStart && t <= bEnd;
|
|
630
|
+
});
|
|
631
|
+
return successRateOf(bEntries);
|
|
632
|
+
});
|
|
633
|
+
return {
|
|
634
|
+
value,
|
|
635
|
+
delta,
|
|
636
|
+
spark
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
__name(durableSuccessRateProvider, "durableSuccessRateProvider");
|
|
642
|
+
function durableThroughputProvider() {
|
|
643
|
+
return {
|
|
644
|
+
name: "durable.throughput",
|
|
645
|
+
async resolve(query, ctx) {
|
|
646
|
+
const entries = await fetchEntries(ctx);
|
|
647
|
+
const windowMs = Number(query?.windowMs ?? 24 * 60 * 60 * 1e3);
|
|
648
|
+
const now = Date.now();
|
|
649
|
+
const { current, previous } = splitWindows(entries, windowMs, now);
|
|
650
|
+
const countCompleted = /* @__PURE__ */ __name((es) => es.filter((e) => (e.content ?? {}).event === "run.completed").length, "countCompleted");
|
|
651
|
+
const windowHours = windowMs / (60 * 60 * 1e3);
|
|
652
|
+
const value = countCompleted(current) / windowHours;
|
|
653
|
+
const prevValue = countCompleted(previous) / windowHours;
|
|
654
|
+
const delta = previous.length > 0 ? value - prevValue : void 0;
|
|
655
|
+
const sparkBuckets = 8;
|
|
656
|
+
const bucketSize = windowMs / sparkBuckets;
|
|
657
|
+
const bucketHours = bucketSize / (60 * 60 * 1e3);
|
|
658
|
+
const sparkStart = now - windowMs;
|
|
659
|
+
const spark = Array.from({
|
|
660
|
+
length: sparkBuckets
|
|
661
|
+
}, (_, i) => {
|
|
662
|
+
const bStart = sparkStart + i * bucketSize;
|
|
663
|
+
const bEnd = bStart + bucketSize;
|
|
664
|
+
const bEntries = current.filter((e) => {
|
|
665
|
+
const t = e.createdAt ? +new Date(e.createdAt) : 0;
|
|
666
|
+
return t > bStart && t <= bEnd;
|
|
667
|
+
});
|
|
668
|
+
return countCompleted(bEntries) / bucketHours;
|
|
669
|
+
});
|
|
670
|
+
return {
|
|
671
|
+
value,
|
|
672
|
+
delta,
|
|
673
|
+
spark
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
__name(durableThroughputProvider, "durableThroughputProvider");
|
|
679
|
+
var STATE_BREAKDOWN_PALETTE = [
|
|
680
|
+
"#34d399",
|
|
681
|
+
"#fbbf24",
|
|
682
|
+
"#f87171",
|
|
683
|
+
"#38bdf8",
|
|
684
|
+
"#a78bfa"
|
|
685
|
+
];
|
|
686
|
+
var STATE_BREAKDOWN_STATUSES = [
|
|
687
|
+
"running",
|
|
688
|
+
"pending",
|
|
689
|
+
"completed",
|
|
690
|
+
"failed",
|
|
691
|
+
"dead"
|
|
692
|
+
];
|
|
693
|
+
function durableStateBreakdownProvider() {
|
|
694
|
+
return {
|
|
695
|
+
name: "durable.stateBreakdown",
|
|
696
|
+
async resolve(_query, ctx) {
|
|
697
|
+
const store = ctx.moduleRef.get(import_nestjs_durable_core2.STATE_STORE, {
|
|
698
|
+
strict: false
|
|
699
|
+
});
|
|
700
|
+
const counts = await Promise.all(STATE_BREAKDOWN_STATUSES.map((status) => store.listRuns({
|
|
701
|
+
status,
|
|
702
|
+
limit: STATE_CAP
|
|
703
|
+
}).then((runs) => runs.length)));
|
|
704
|
+
const segments = STATE_BREAKDOWN_STATUSES.map((label, i) => ({
|
|
705
|
+
label,
|
|
706
|
+
value: counts[i],
|
|
707
|
+
color: STATE_BREAKDOWN_PALETTE[i]
|
|
708
|
+
}));
|
|
709
|
+
return {
|
|
710
|
+
segments
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
__name(durableStateBreakdownProvider, "durableStateBreakdownProvider");
|
|
716
|
+
|
|
717
|
+
// src/durable-telescope.extension.ts
|
|
718
|
+
function durableTelescopeExtension(opts = {}) {
|
|
719
|
+
return (0, import_nestjs_telescope2.defineTelescopeExtension)({
|
|
720
|
+
name: "durable",
|
|
721
|
+
watchers: /* @__PURE__ */ __name(() => [
|
|
722
|
+
new DurableTelescopeWatcher()
|
|
723
|
+
], "watchers"),
|
|
724
|
+
entryTypes: /* @__PURE__ */ __name(() => [
|
|
725
|
+
{
|
|
726
|
+
id: "durable",
|
|
727
|
+
label: "Workflows",
|
|
728
|
+
dot: "bg-amber-400"
|
|
729
|
+
}
|
|
730
|
+
], "entryTypes"),
|
|
731
|
+
dashboards: /* @__PURE__ */ __name(() => [
|
|
732
|
+
durableDashboard(opts)
|
|
733
|
+
], "dashboards"),
|
|
734
|
+
dataProviders: /* @__PURE__ */ __name(() => [
|
|
735
|
+
durableStateProvider(),
|
|
736
|
+
durableTimeseriesProvider(),
|
|
737
|
+
durableRecentFailuresProvider(),
|
|
738
|
+
durableWorkerHealthProvider(),
|
|
739
|
+
durableDurationProvider(),
|
|
740
|
+
durableRunsOverTimeProvider(),
|
|
741
|
+
durableSuccessRateProvider(),
|
|
742
|
+
durableThroughputProvider(),
|
|
743
|
+
durableStateBreakdownProvider()
|
|
744
|
+
], "dataProviders")
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
__name(durableTelescopeExtension, "durableTelescopeExtension");
|
|
748
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
749
|
+
0 && (module.exports = {
|
|
750
|
+
DurableTelescopeWatcher,
|
|
751
|
+
durableDashboard,
|
|
752
|
+
durableDurationProvider,
|
|
753
|
+
durableRecentFailuresProvider,
|
|
754
|
+
durableRunsOverTimeProvider,
|
|
755
|
+
durableStateBreakdownProvider,
|
|
756
|
+
durableStateProvider,
|
|
757
|
+
durableSuccessRateProvider,
|
|
758
|
+
durableTelescopeExtension,
|
|
759
|
+
durableThroughputProvider,
|
|
760
|
+
durableTimeseriesProvider,
|
|
761
|
+
durableWorkerHealthProvider
|
|
762
|
+
});
|
|
763
|
+
//# sourceMappingURL=index.cjs.map
|