@diia-inhouse/workflow 2.5.1 → 2.6.1
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.d.ts +1 -2
- package/dist/interfaces/config.d.ts +0 -16
- package/dist/services/worker.js +3 -70
- package/dist/worker.d.ts +1 -3
- package/dist/worker.js +1 -2
- package/package.json +10 -11
- package/dist/interfaces/services/schedulesExporter.d.ts +0 -96
- package/dist/services/schedulesExporter.d.ts +0 -101
- package/dist/services/schedulesExporter.js +0 -456
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { SchedulesExporterConfig } from "./interfaces/services/schedulesExporter.js";
|
|
2
1
|
import { TemporalConfig } from "./interfaces/config.js";
|
|
3
2
|
import { getDataConverter } from "./encryption/dataConverter.js";
|
|
4
3
|
import { EncryptionCodec } from "./encryption/encryptionCodec.js";
|
|
5
4
|
import { decrypt, encrypt } from "./encryption/crypto.js";
|
|
6
5
|
import * as proto from "@temporalio/proto";
|
|
7
|
-
export { EncryptionCodec, type
|
|
6
|
+
export { EncryptionCodec, type TemporalConfig, decrypt, encrypt, getDataConverter, proto };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { SchedulesExporterConfig } from "./services/schedulesExporter.js";
|
|
2
1
|
import { ClientOptions, ConnectionOptions } from "@temporalio/client";
|
|
3
2
|
import { MetricsConfig } from "@diia-inhouse/diia-metrics";
|
|
4
3
|
import { QueueConnectionConfig } from "@diia-inhouse/diia-queue";
|
|
@@ -31,21 +30,6 @@ interface TemporalConfig extends Omit<ClientOptions, "dataConverter"> {
|
|
|
31
30
|
* (i.e. when `configFactory` and `deps` are provided).
|
|
32
31
|
*/
|
|
33
32
|
disableQueueConsumers?: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* Controls the SchedulesExporter, which polls Temporal Schedule and Visibility APIs and
|
|
36
|
-
* emits per-schedule + in-flight workflow gauges (`diia_schedule_*`, `diia_workflows_running`,
|
|
37
|
-
* etc.). Auto-started by `bootstrapWorker` / `initTemporalWorker` — services do not need
|
|
38
|
-
* to instantiate it themselves.
|
|
39
|
-
*
|
|
40
|
-
* Runs only in the worker process: when `workerInProcess === false` and the service is
|
|
41
|
-
* started without `bootstrapWorker`'s lifecycle path, the exporter is skipped (the worker
|
|
42
|
-
* process owns it).
|
|
43
|
-
*
|
|
44
|
-
* - Omit (default) — exporter starts with the defaults in `SchedulesExporterConfig`.
|
|
45
|
-
* - `false` — disable the exporter entirely (e.g. for clusters without advanced visibility).
|
|
46
|
-
* - Object — override polling intervals or other knobs. See `SchedulesExporterConfig`.
|
|
47
|
-
*/
|
|
48
|
-
schedulesExporter?: SchedulesExporterConfig | false;
|
|
49
33
|
}
|
|
50
34
|
interface AppConfig {
|
|
51
35
|
temporal: TemporalConfig;
|
package/dist/services/worker.js
CHANGED
|
@@ -2,7 +2,6 @@ import { getDataConverter } from "../encryption/dataConverter.js";
|
|
|
2
2
|
import "../encryption/index.js";
|
|
3
3
|
import { traceExporter } from "../instrumentation.js";
|
|
4
4
|
import { AsyncLocalStorageBridgeInterceptor } from "../interceptors/asyncLocalStorageBridge.js";
|
|
5
|
-
import { SchedulesExporter } from "./schedulesExporter.js";
|
|
6
5
|
import { buildWorkerIdentity } from "./worker/identity.js";
|
|
7
6
|
import { WorkerHealthService } from "./workerHealth.js";
|
|
8
7
|
import { EnvService } from "@diia-inhouse/env";
|
|
@@ -12,7 +11,6 @@ import { fileURLToPath } from "node:url";
|
|
|
12
11
|
import { Resource } from "@opentelemetry/resources";
|
|
13
12
|
import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
|
|
14
13
|
import { OpenTelemetryActivityInboundInterceptor, OpenTelemetryActivityOutboundInterceptor, makeWorkflowExporter } from "@temporalio/interceptors-opentelemetry/lib/worker/index.js";
|
|
15
|
-
import * as promClient from "prom-client";
|
|
16
14
|
//#region src/services/worker.ts
|
|
17
15
|
/**
|
|
18
16
|
* Applies service process configuration overrides when the worker runs separately.
|
|
@@ -148,17 +146,11 @@ async function initTemporalWorker(app, options) {
|
|
|
148
146
|
const logger = app.container.resolve("logger");
|
|
149
147
|
const asyncLocalStorage = app.container.resolve("asyncLocalStorage");
|
|
150
148
|
const instantiatedActivities = instantiateActivities(app, activities);
|
|
151
|
-
|
|
149
|
+
await (await initWorker(config, {
|
|
152
150
|
...workerOptions,
|
|
153
151
|
workflowsPath,
|
|
154
152
|
activities: instantiatedActivities
|
|
155
|
-
}, envService, logger, nodeTracerProvider, asyncLocalStorage);
|
|
156
|
-
const schedulesExporter = await startSchedulesExporter(app, config, logger);
|
|
157
|
-
try {
|
|
158
|
-
await worker.run();
|
|
159
|
-
} finally {
|
|
160
|
-
await schedulesExporter?.onDestroy().catch((err) => logger.error("SchedulesExporter shutdown failed", { err }));
|
|
161
|
-
}
|
|
153
|
+
}, envService, logger, nodeTracerProvider, asyncLocalStorage)).run();
|
|
162
154
|
}
|
|
163
155
|
/**
|
|
164
156
|
* Bootstraps and runs Temporal worker with graceful shutdown.
|
|
@@ -218,7 +210,6 @@ async function bootstrapWorker(app, options) {
|
|
|
218
210
|
activities: instantiatedActivities,
|
|
219
211
|
identity
|
|
220
212
|
}, envService, logger, nodeTracerProvider, asyncLocalStorage);
|
|
221
|
-
const schedulesExporter = await startSchedulesExporter(app, config, logger);
|
|
222
213
|
logger.info("Starting Temporal worker", {
|
|
223
214
|
taskQueue,
|
|
224
215
|
identity
|
|
@@ -237,7 +228,6 @@ async function bootstrapWorker(app, options) {
|
|
|
237
228
|
await worker.run();
|
|
238
229
|
} finally {
|
|
239
230
|
for (const signal of shutdownSignals) process.off(signal, signalHandler);
|
|
240
|
-
await schedulesExporter?.onDestroy().catch((err) => logger.error("SchedulesExporter shutdown failed", { err }));
|
|
241
231
|
}
|
|
242
232
|
}
|
|
243
233
|
function tryResolve(container, key) {
|
|
@@ -248,27 +238,6 @@ function tryResolve(container, key) {
|
|
|
248
238
|
}
|
|
249
239
|
}
|
|
250
240
|
/**
|
|
251
|
-
* Wires the SchedulesExporter to the in-process worker. Auto-enabled; opt out by setting
|
|
252
|
-
* `config.temporal.schedulesExporter` to `false`. Returns the exporter so callers can stop
|
|
253
|
-
* it on shutdown.
|
|
254
|
-
*/
|
|
255
|
-
async function startSchedulesExporter(app, config, logger) {
|
|
256
|
-
const exporterConfig = config.temporal.schedulesExporter;
|
|
257
|
-
if (exporterConfig === false) return;
|
|
258
|
-
const temporalClient = tryResolve(app.container, "temporalClient");
|
|
259
|
-
if (!temporalClient) {
|
|
260
|
-
logger.warn("SchedulesExporter not started: temporalClient is not registered in the DI container");
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
const exporter = new SchedulesExporter({
|
|
264
|
-
client: temporalClient,
|
|
265
|
-
taskQueue: config.temporal.taskQueue,
|
|
266
|
-
logger
|
|
267
|
-
}, exporterConfig ?? {});
|
|
268
|
-
await exporter.onInit();
|
|
269
|
-
return exporter;
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
241
|
* Initializes Temporal worker.
|
|
273
242
|
*
|
|
274
243
|
* @param config - Application configuration
|
|
@@ -298,7 +267,7 @@ async function initWorker({ temporal: temporalConfig, metrics: { custom: metrics
|
|
|
298
267
|
const workflowsPath = options.workflowsPath ? toWorkflowsPath(options.workflowsPath) : void 0;
|
|
299
268
|
const mergedInterceptors = mergeInterceptors(buildWorkerInterceptors(tracingEnabled, asyncLocalStorage, logger, workflowsPath), options.interceptors);
|
|
300
269
|
try {
|
|
301
|
-
|
|
270
|
+
return await Worker.create({
|
|
302
271
|
namespace,
|
|
303
272
|
taskQueue,
|
|
304
273
|
connection: await NativeConnection.connect({ address }),
|
|
@@ -308,46 +277,10 @@ async function initWorker({ temporal: temporalConfig, metrics: { custom: metrics
|
|
|
308
277
|
sinks: tracingEnabled ? { exporter: makeWorkflowExporter(traceExporter, resource) } : void 0,
|
|
309
278
|
interceptors: mergedInterceptors
|
|
310
279
|
});
|
|
311
|
-
if (workflowsPath && taskQueue) await emitRegisteredWorkflowTypes(workflowsPath, taskQueue, logger);
|
|
312
|
-
return worker;
|
|
313
280
|
} catch (err) {
|
|
314
281
|
logger?.error("Failed to create Temporal worker", { err });
|
|
315
282
|
throw new Error("Failed to create Temporal worker", { cause: err });
|
|
316
283
|
}
|
|
317
284
|
}
|
|
318
|
-
/**
|
|
319
|
-
* Dynamic-imports the workflows entrypoint module and emits
|
|
320
|
-
* `diia_workflow_registered_type_info{workflow_type, task_queue}=1` for every exported
|
|
321
|
-
* function. Pairs with `diia_schedule_*` series so a dashboard can detect zombie schedules
|
|
322
|
-
* (schedules whose workflow type is no longer registered on the same task queue).
|
|
323
|
-
*
|
|
324
|
-
* Failures are logged and swallowed — this is observability, not load-bearing.
|
|
325
|
-
*/
|
|
326
|
-
async function emitRegisteredWorkflowTypes(workflowsPath, taskQueue, logger) {
|
|
327
|
-
try {
|
|
328
|
-
const mod = await (workflowsPath.startsWith("file://") ? import(workflowsPath) : import(`file://${workflowsPath}`));
|
|
329
|
-
const existing = promClient.register.getSingleMetric("diia_workflow_registered_type_info");
|
|
330
|
-
const gauge = existing instanceof promClient.Gauge ? existing : new promClient.Gauge({
|
|
331
|
-
name: "diia_workflow_registered_type_info",
|
|
332
|
-
help: "1 if the workflow type is registered on this task queue (function exported from the worker entrypoint module)",
|
|
333
|
-
labelNames: ["workflow_type", "task_queue"]
|
|
334
|
-
});
|
|
335
|
-
const registered = [];
|
|
336
|
-
for (const [name, value] of Object.entries(mod)) if (typeof value === "function") {
|
|
337
|
-
gauge.set({
|
|
338
|
-
workflow_type: name,
|
|
339
|
-
task_queue: taskQueue
|
|
340
|
-
}, 1);
|
|
341
|
-
registered.push(name);
|
|
342
|
-
}
|
|
343
|
-
logger?.info("Registered workflow types emitted to Prometheus", {
|
|
344
|
-
taskQueue,
|
|
345
|
-
count: registered.length,
|
|
346
|
-
types: registered
|
|
347
|
-
});
|
|
348
|
-
} catch (err) {
|
|
349
|
-
logger?.warn("Failed to enumerate registered workflow types", { err });
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
285
|
//#endregion
|
|
353
286
|
export { applyServiceProcessConfig, applyWorkerProcessConfig, bootstrapWorker, initTemporalWorker, initWorker, instantiateActivities, toWorkflowsPath };
|
package/dist/worker.d.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { ScheduleCalendarEvent, ScheduleRecentAction, SchedulesExporterDeps } from "./interfaces/services/schedulesExporter.js";
|
|
2
1
|
import { workflowInterceptors } from "./interceptors.js";
|
|
3
2
|
import { ActivityClass, App, WorkerBootstrapOptions } from "./interfaces/services/worker.js";
|
|
4
3
|
import { WorkerHealthDetails, WorkerHealthService } from "./services/workerHealth.js";
|
|
5
4
|
import { buildWorkerIdentity } from "./services/worker/identity.js";
|
|
6
5
|
import { applyServiceProcessConfig, applyWorkerProcessConfig, bootstrapWorker, initTemporalWorker, initWorker, instantiateActivities, toWorkflowsPath } from "./services/worker.js";
|
|
7
|
-
import { SchedulesExporter } from "./services/schedulesExporter.js";
|
|
8
6
|
import { NativeConnection, Runtime, State, Worker, WorkerInterceptors, WorkerOptions, WorkerStatus, bundleWorkflowCode } from "@temporalio/worker";
|
|
9
|
-
export { ActivityClass, App, NativeConnection, Runtime, type
|
|
7
|
+
export { ActivityClass, App, NativeConnection, Runtime, type State, Worker, WorkerBootstrapOptions, WorkerHealthDetails, WorkerHealthService, type WorkerInterceptors, type WorkerOptions, type WorkerStatus, applyServiceProcessConfig, applyWorkerProcessConfig, bootstrapWorker, buildWorkerIdentity, bundleWorkflowCode, initTemporalWorker, initWorker, instantiateActivities, toWorkflowsPath, workflowInterceptors };
|
package/dist/worker.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { workflowInterceptors } from "./interceptors.js";
|
|
2
|
-
import { SchedulesExporter } from "./services/schedulesExporter.js";
|
|
3
2
|
import { buildWorkerIdentity } from "./services/worker/identity.js";
|
|
4
3
|
import { WorkerHealthService } from "./services/workerHealth.js";
|
|
5
4
|
import { applyServiceProcessConfig, applyWorkerProcessConfig, bootstrapWorker, initTemporalWorker, initWorker, instantiateActivities, toWorkflowsPath } from "./services/worker.js";
|
|
6
5
|
import { NativeConnection, Runtime, Worker, bundleWorkflowCode } from "@temporalio/worker";
|
|
7
|
-
export { NativeConnection, Runtime,
|
|
6
|
+
export { NativeConnection, Runtime, Worker, WorkerHealthService, applyServiceProcessConfig, applyWorkerProcessConfig, bootstrapWorker, buildWorkerIdentity, bundleWorkflowCode, initTemporalWorker, initWorker, instantiateActivities, toWorkflowsPath, workflowInterceptors };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diia-inhouse/workflow",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.1",
|
|
4
4
|
"description": "Workflow",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -88,8 +88,7 @@
|
|
|
88
88
|
"@diia-inhouse/env": ">=3.0.0",
|
|
89
89
|
"@diia-inhouse/healthcheck": ">=2.0.0",
|
|
90
90
|
"@diia-inhouse/types": ">=13.0.0",
|
|
91
|
-
"@diia-inhouse/utils": ">=6.0.0"
|
|
92
|
-
"prom-client": ">=15.0.0"
|
|
91
|
+
"@diia-inhouse/utils": ">=6.0.0"
|
|
93
92
|
},
|
|
94
93
|
"peerDependenciesMeta": {
|
|
95
94
|
"@diia-inhouse/diia-queue": {
|
|
@@ -101,15 +100,15 @@
|
|
|
101
100
|
},
|
|
102
101
|
"devDependencies": {
|
|
103
102
|
"@diia-inhouse/configs": "7.0.0",
|
|
104
|
-
"@diia-inhouse/diia-logger": "4.3.
|
|
105
|
-
"@diia-inhouse/diia-metrics": "7.1.
|
|
106
|
-
"@diia-inhouse/diia-queue": "14.0.
|
|
107
|
-
"@diia-inhouse/env": "3.3.
|
|
108
|
-
"@diia-inhouse/healthcheck": "2.1.
|
|
109
|
-
"@diia-inhouse/oxc-config": "1.10.
|
|
110
|
-
"@diia-inhouse/test": "8.2.
|
|
103
|
+
"@diia-inhouse/diia-logger": "4.3.5",
|
|
104
|
+
"@diia-inhouse/diia-metrics": "7.1.13",
|
|
105
|
+
"@diia-inhouse/diia-queue": "14.0.14",
|
|
106
|
+
"@diia-inhouse/env": "3.3.5",
|
|
107
|
+
"@diia-inhouse/healthcheck": "2.1.17",
|
|
108
|
+
"@diia-inhouse/oxc-config": "1.10.1",
|
|
109
|
+
"@diia-inhouse/test": "8.2.4",
|
|
111
110
|
"@diia-inhouse/types": "13.2.0",
|
|
112
|
-
"@diia-inhouse/utils": "6.0.
|
|
111
|
+
"@diia-inhouse/utils": "6.0.16",
|
|
113
112
|
"@types/lodash": "4.17.24",
|
|
114
113
|
"@types/node": "25.6.2",
|
|
115
114
|
"@types/yargs": "17.0.35",
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { Logger } from "@diia-inhouse/types";
|
|
2
|
-
import { Client } from "@temporalio/client";
|
|
3
|
-
|
|
4
|
-
//#region src/interfaces/services/schedulesExporter.d.ts
|
|
5
|
-
interface SchedulesExporterConfig {
|
|
6
|
-
/**
|
|
7
|
-
* Polling interval for `client.schedule.list()` and `describe()`. Default 30s.
|
|
8
|
-
*/
|
|
9
|
-
pollIntervalMs?: number;
|
|
10
|
-
/**
|
|
11
|
-
* Polling interval for the visibility query that powers `diia_workflows_running`.
|
|
12
|
-
* Default 10s. Ignored when `pollVisibility` is `false`.
|
|
13
|
-
*/
|
|
14
|
-
visibilityPollIntervalMs?: number;
|
|
15
|
-
/**
|
|
16
|
-
* If `false`, skip the visibility query (`workflow.list({ status=Running })`). Use this
|
|
17
|
-
* when the Temporal cluster does not have advanced visibility (Elasticsearch) enabled.
|
|
18
|
-
* Default `true`.
|
|
19
|
-
*/
|
|
20
|
-
pollVisibility?: boolean;
|
|
21
|
-
/**
|
|
22
|
-
* How many upcoming fire times to expose per schedule as `slot=0..N-1` gauges.
|
|
23
|
-
* Default 5.
|
|
24
|
-
*/
|
|
25
|
-
nextActionSlots?: number;
|
|
26
|
-
/**
|
|
27
|
-
* Maximum number of recent action events kept in memory for the `getRecentActions()`
|
|
28
|
-
* snapshot. Default 200.
|
|
29
|
-
*/
|
|
30
|
-
recentActionsHistorySize?: number;
|
|
31
|
-
/**
|
|
32
|
-
* If `false`, skip the completed-executions query that powers `diia_workflow_duration_seconds`.
|
|
33
|
-
* Default `true`. Disable when you have no advanced visibility, or when duration
|
|
34
|
-
* tracking is handled elsewhere (e.g. via the SDK's `workflow_endtoend_latency` metric).
|
|
35
|
-
*/
|
|
36
|
-
pollCompletions?: boolean;
|
|
37
|
-
/**
|
|
38
|
-
* Polling interval for the completed-executions query. Default 60s.
|
|
39
|
-
*/
|
|
40
|
-
completionsPollIntervalMs?: number;
|
|
41
|
-
/**
|
|
42
|
-
* How far back the FIRST completions poll looks (subsequent polls use a sliding window
|
|
43
|
-
* based on the previous poll time). Set this to roughly the histogram retention you want
|
|
44
|
-
* after a fresh restart. Default 1h.
|
|
45
|
-
*/
|
|
46
|
-
completionsLookbackMs?: number;
|
|
47
|
-
/**
|
|
48
|
-
* Maximum number of recently-observed completed run IDs kept in memory for de-duplication
|
|
49
|
-
* across overlapping polls. LRU-trimmed. Default 2000.
|
|
50
|
-
*/
|
|
51
|
-
completionsSeenCapacity?: number;
|
|
52
|
-
}
|
|
53
|
-
interface SchedulesExporterDeps {
|
|
54
|
-
/**
|
|
55
|
-
* Temporal client used to call `schedule.list`, `schedule.getHandle().describe`, and
|
|
56
|
-
* `workflow.list`. Typed as `Pick<Client, 'schedule' | 'workflow'>` so this interface
|
|
57
|
-
* stays decoupled from the `TemporalClient` wrapper class (avoids a cycle through
|
|
58
|
-
* `interfaces/config.ts`); the wrapper's `schedule`/`workflow` getters satisfy this shape.
|
|
59
|
-
*/
|
|
60
|
-
client: Pick<Client, "schedule" | "workflow">;
|
|
61
|
-
/**
|
|
62
|
-
* The worker's task queue. Schedules whose `action.taskQueue` does not match are
|
|
63
|
-
* ignored — this guarantees each service emits metrics only for the schedules it owns.
|
|
64
|
-
*/
|
|
65
|
-
taskQueue: string;
|
|
66
|
-
logger?: Logger;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Single upcoming schedule fire returned by `SchedulesExporter.getCalendarEvents()`.
|
|
70
|
-
* Each schedule contributes up to `nextActionSlots` events, ordered by `slot` ascending
|
|
71
|
-
* (slot 0 is the next fire).
|
|
72
|
-
*/
|
|
73
|
-
interface ScheduleCalendarEvent {
|
|
74
|
-
scheduleId: string;
|
|
75
|
-
workflowType: string;
|
|
76
|
-
taskQueue: string;
|
|
77
|
-
cadence: string;
|
|
78
|
-
slot: number;
|
|
79
|
-
fireAt: string;
|
|
80
|
-
fireAtMs: number;
|
|
81
|
-
paused: number;
|
|
82
|
-
lastSucceeded: number | null;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Single recent schedule action returned by `SchedulesExporter.getRecentActions()`.
|
|
86
|
-
*/
|
|
87
|
-
interface ScheduleRecentAction {
|
|
88
|
-
scheduleId: string;
|
|
89
|
-
workflowType: string;
|
|
90
|
-
taskQueue: string;
|
|
91
|
-
firedAt: string;
|
|
92
|
-
firedAtMs: number;
|
|
93
|
-
succeeded: number;
|
|
94
|
-
}
|
|
95
|
-
//#endregion
|
|
96
|
-
export { ScheduleCalendarEvent, ScheduleRecentAction, SchedulesExporterConfig, SchedulesExporterDeps };
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { ScheduleCalendarEvent, ScheduleRecentAction, SchedulesExporterConfig, SchedulesExporterDeps } from "../interfaces/services/schedulesExporter.js";
|
|
2
|
-
import { OnDestroy, OnInit } from "@diia-inhouse/types";
|
|
3
|
-
|
|
4
|
-
//#region src/services/schedulesExporter.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* Periodically polls the Temporal Schedule and Visibility APIs, exposing per-schedule and
|
|
7
|
-
* per-workflow-type business metrics that the SDK does not provide out of the box:
|
|
8
|
-
*
|
|
9
|
-
* - `diia_schedule_paused` (0/1)
|
|
10
|
-
* - `diia_schedule_last_action_age_seconds`
|
|
11
|
-
* - `diia_schedule_last_action_succeeded` (0/1)
|
|
12
|
-
* - `diia_schedule_next_action_at_seconds{slot}` — Unix seconds of upcoming fire
|
|
13
|
-
* - `diia_schedule_next_action_eta_seconds{slot}` — seconds from now until upcoming fire
|
|
14
|
-
|
|
15
|
-
* - `diia_schedule_cadence_seconds` — approximate gap between fires (slot1 − slot0, falls back to spec)
|
|
16
|
-
* - `diia_schedule_fires_total{result="ok"|"failed"}` — counter of observed schedule fires; pair
|
|
17
|
-
* with cadence to compute expected/day and detect missed runs
|
|
18
|
-
* - `diia_workflows_running` — count of executions in `Running` state for this task queue
|
|
19
|
-
* - `diia_workflows_oldest_running_age_seconds` — age of the oldest running workflow
|
|
20
|
-
*
|
|
21
|
-
* The `service` label is supplied by the `diia-metrics` default labels at scrape time, so it
|
|
22
|
-
* is intentionally absent from the metric labelNames.
|
|
23
|
-
*
|
|
24
|
-
* Schedules with `action.taskQueue` other than `deps.taskQueue` are ignored, so a service
|
|
25
|
-
* only emits metrics for schedules it actually owns. This avoids fan-out when many services
|
|
26
|
-
* share a Temporal namespace.
|
|
27
|
-
*
|
|
28
|
-
* The exporter does NOT run its own HTTP server. Metrics flow through the existing
|
|
29
|
-
* `diia-metrics` `/metrics` endpoint. If a service needs to expose the calendar event list
|
|
30
|
-
* or recent action feed as JSON (e.g. for a Grafana Infinity datasource), it should call
|
|
31
|
-
* {@link SchedulesExporter.getCalendarEvents} and {@link SchedulesExporter.getRecentActions}
|
|
32
|
-
* from a route on its existing HTTP framework.
|
|
33
|
-
*/
|
|
34
|
-
declare class SchedulesExporter implements OnInit, OnDestroy {
|
|
35
|
-
private readonly deps;
|
|
36
|
-
private readonly pollIntervalMs;
|
|
37
|
-
private readonly visibilityPollIntervalMs;
|
|
38
|
-
private readonly pollVisibility;
|
|
39
|
-
private readonly nextActionSlots;
|
|
40
|
-
private readonly historySize;
|
|
41
|
-
private readonly logger;
|
|
42
|
-
private readonly calendarEvents;
|
|
43
|
-
private readonly recentActionsLog;
|
|
44
|
-
private readonly paused;
|
|
45
|
-
private readonly lastActionAge;
|
|
46
|
-
private readonly lastActionSucceeded;
|
|
47
|
-
private readonly nextActionAt;
|
|
48
|
-
private readonly nextActionEta;
|
|
49
|
-
private readonly cadence;
|
|
50
|
-
private readonly running;
|
|
51
|
-
private readonly oldestRunningAge;
|
|
52
|
-
private readonly firesTotal;
|
|
53
|
-
private readonly workflowDuration;
|
|
54
|
-
private readonly seenCompletedRunIds;
|
|
55
|
-
private readonly seenCompletedRunIdSet;
|
|
56
|
-
private readonly seenActionKeys;
|
|
57
|
-
private readonly seenActionKeySet;
|
|
58
|
-
private static readonly ACTIONS_SEEN_CAPACITY;
|
|
59
|
-
private readonly lastSeenScheduleLabels;
|
|
60
|
-
private readonly pollCompletions;
|
|
61
|
-
private readonly completionsPollIntervalMs;
|
|
62
|
-
private readonly completionsLookbackMs;
|
|
63
|
-
private readonly completionsSeenCapacity;
|
|
64
|
-
private completionsLastPollMs;
|
|
65
|
-
private scheduleTimer;
|
|
66
|
-
private visibilityTimer;
|
|
67
|
-
private completionsTimer;
|
|
68
|
-
private schedulesPolling;
|
|
69
|
-
private runningPolling;
|
|
70
|
-
private completionsPolling;
|
|
71
|
-
private activeSchedulesPoll;
|
|
72
|
-
private activeRunningPoll;
|
|
73
|
-
private activeCompletionsPoll;
|
|
74
|
-
private stopped;
|
|
75
|
-
constructor(deps: SchedulesExporterDeps, config?: SchedulesExporterConfig);
|
|
76
|
-
onInit(): Promise<void>;
|
|
77
|
-
onDestroy(): Promise<void>;
|
|
78
|
-
private armSchedulesTimer;
|
|
79
|
-
private armVisibilityTimer;
|
|
80
|
-
private armCompletionsTimer;
|
|
81
|
-
private runSchedulesPoll;
|
|
82
|
-
private runRunningPoll;
|
|
83
|
-
private runCompletionsPoll;
|
|
84
|
-
/**
|
|
85
|
-
* Snapshot of the next-N upcoming fires across all tracked schedules.
|
|
86
|
-
* Each schedule contributes up to `nextActionSlots` events, ordered by `slot`.
|
|
87
|
-
* Mount on a service HTTP route to feed the Grafana Business Calendar panel via the
|
|
88
|
-
* Infinity datasource — the data shape is already calendar-event-shaped.
|
|
89
|
-
*/
|
|
90
|
-
getCalendarEvents(): readonly ScheduleCalendarEvent[];
|
|
91
|
-
/**
|
|
92
|
-
* Snapshot of the most recent schedule actions across all tracked schedules,
|
|
93
|
-
* ordered newest first. Capped at `recentActionsHistorySize` entries.
|
|
94
|
-
*/
|
|
95
|
-
getRecentActions(limit?: number): readonly ScheduleRecentAction[];
|
|
96
|
-
private pollSchedules;
|
|
97
|
-
private pollRunning;
|
|
98
|
-
private pollCompleted;
|
|
99
|
-
}
|
|
100
|
-
//#endregion
|
|
101
|
-
export { SchedulesExporter };
|
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
import * as promClient from "prom-client";
|
|
2
|
-
//#region src/services/schedulesExporter.ts
|
|
3
|
-
const SCHEDULE_LABELS = [
|
|
4
|
-
"schedule_id",
|
|
5
|
-
"workflow_type",
|
|
6
|
-
"task_queue"
|
|
7
|
-
];
|
|
8
|
-
const SCHEDULE_SLOT_LABELS = [...SCHEDULE_LABELS, "slot"];
|
|
9
|
-
const RUNNING_LABELS = ["workflow_type", "task_queue"];
|
|
10
|
-
/**
|
|
11
|
-
* Periodically polls the Temporal Schedule and Visibility APIs, exposing per-schedule and
|
|
12
|
-
* per-workflow-type business metrics that the SDK does not provide out of the box:
|
|
13
|
-
*
|
|
14
|
-
* - `diia_schedule_paused` (0/1)
|
|
15
|
-
* - `diia_schedule_last_action_age_seconds`
|
|
16
|
-
* - `diia_schedule_last_action_succeeded` (0/1)
|
|
17
|
-
* - `diia_schedule_next_action_at_seconds{slot}` — Unix seconds of upcoming fire
|
|
18
|
-
* - `diia_schedule_next_action_eta_seconds{slot}` — seconds from now until upcoming fire
|
|
19
|
-
|
|
20
|
-
* - `diia_schedule_cadence_seconds` — approximate gap between fires (slot1 − slot0, falls back to spec)
|
|
21
|
-
* - `diia_schedule_fires_total{result="ok"|"failed"}` — counter of observed schedule fires; pair
|
|
22
|
-
* with cadence to compute expected/day and detect missed runs
|
|
23
|
-
* - `diia_workflows_running` — count of executions in `Running` state for this task queue
|
|
24
|
-
* - `diia_workflows_oldest_running_age_seconds` — age of the oldest running workflow
|
|
25
|
-
*
|
|
26
|
-
* The `service` label is supplied by the `diia-metrics` default labels at scrape time, so it
|
|
27
|
-
* is intentionally absent from the metric labelNames.
|
|
28
|
-
*
|
|
29
|
-
* Schedules with `action.taskQueue` other than `deps.taskQueue` are ignored, so a service
|
|
30
|
-
* only emits metrics for schedules it actually owns. This avoids fan-out when many services
|
|
31
|
-
* share a Temporal namespace.
|
|
32
|
-
*
|
|
33
|
-
* The exporter does NOT run its own HTTP server. Metrics flow through the existing
|
|
34
|
-
* `diia-metrics` `/metrics` endpoint. If a service needs to expose the calendar event list
|
|
35
|
-
* or recent action feed as JSON (e.g. for a Grafana Infinity datasource), it should call
|
|
36
|
-
* {@link SchedulesExporter.getCalendarEvents} and {@link SchedulesExporter.getRecentActions}
|
|
37
|
-
* from a route on its existing HTTP framework.
|
|
38
|
-
*/
|
|
39
|
-
var SchedulesExporter = class SchedulesExporter {
|
|
40
|
-
deps;
|
|
41
|
-
pollIntervalMs;
|
|
42
|
-
visibilityPollIntervalMs;
|
|
43
|
-
pollVisibility;
|
|
44
|
-
nextActionSlots;
|
|
45
|
-
historySize;
|
|
46
|
-
logger;
|
|
47
|
-
calendarEvents = [];
|
|
48
|
-
recentActionsLog = [];
|
|
49
|
-
paused;
|
|
50
|
-
lastActionAge;
|
|
51
|
-
lastActionSucceeded;
|
|
52
|
-
nextActionAt;
|
|
53
|
-
nextActionEta;
|
|
54
|
-
cadence;
|
|
55
|
-
running;
|
|
56
|
-
oldestRunningAge;
|
|
57
|
-
firesTotal;
|
|
58
|
-
workflowDuration;
|
|
59
|
-
seenCompletedRunIds = [];
|
|
60
|
-
seenCompletedRunIdSet = /* @__PURE__ */ new Set();
|
|
61
|
-
seenActionKeys = [];
|
|
62
|
-
seenActionKeySet = /* @__PURE__ */ new Set();
|
|
63
|
-
static ACTIONS_SEEN_CAPACITY = 2e3;
|
|
64
|
-
lastSeenScheduleLabels = /* @__PURE__ */ new Map();
|
|
65
|
-
pollCompletions;
|
|
66
|
-
completionsPollIntervalMs;
|
|
67
|
-
completionsLookbackMs;
|
|
68
|
-
completionsSeenCapacity;
|
|
69
|
-
completionsLastPollMs;
|
|
70
|
-
scheduleTimer;
|
|
71
|
-
visibilityTimer;
|
|
72
|
-
completionsTimer;
|
|
73
|
-
schedulesPolling = false;
|
|
74
|
-
runningPolling = false;
|
|
75
|
-
completionsPolling = false;
|
|
76
|
-
activeSchedulesPoll;
|
|
77
|
-
activeRunningPoll;
|
|
78
|
-
activeCompletionsPoll;
|
|
79
|
-
stopped = false;
|
|
80
|
-
constructor(deps, config = {}) {
|
|
81
|
-
this.deps = deps;
|
|
82
|
-
this.pollIntervalMs = config.pollIntervalMs ?? 3e4;
|
|
83
|
-
this.visibilityPollIntervalMs = config.visibilityPollIntervalMs ?? 1e4;
|
|
84
|
-
this.pollVisibility = config.pollVisibility ?? true;
|
|
85
|
-
this.nextActionSlots = config.nextActionSlots ?? 5;
|
|
86
|
-
this.historySize = config.recentActionsHistorySize ?? 200;
|
|
87
|
-
this.pollCompletions = config.pollCompletions ?? true;
|
|
88
|
-
this.completionsPollIntervalMs = config.completionsPollIntervalMs ?? 6e4;
|
|
89
|
-
this.completionsLookbackMs = config.completionsLookbackMs ?? 3600 * 1e3;
|
|
90
|
-
this.completionsSeenCapacity = config.completionsSeenCapacity ?? 2e3;
|
|
91
|
-
this.completionsLastPollMs = Date.now() - this.completionsLookbackMs;
|
|
92
|
-
this.logger = deps.logger;
|
|
93
|
-
const scheduleLabels = [...SCHEDULE_LABELS];
|
|
94
|
-
const scheduleSlotLabels = [...SCHEDULE_SLOT_LABELS];
|
|
95
|
-
const runningLabels = [...RUNNING_LABELS];
|
|
96
|
-
this.paused = getOrCreateGauge("diia_schedule_paused", "1 if the schedule is paused", scheduleLabels);
|
|
97
|
-
this.lastActionAge = getOrCreateGauge("diia_schedule_last_action_age_seconds", "Seconds since the schedule last fired (across all action results)", scheduleLabels);
|
|
98
|
-
this.lastActionSucceeded = getOrCreateGauge("diia_schedule_last_action_succeeded", "1 if the most recent schedule action started a workflow successfully", scheduleLabels);
|
|
99
|
-
this.nextActionAt = getOrCreateGauge("diia_schedule_next_action_at_seconds", "Unix seconds of an upcoming scheduled action (slot=0 is the next fire)", scheduleSlotLabels);
|
|
100
|
-
this.nextActionEta = getOrCreateGauge("diia_schedule_next_action_eta_seconds", "Seconds from now until an upcoming scheduled action (slot=0 is the next fire)", scheduleSlotLabels);
|
|
101
|
-
this.cadence = getOrCreateGauge("diia_schedule_cadence_seconds", "Approximate cadence between fires, derived from the gap between the next two upcoming actions (falls back to spec for single-slot schedules)", scheduleLabels);
|
|
102
|
-
this.running = getOrCreateGauge("diia_workflows_running", "Currently running workflow executions for this task queue, by workflow type", runningLabels);
|
|
103
|
-
this.oldestRunningAge = getOrCreateGauge("diia_workflows_oldest_running_age_seconds", "Age of the oldest running workflow per workflow type", runningLabels);
|
|
104
|
-
this.firesTotal = getOrCreateCounter("diia_schedule_fires_total", "Total schedule fires observed by the exporter, partitioned by result (ok/failed). Compare with expected fires from `diia_schedule_cadence_seconds` to detect missed runs.", [...scheduleLabels, "result"]);
|
|
105
|
-
this.workflowDuration = getOrCreateHistogram("diia_workflow_duration_seconds", "End-to-end duration of completed (status=Completed) workflow executions per workflow_type. Sample is taken from Temporal Visibility close_time − start_time. Pair with `diia_schedule_fires_total` to compute per-schedule cost (fires/day × p95 duration).", ["workflow_type", "task_queue"], [
|
|
106
|
-
1,
|
|
107
|
-
5,
|
|
108
|
-
15,
|
|
109
|
-
60,
|
|
110
|
-
180,
|
|
111
|
-
600,
|
|
112
|
-
1800,
|
|
113
|
-
3600,
|
|
114
|
-
7200,
|
|
115
|
-
21600,
|
|
116
|
-
43200,
|
|
117
|
-
86400
|
|
118
|
-
]);
|
|
119
|
-
}
|
|
120
|
-
async onInit() {
|
|
121
|
-
this.runSchedulesPoll("initial");
|
|
122
|
-
if (this.pollVisibility) this.runRunningPoll("initial");
|
|
123
|
-
if (this.pollCompletions) this.runCompletionsPoll("initial");
|
|
124
|
-
this.armSchedulesTimer();
|
|
125
|
-
if (this.pollVisibility) this.armVisibilityTimer();
|
|
126
|
-
if (this.pollCompletions) this.armCompletionsTimer();
|
|
127
|
-
}
|
|
128
|
-
async onDestroy() {
|
|
129
|
-
this.stopped = true;
|
|
130
|
-
if (this.scheduleTimer) clearTimeout(this.scheduleTimer);
|
|
131
|
-
if (this.visibilityTimer) clearTimeout(this.visibilityTimer);
|
|
132
|
-
if (this.completionsTimer) clearTimeout(this.completionsTimer);
|
|
133
|
-
await Promise.allSettled([
|
|
134
|
-
this.activeSchedulesPoll,
|
|
135
|
-
this.activeRunningPoll,
|
|
136
|
-
this.activeCompletionsPoll
|
|
137
|
-
]);
|
|
138
|
-
}
|
|
139
|
-
armSchedulesTimer() {
|
|
140
|
-
if (this.stopped) return;
|
|
141
|
-
this.scheduleTimer = setTimeout(() => {
|
|
142
|
-
this.runSchedulesPoll("interval").finally(() => this.armSchedulesTimer());
|
|
143
|
-
}, this.pollIntervalMs);
|
|
144
|
-
this.scheduleTimer.unref();
|
|
145
|
-
}
|
|
146
|
-
armVisibilityTimer() {
|
|
147
|
-
if (this.stopped) return;
|
|
148
|
-
this.visibilityTimer = setTimeout(() => {
|
|
149
|
-
this.runRunningPoll("interval").finally(() => this.armVisibilityTimer());
|
|
150
|
-
}, this.visibilityPollIntervalMs);
|
|
151
|
-
this.visibilityTimer.unref();
|
|
152
|
-
}
|
|
153
|
-
armCompletionsTimer() {
|
|
154
|
-
if (this.stopped) return;
|
|
155
|
-
this.completionsTimer = setTimeout(() => {
|
|
156
|
-
this.runCompletionsPoll("interval").finally(() => this.armCompletionsTimer());
|
|
157
|
-
}, this.completionsPollIntervalMs);
|
|
158
|
-
this.completionsTimer.unref();
|
|
159
|
-
}
|
|
160
|
-
runSchedulesPoll(source) {
|
|
161
|
-
if (this.schedulesPolling) return Promise.resolve();
|
|
162
|
-
this.schedulesPolling = true;
|
|
163
|
-
const promise = (async () => {
|
|
164
|
-
try {
|
|
165
|
-
await this.pollSchedules();
|
|
166
|
-
} catch (err) {
|
|
167
|
-
this.logger?.error(`SchedulesExporter ${source} schedule poll failed`, { err });
|
|
168
|
-
} finally {
|
|
169
|
-
this.schedulesPolling = false;
|
|
170
|
-
this.activeSchedulesPoll = void 0;
|
|
171
|
-
}
|
|
172
|
-
})();
|
|
173
|
-
this.activeSchedulesPoll = promise;
|
|
174
|
-
return promise;
|
|
175
|
-
}
|
|
176
|
-
runRunningPoll(source) {
|
|
177
|
-
if (this.runningPolling) return Promise.resolve();
|
|
178
|
-
this.runningPolling = true;
|
|
179
|
-
const promise = (async () => {
|
|
180
|
-
try {
|
|
181
|
-
await this.pollRunning();
|
|
182
|
-
} catch (err) {
|
|
183
|
-
this.logger?.error(`SchedulesExporter ${source} visibility poll failed`, { err });
|
|
184
|
-
} finally {
|
|
185
|
-
this.runningPolling = false;
|
|
186
|
-
this.activeRunningPoll = void 0;
|
|
187
|
-
}
|
|
188
|
-
})();
|
|
189
|
-
this.activeRunningPoll = promise;
|
|
190
|
-
return promise;
|
|
191
|
-
}
|
|
192
|
-
runCompletionsPoll(source) {
|
|
193
|
-
if (this.completionsPolling) return Promise.resolve();
|
|
194
|
-
this.completionsPolling = true;
|
|
195
|
-
const promise = (async () => {
|
|
196
|
-
try {
|
|
197
|
-
await this.pollCompleted();
|
|
198
|
-
} catch (err) {
|
|
199
|
-
this.logger?.error(`SchedulesExporter ${source} completions poll failed`, { err });
|
|
200
|
-
} finally {
|
|
201
|
-
this.completionsPolling = false;
|
|
202
|
-
this.activeCompletionsPoll = void 0;
|
|
203
|
-
}
|
|
204
|
-
})();
|
|
205
|
-
this.activeCompletionsPoll = promise;
|
|
206
|
-
return promise;
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Snapshot of the next-N upcoming fires across all tracked schedules.
|
|
210
|
-
* Each schedule contributes up to `nextActionSlots` events, ordered by `slot`.
|
|
211
|
-
* Mount on a service HTTP route to feed the Grafana Business Calendar panel via the
|
|
212
|
-
* Infinity datasource — the data shape is already calendar-event-shaped.
|
|
213
|
-
*/
|
|
214
|
-
getCalendarEvents() {
|
|
215
|
-
return this.calendarEvents;
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Snapshot of the most recent schedule actions across all tracked schedules,
|
|
219
|
-
* ordered newest first. Capped at `recentActionsHistorySize` entries.
|
|
220
|
-
*/
|
|
221
|
-
getRecentActions(limit = 50) {
|
|
222
|
-
return this.recentActionsLog.slice(0, limit).map(({ key: _key, ...rest }) => rest);
|
|
223
|
-
}
|
|
224
|
-
async pollSchedules() {
|
|
225
|
-
const events = [];
|
|
226
|
-
const currentSeen = /* @__PURE__ */ new Map();
|
|
227
|
-
for await (const summary of this.deps.client.schedule.list()) {
|
|
228
|
-
const description = await this.deps.client.schedule.getHandle(summary.scheduleId).describe();
|
|
229
|
-
const action = description.action;
|
|
230
|
-
if (action.type !== "startWorkflow") continue;
|
|
231
|
-
if (action.taskQueue !== this.deps.taskQueue) continue;
|
|
232
|
-
const workflowType = action.workflowType;
|
|
233
|
-
const baseLabels = {
|
|
234
|
-
schedule_id: summary.scheduleId,
|
|
235
|
-
workflow_type: workflowType,
|
|
236
|
-
task_queue: this.deps.taskQueue
|
|
237
|
-
};
|
|
238
|
-
currentSeen.set(summary.scheduleId, baseLabels);
|
|
239
|
-
const info = description.info;
|
|
240
|
-
const state = description.state;
|
|
241
|
-
const recentActions = info.recentActions ?? [];
|
|
242
|
-
const upcoming = info.nextActionTimes ?? info.futureActionTimes ?? [];
|
|
243
|
-
const pausedFlag = state.paused ? 1 : 0;
|
|
244
|
-
const cadenceLabel = describeCadence(description.spec);
|
|
245
|
-
this.paused.set(baseLabels, pausedFlag);
|
|
246
|
-
const lastAction = recentActions[recentActions.length - 1];
|
|
247
|
-
const lastSucceeded = lastAction ? lastAction.action ? 1 : 0 : null;
|
|
248
|
-
if (lastAction) {
|
|
249
|
-
const at = lastAction.takenAt ?? lastAction.scheduledAt;
|
|
250
|
-
if (at) this.lastActionAge.set(baseLabels, (Date.now() - new Date(at).getTime()) / 1e3);
|
|
251
|
-
this.lastActionSucceeded.set(baseLabels, lastAction.action ? 1 : 0);
|
|
252
|
-
}
|
|
253
|
-
for (const recent of recentActions) {
|
|
254
|
-
const at = recent.takenAt ?? recent.scheduledAt;
|
|
255
|
-
if (!at) continue;
|
|
256
|
-
const key = `${summary.scheduleId}|${new Date(at).toISOString()}`;
|
|
257
|
-
if (this.seenActionKeySet.has(key)) continue;
|
|
258
|
-
this.seenActionKeySet.add(key);
|
|
259
|
-
this.seenActionKeys.push(key);
|
|
260
|
-
const succeeded = recent.action ? 1 : 0;
|
|
261
|
-
this.recentActionsLog.push({
|
|
262
|
-
key,
|
|
263
|
-
scheduleId: summary.scheduleId,
|
|
264
|
-
workflowType,
|
|
265
|
-
taskQueue: this.deps.taskQueue,
|
|
266
|
-
firedAt: new Date(at).toISOString(),
|
|
267
|
-
firedAtMs: new Date(at).getTime(),
|
|
268
|
-
succeeded
|
|
269
|
-
});
|
|
270
|
-
this.firesTotal.inc({
|
|
271
|
-
...baseLabels,
|
|
272
|
-
result: succeeded ? "ok" : "failed"
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
for (let slot = 0; slot < this.nextActionSlots; slot++) {
|
|
276
|
-
const at = upcoming[slot];
|
|
277
|
-
const slotLabels = {
|
|
278
|
-
...baseLabels,
|
|
279
|
-
slot: String(slot)
|
|
280
|
-
};
|
|
281
|
-
if (!at) {
|
|
282
|
-
this.nextActionAt.remove(slotLabels);
|
|
283
|
-
this.nextActionEta.remove(slotLabels);
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
const ms = new Date(at).getTime();
|
|
287
|
-
this.nextActionAt.set(slotLabels, ms / 1e3);
|
|
288
|
-
this.nextActionEta.set(slotLabels, Math.max(0, (ms - Date.now()) / 1e3));
|
|
289
|
-
events.push({
|
|
290
|
-
scheduleId: summary.scheduleId,
|
|
291
|
-
workflowType,
|
|
292
|
-
taskQueue: this.deps.taskQueue,
|
|
293
|
-
cadence: cadenceLabel,
|
|
294
|
-
slot,
|
|
295
|
-
fireAt: new Date(ms).toISOString(),
|
|
296
|
-
fireAtMs: ms,
|
|
297
|
-
paused: pausedFlag,
|
|
298
|
-
lastSucceeded
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
const cadenceSeconds = computeCadenceSeconds(upcoming, description.spec);
|
|
302
|
-
if (cadenceSeconds !== void 0) this.cadence.set(baseLabels, cadenceSeconds);
|
|
303
|
-
else this.cadence.remove(baseLabels);
|
|
304
|
-
}
|
|
305
|
-
this.calendarEvents.length = 0;
|
|
306
|
-
this.calendarEvents.push(...events);
|
|
307
|
-
this.recentActionsLog.sort((a, b) => b.firedAtMs - a.firedAtMs);
|
|
308
|
-
if (this.recentActionsLog.length > this.historySize) this.recentActionsLog.length = this.historySize;
|
|
309
|
-
while (this.seenActionKeys.length > SchedulesExporter.ACTIONS_SEEN_CAPACITY) {
|
|
310
|
-
const evicted = this.seenActionKeys.shift();
|
|
311
|
-
if (evicted) this.seenActionKeySet.delete(evicted);
|
|
312
|
-
}
|
|
313
|
-
for (const [scheduleId, labels] of this.lastSeenScheduleLabels) {
|
|
314
|
-
if (currentSeen.has(scheduleId)) continue;
|
|
315
|
-
this.paused.remove(labels);
|
|
316
|
-
this.lastActionAge.remove(labels);
|
|
317
|
-
this.lastActionSucceeded.remove(labels);
|
|
318
|
-
this.cadence.remove(labels);
|
|
319
|
-
this.firesTotal.remove({
|
|
320
|
-
...labels,
|
|
321
|
-
result: "ok"
|
|
322
|
-
});
|
|
323
|
-
this.firesTotal.remove({
|
|
324
|
-
...labels,
|
|
325
|
-
result: "failed"
|
|
326
|
-
});
|
|
327
|
-
for (let slot = 0; slot < this.nextActionSlots; slot++) {
|
|
328
|
-
const slotLabels = {
|
|
329
|
-
...labels,
|
|
330
|
-
slot: String(slot)
|
|
331
|
-
};
|
|
332
|
-
this.nextActionAt.remove(slotLabels);
|
|
333
|
-
this.nextActionEta.remove(slotLabels);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
this.lastSeenScheduleLabels.clear();
|
|
337
|
-
for (const [scheduleId, labels] of currentSeen) this.lastSeenScheduleLabels.set(scheduleId, labels);
|
|
338
|
-
}
|
|
339
|
-
async pollRunning() {
|
|
340
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
341
|
-
const query = `TaskQueue="${this.deps.taskQueue}" AND ExecutionStatus="Running"`;
|
|
342
|
-
for await (const wf of this.deps.client.workflow.list({ query })) {
|
|
343
|
-
const startMs = wf.startTime ? new Date(wf.startTime).getTime() : Date.now();
|
|
344
|
-
const existing = buckets.get(wf.type) ?? {
|
|
345
|
-
count: 0,
|
|
346
|
-
oldestStartMs: Date.now()
|
|
347
|
-
};
|
|
348
|
-
existing.count += 1;
|
|
349
|
-
if (startMs < existing.oldestStartMs) existing.oldestStartMs = startMs;
|
|
350
|
-
buckets.set(wf.type, existing);
|
|
351
|
-
}
|
|
352
|
-
this.running.reset();
|
|
353
|
-
this.oldestRunningAge.reset();
|
|
354
|
-
for (const [workflowType, { count, oldestStartMs }] of buckets) {
|
|
355
|
-
const labels = {
|
|
356
|
-
workflow_type: workflowType,
|
|
357
|
-
task_queue: this.deps.taskQueue
|
|
358
|
-
};
|
|
359
|
-
this.running.set(labels, count);
|
|
360
|
-
this.oldestRunningAge.set(labels, (Date.now() - oldestStartMs) / 1e3);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
async pollCompleted() {
|
|
364
|
-
const sinceMs = this.completionsLastPollMs;
|
|
365
|
-
const nowMs = Date.now();
|
|
366
|
-
const sinceIso = new Date(sinceMs).toISOString();
|
|
367
|
-
const query = `TaskQueue="${this.deps.taskQueue}" AND ExecutionStatus="Completed" AND CloseTime>="${sinceIso}"`;
|
|
368
|
-
let observed = 0;
|
|
369
|
-
for await (const wf of this.deps.client.workflow.list({ query })) {
|
|
370
|
-
const raw = wf.raw;
|
|
371
|
-
const rawJson = raw && typeof raw.toJSON === "function" ? raw.toJSON() : raw;
|
|
372
|
-
const runId = rawJson?.execution?.runId;
|
|
373
|
-
const workflowType = rawJson?.type?.name;
|
|
374
|
-
const startTimeRaw = rawJson?.startTime ?? (wf.startTime ? new Date(wf.startTime).toISOString() : void 0);
|
|
375
|
-
const closeTimeRaw = rawJson?.closeTime ?? (wf.closeTime ? new Date(wf.closeTime).toISOString() : void 0);
|
|
376
|
-
if (!runId || !workflowType || !startTimeRaw || !closeTimeRaw) continue;
|
|
377
|
-
if (this.seenCompletedRunIdSet.has(runId)) continue;
|
|
378
|
-
const durationSec = (new Date(closeTimeRaw).getTime() - new Date(startTimeRaw).getTime()) / 1e3;
|
|
379
|
-
if (durationSec < 0) continue;
|
|
380
|
-
this.workflowDuration.observe({
|
|
381
|
-
workflow_type: workflowType,
|
|
382
|
-
task_queue: this.deps.taskQueue
|
|
383
|
-
}, durationSec);
|
|
384
|
-
this.seenCompletedRunIdSet.add(runId);
|
|
385
|
-
this.seenCompletedRunIds.push(runId);
|
|
386
|
-
observed++;
|
|
387
|
-
}
|
|
388
|
-
while (this.seenCompletedRunIds.length > this.completionsSeenCapacity) {
|
|
389
|
-
const evicted = this.seenCompletedRunIds.shift();
|
|
390
|
-
if (evicted) this.seenCompletedRunIdSet.delete(evicted);
|
|
391
|
-
}
|
|
392
|
-
this.completionsLastPollMs = nowMs - this.completionsPollIntervalMs;
|
|
393
|
-
if (observed > 0) this.logger?.debug("SchedulesExporter pollCompleted observed completions", {
|
|
394
|
-
taskQueue: this.deps.taskQueue,
|
|
395
|
-
observed
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
function getOrCreateGauge(name, help, labelNames) {
|
|
400
|
-
const existing = promClient.register.getSingleMetric(name);
|
|
401
|
-
if (existing instanceof promClient.Gauge) return existing;
|
|
402
|
-
return new promClient.Gauge({
|
|
403
|
-
name,
|
|
404
|
-
help,
|
|
405
|
-
labelNames
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
function getOrCreateCounter(name, help, labelNames) {
|
|
409
|
-
const existing = promClient.register.getSingleMetric(name);
|
|
410
|
-
if (existing instanceof promClient.Counter) return existing;
|
|
411
|
-
return new promClient.Counter({
|
|
412
|
-
name,
|
|
413
|
-
help,
|
|
414
|
-
labelNames
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
function getOrCreateHistogram(name, help, labelNames, buckets) {
|
|
418
|
-
const existing = promClient.register.getSingleMetric(name);
|
|
419
|
-
if (existing instanceof promClient.Histogram) return existing;
|
|
420
|
-
return new promClient.Histogram({
|
|
421
|
-
name,
|
|
422
|
-
help,
|
|
423
|
-
labelNames,
|
|
424
|
-
buckets
|
|
425
|
-
});
|
|
426
|
-
}
|
|
427
|
-
function describeCadence(spec) {
|
|
428
|
-
const s = spec;
|
|
429
|
-
if (!s) return "unknown";
|
|
430
|
-
if (s.intervals?.length) {
|
|
431
|
-
const every = s.intervals[0].every;
|
|
432
|
-
if (typeof every === "number") return `every ${Math.round(every / 1e3)}s`;
|
|
433
|
-
return `every ${every.toString()}`;
|
|
434
|
-
}
|
|
435
|
-
if (s.cronExpressions?.length) return s.cronExpressions[0];
|
|
436
|
-
if (s.calendars?.length) {
|
|
437
|
-
const calendar = s.calendars[0];
|
|
438
|
-
return `cal ${calendar.hour ?? "*"}:${String(calendar.minute ?? 0).padStart(2, "0")}`;
|
|
439
|
-
}
|
|
440
|
-
return "unknown";
|
|
441
|
-
}
|
|
442
|
-
function computeCadenceSeconds(upcoming, spec) {
|
|
443
|
-
if (upcoming.length >= 2) {
|
|
444
|
-
const gap = new Date(upcoming[1]).getTime() - new Date(upcoming[0]).getTime();
|
|
445
|
-
if (gap > 0) return gap / 1e3;
|
|
446
|
-
}
|
|
447
|
-
const s = spec;
|
|
448
|
-
if (!s) return;
|
|
449
|
-
if (s.intervals?.length) {
|
|
450
|
-
const every = s.intervals[0].every;
|
|
451
|
-
if (typeof every === "number") return every / 1e3;
|
|
452
|
-
}
|
|
453
|
-
if (s.calendars?.length) return 86400;
|
|
454
|
-
}
|
|
455
|
-
//#endregion
|
|
456
|
-
export { SchedulesExporter };
|