@zintrust/workers 0.1.28 → 0.1.30
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/README.md +16 -1
- package/dist/AnomalyDetection.d.ts +4 -0
- package/dist/AnomalyDetection.js +8 -0
- package/dist/BroadcastWorker.d.ts +2 -0
- package/dist/CanaryController.js +49 -5
- package/dist/ChaosEngineering.js +13 -0
- package/dist/ClusterLock.js +21 -10
- package/dist/DeadLetterQueue.js +12 -8
- package/dist/MultiQueueWorker.d.ts +1 -1
- package/dist/MultiQueueWorker.js +12 -7
- package/dist/NotificationWorker.d.ts +2 -0
- package/dist/PriorityQueue.d.ts +2 -2
- package/dist/PriorityQueue.js +20 -21
- package/dist/ResourceMonitor.js +65 -38
- package/dist/WorkerFactory.d.ts +23 -3
- package/dist/WorkerFactory.js +420 -40
- package/dist/WorkerInit.js +8 -3
- package/dist/WorkerMetrics.d.ts +2 -1
- package/dist/WorkerMetrics.js +152 -93
- package/dist/WorkerRegistry.d.ts +6 -0
- package/dist/WorkerRegistry.js +70 -1
- package/dist/WorkerShutdown.d.ts +21 -0
- package/dist/WorkerShutdown.js +82 -9
- package/dist/WorkerShutdownDurableObject.d.ts +12 -0
- package/dist/WorkerShutdownDurableObject.js +41 -0
- package/dist/build-manifest.json +171 -99
- package/dist/createQueueWorker.d.ts +2 -0
- package/dist/createQueueWorker.js +42 -27
- package/dist/dashboard/types.d.ts +5 -0
- package/dist/dashboard/workers-api.js +136 -43
- package/dist/http/WorkerApiController.js +1 -0
- package/dist/http/WorkerController.js +133 -85
- package/dist/http/WorkerMonitoringService.d.ts +11 -0
- package/dist/http/WorkerMonitoringService.js +62 -0
- package/dist/http/middleware/CustomValidation.js +1 -1
- package/dist/http/middleware/EditWorkerValidation.d.ts +1 -1
- package/dist/http/middleware/EditWorkerValidation.js +7 -6
- package/dist/http/middleware/ProcessorPathSanitizer.js +101 -35
- package/dist/http/middleware/WorkerValidationChain.js +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/routes/workers.js +48 -6
- package/dist/storage/WorkerStore.d.ts +4 -1
- package/dist/storage/WorkerStore.js +55 -7
- package/dist/telemetry/api/TelemetryAPI.d.ts +46 -0
- package/dist/telemetry/api/TelemetryAPI.js +219 -0
- package/dist/telemetry/api/TelemetryMonitoringService.d.ts +17 -0
- package/dist/telemetry/api/TelemetryMonitoringService.js +113 -0
- package/dist/telemetry/components/AlertPanel.d.ts +1 -0
- package/dist/telemetry/components/AlertPanel.js +13 -0
- package/dist/telemetry/components/CostTracking.d.ts +1 -0
- package/dist/telemetry/components/CostTracking.js +14 -0
- package/dist/telemetry/components/ResourceUsageChart.d.ts +1 -0
- package/dist/telemetry/components/ResourceUsageChart.js +11 -0
- package/dist/telemetry/components/WorkerHealthChart.d.ts +1 -0
- package/dist/telemetry/components/WorkerHealthChart.js +11 -0
- package/dist/telemetry/index.d.ts +15 -0
- package/dist/telemetry/index.js +60 -0
- package/dist/telemetry/routes/dashboard.d.ts +6 -0
- package/dist/telemetry/routes/dashboard.js +608 -0
- package/dist/ui/router/EmbeddedAssets.d.ts +4 -0
- package/dist/ui/router/EmbeddedAssets.js +13 -0
- package/dist/ui/router/ui.js +100 -4
- package/package.json +9 -5
- package/src/AnomalyDetection.ts +9 -0
- package/src/CanaryController.ts +41 -5
- package/src/ChaosEngineering.ts +14 -0
- package/src/ClusterLock.ts +22 -9
- package/src/DeadLetterQueue.ts +13 -8
- package/src/MultiQueueWorker.ts +15 -8
- package/src/PriorityQueue.ts +21 -22
- package/src/ResourceMonitor.ts +72 -40
- package/src/WorkerFactory.ts +545 -49
- package/src/WorkerInit.ts +8 -3
- package/src/WorkerMetrics.ts +183 -105
- package/src/WorkerRegistry.ts +80 -1
- package/src/WorkerShutdown.ts +115 -9
- package/src/WorkerShutdownDurableObject.ts +64 -0
- package/src/createQueueWorker.ts +73 -30
- package/src/dashboard/types.ts +5 -0
- package/src/dashboard/workers-api.ts +165 -52
- package/src/http/WorkerApiController.ts +1 -0
- package/src/http/WorkerController.ts +167 -90
- package/src/http/WorkerMonitoringService.ts +77 -0
- package/src/http/middleware/CustomValidation.ts +1 -1
- package/src/http/middleware/EditWorkerValidation.ts +7 -6
- package/src/http/middleware/ProcessorPathSanitizer.ts +123 -36
- package/src/http/middleware/WorkerValidationChain.ts +1 -0
- package/src/index.ts +6 -1
- package/src/routes/workers.ts +66 -9
- package/src/storage/WorkerStore.ts +59 -9
- package/src/telemetry/api/TelemetryAPI.ts +292 -0
- package/src/telemetry/api/TelemetryMonitoringService.ts +149 -0
- package/src/telemetry/components/AlertPanel.ts +13 -0
- package/src/telemetry/components/CostTracking.ts +14 -0
- package/src/telemetry/components/ResourceUsageChart.ts +11 -0
- package/src/telemetry/components/WorkerHealthChart.ts +11 -0
- package/src/telemetry/index.ts +121 -0
- package/src/telemetry/public/assets/zintrust-logo.svg +15 -0
- package/src/telemetry/routes/dashboard.ts +638 -0
- package/src/telemetry/styles/tailwind.css +1 -0
- package/src/telemetry/styles/zintrust-theme.css +8 -0
- package/src/ui/router/EmbeddedAssets.ts +13 -0
- package/src/ui/router/ui.ts +112 -5
- package/src/ui/workers/index.html +2 -2
- package/src/ui/workers/main.js +232 -61
- package/src/ui/workers/zintrust.svg +30 -0
- package/dist/dashboard/workers-dashboard-ui.d.ts +0 -3
- package/dist/dashboard/workers-dashboard-ui.js +0 -1026
- package/dist/dashboard/workers-dashboard.d.ts +0 -4
- package/dist/dashboard/workers-dashboard.js +0 -904
|
@@ -1,8 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Logger,
|
|
3
|
+
NodeSingletons,
|
|
4
|
+
workersConfig,
|
|
5
|
+
type IRequest,
|
|
6
|
+
type IResponse,
|
|
7
|
+
} from '@zintrust/core';
|
|
2
8
|
|
|
3
9
|
export type RouteHandler = (req: IRequest, res: IResponse) => Promise<void> | void;
|
|
4
10
|
|
|
5
|
-
const PROCESSOR_PATH_PATTERN = /^[a-zA-Z0-9/_.-]+\.(ts|js)$/;
|
|
11
|
+
const PROCESSOR_PATH_PATTERN = /^[a-zA-Z0-9/_.-]+\.(ts|js|mjs|cjs)$/;
|
|
12
|
+
|
|
13
|
+
const isUrlSpec = (value: string): boolean => {
|
|
14
|
+
if (value.startsWith('url:')) return true;
|
|
15
|
+
return value.includes('://');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const normalizeUrlSpec = (value: string): string => {
|
|
19
|
+
return value.startsWith('url:') ? value.slice(4) : value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const isAllowedRemoteHost = (host: string): boolean => {
|
|
23
|
+
const allowlist = workersConfig.processorSpec.remoteAllowlist;
|
|
24
|
+
return allowlist.map((value) => value.toLowerCase()).includes(host.toLowerCase());
|
|
25
|
+
};
|
|
6
26
|
|
|
7
27
|
const decodeProcessorPath = (processor: string): string => {
|
|
8
28
|
return processor
|
|
@@ -16,6 +36,84 @@ const decodeProcessorPath = (processor: string): string => {
|
|
|
16
36
|
.replaceAll('%2D', '-'); // URL encoding for -
|
|
17
37
|
};
|
|
18
38
|
|
|
39
|
+
const validateUrlSpec = (
|
|
40
|
+
processor: string
|
|
41
|
+
): { isValid: boolean; error?: { error: string; code: string } } => {
|
|
42
|
+
const normalized = normalizeUrlSpec(processor);
|
|
43
|
+
let parsed: URL;
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
parsed = new URL(normalized);
|
|
47
|
+
} catch {
|
|
48
|
+
return {
|
|
49
|
+
isValid: false,
|
|
50
|
+
error: { error: 'Invalid processor url', code: 'INVALID_PROCESSOR_URL' },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (parsed.protocol === 'file:') {
|
|
55
|
+
const path = NodeSingletons.path;
|
|
56
|
+
const baseDir = path.resolve(process.cwd());
|
|
57
|
+
const resolved = path.resolve(baseDir, decodeURIComponent(parsed.pathname));
|
|
58
|
+
|
|
59
|
+
if (!resolved.startsWith(baseDir)) {
|
|
60
|
+
return {
|
|
61
|
+
isValid: false,
|
|
62
|
+
error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_PATH_TRAVERSAL' },
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
if (parsed.protocol !== 'https:') {
|
|
67
|
+
return {
|
|
68
|
+
isValid: false,
|
|
69
|
+
error: { error: 'Invalid processor url', code: 'INVALID_PROCESSOR_URL' },
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!isAllowedRemoteHost(parsed.host)) {
|
|
74
|
+
return {
|
|
75
|
+
isValid: false,
|
|
76
|
+
error: { error: 'Invalid processor url host', code: 'INVALID_PROCESSOR_URL_HOST' },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { isValid: true };
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const validateRelativePath = (
|
|
85
|
+
processor: string
|
|
86
|
+
): { isValid: boolean; error?: { error: string; code: string } } => {
|
|
87
|
+
if (processor.includes('..') || processor.startsWith('/')) {
|
|
88
|
+
return {
|
|
89
|
+
isValid: false,
|
|
90
|
+
error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_PATH' },
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!PROCESSOR_PATH_PATTERN.test(processor)) {
|
|
95
|
+
return {
|
|
96
|
+
isValid: false,
|
|
97
|
+
error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_EXTENSION' },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { isValid: true };
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const sanitizeAndResolvePath = (processor: string): { isValid: boolean; sanitized: string } => {
|
|
105
|
+
const sanitizedProcessor = processor.replaceAll(/[^a-zA-Z0-9/_.-]/g, '');
|
|
106
|
+
const path = NodeSingletons.path;
|
|
107
|
+
const baseDir = path.resolve(process.cwd());
|
|
108
|
+
const resolved = path.resolve(baseDir, sanitizedProcessor);
|
|
109
|
+
|
|
110
|
+
if (!resolved.startsWith(baseDir)) {
|
|
111
|
+
return { isValid: false, sanitized: processor };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { isValid: true, sanitized: sanitizedProcessor };
|
|
115
|
+
};
|
|
116
|
+
|
|
19
117
|
export const withProcessorPathValidation = (handler: RouteHandler): RouteHandler => {
|
|
20
118
|
return async (req: IRequest, res: IResponse): Promise<void> => {
|
|
21
119
|
try {
|
|
@@ -24,8 +122,8 @@ export const withProcessorPathValidation = (handler: RouteHandler): RouteHandler
|
|
|
24
122
|
|
|
25
123
|
if (!processor) {
|
|
26
124
|
return res.setStatus(400).json({
|
|
27
|
-
error: 'Processor
|
|
28
|
-
code: '
|
|
125
|
+
error: 'Processor spec is required',
|
|
126
|
+
code: 'MISSING_PROCESSOR_SPEC',
|
|
29
127
|
});
|
|
30
128
|
}
|
|
31
129
|
|
|
@@ -35,44 +133,33 @@ export const withProcessorPathValidation = (handler: RouteHandler): RouteHandler
|
|
|
35
133
|
// Trim whitespace
|
|
36
134
|
processor = processor.trim();
|
|
37
135
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return res.setStatus(400).json({
|
|
41
|
-
error: 'Invalid processor path',
|
|
42
|
-
message: 'Processor path must be relative and cannot contain path traversal',
|
|
43
|
-
code: 'INVALID_PROCESSOR_PATH',
|
|
44
|
-
});
|
|
45
|
-
}
|
|
136
|
+
const isUrl = isUrlSpec(processor);
|
|
137
|
+
let validation: { isValid: boolean; error?: { error: string; code: string } };
|
|
46
138
|
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
139
|
+
if (isUrl) {
|
|
140
|
+
validation = validateUrlSpec(processor);
|
|
141
|
+
} else {
|
|
142
|
+
validation = validateRelativePath(processor);
|
|
143
|
+
|
|
144
|
+
if (validation.isValid) {
|
|
145
|
+
const pathValidation = sanitizeAndResolvePath(processor);
|
|
146
|
+
if (pathValidation.isValid) {
|
|
147
|
+
processor = pathValidation.sanitized;
|
|
148
|
+
} else {
|
|
149
|
+
validation = {
|
|
150
|
+
isValid: false,
|
|
151
|
+
error: { error: 'Invalid processor path', code: 'INVALID_PROCESSOR_PATH_TRAVERSAL' },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
}
|
|
58
155
|
}
|
|
59
156
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const path = NodeSingletons.path;
|
|
63
|
-
// Ensure resolved path stays within repository/app scope
|
|
64
|
-
const BASE_PROCESSOR_DIR = path.resolve(process.cwd());
|
|
65
|
-
const resolved = path.resolve(BASE_PROCESSOR_DIR, sanitizedProcessor);
|
|
66
|
-
if (!resolved.startsWith(BASE_PROCESSOR_DIR)) {
|
|
67
|
-
return res.setStatus(400).json({
|
|
68
|
-
error: 'Invalid processor path',
|
|
69
|
-
message: 'Processor path resolves outside of allowed base directory',
|
|
70
|
-
code: 'INVALID_PROCESSOR_PATH_TRAVERSAL',
|
|
71
|
-
});
|
|
157
|
+
if (!validation.isValid) {
|
|
158
|
+
return res.setStatus(400).json(validation.error);
|
|
72
159
|
}
|
|
73
160
|
|
|
74
161
|
const currentBody = req.getBody() as Record<string, unknown>;
|
|
75
|
-
req.setBody({ ...currentBody, processor
|
|
162
|
+
req.setBody({ ...currentBody, processor });
|
|
76
163
|
|
|
77
164
|
return handler(req, res);
|
|
78
165
|
} catch (error) {
|
package/src/index.ts
CHANGED
|
@@ -39,9 +39,14 @@ export { WorkerVersioning } from './WorkerVersioning';
|
|
|
39
39
|
|
|
40
40
|
// Factory & Lifecycle
|
|
41
41
|
export { WorkerFactory } from './WorkerFactory';
|
|
42
|
-
export type {
|
|
42
|
+
export type {
|
|
43
|
+
ProcessorResolver,
|
|
44
|
+
WorkerFactoryConfig,
|
|
45
|
+
WorkerPersistenceConfig,
|
|
46
|
+
} from './WorkerFactory';
|
|
43
47
|
export { WorkerInit } from './WorkerInit';
|
|
44
48
|
export { WorkerShutdown } from './WorkerShutdown';
|
|
49
|
+
export { ZinTrustWorkerShutdownDurableObject } from './WorkerShutdownDurableObject';
|
|
45
50
|
|
|
46
51
|
// HTTP Controllers & Routes
|
|
47
52
|
export { WorkerController } from './http/WorkerController';
|
package/src/routes/workers.ts
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* HTTP API for managing workers with dashboard functionality
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { IRouter } from '@zintrust/core';
|
|
6
|
+
import type { IRequest, IResponse, IRouter } from '@zintrust/core';
|
|
7
7
|
import { Logger, Router } from '@zintrust/core';
|
|
8
8
|
import { type WorkersDashboardUiOptions } from '../dashboard';
|
|
9
|
-
import {
|
|
10
|
-
import { WorkerController } from '../http/WorkerController';
|
|
9
|
+
import { HealthMonitor } from '../HealthMonitor';
|
|
11
10
|
import { ValidationSchemas, withCustomValidation } from '../http/middleware/CustomValidation';
|
|
12
11
|
import { withEditWorkerValidation } from '../http/middleware/EditWorkerValidation';
|
|
13
12
|
import { withDriverValidation } from '../http/middleware/ValidateDriver';
|
|
@@ -15,8 +14,12 @@ import {
|
|
|
15
14
|
withCreateWorkerValidation,
|
|
16
15
|
withWorkerOperationValidation,
|
|
17
16
|
} from '../http/middleware/WorkerValidationChain';
|
|
17
|
+
import { WorkerApiController } from '../http/WorkerApiController';
|
|
18
|
+
import { WorkerController } from '../http/WorkerController';
|
|
19
|
+
import { ResourceMonitor } from '../ResourceMonitor';
|
|
20
|
+
import { TelemetryDashboard } from '../telemetry';
|
|
18
21
|
import { registerStaticAssets } from '../ui/router/ui';
|
|
19
|
-
|
|
22
|
+
import { WorkerFactory } from '../WorkerFactory';
|
|
20
23
|
type WorkerUiOptions = WorkersDashboardUiOptions;
|
|
21
24
|
type RouteOptions = { middleware?: ReadonlyArray<string> } | undefined;
|
|
22
25
|
|
|
@@ -28,7 +31,7 @@ function registerCoreWorkerRoutes(r: IRouter): void {
|
|
|
28
31
|
Router.post(r, '/create', withCreateWorkerValidation(controller.create));
|
|
29
32
|
Router.put(r, '/:name', withCreateWorkerValidation(controller.update));
|
|
30
33
|
|
|
31
|
-
// Worker editing with custom validation that handles
|
|
34
|
+
// Worker editing with custom validation that handles mapping
|
|
32
35
|
Router.put(r, '/:name/edit', withEditWorkerValidation(controller.update));
|
|
33
36
|
Router.post(
|
|
34
37
|
r,
|
|
@@ -96,6 +99,9 @@ function registerWorkerQueryRoutes(r: IRouter): void {
|
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
function registerMonitoringRoutes(r: IRouter): void {
|
|
102
|
+
// SSE events stream for monitoring + workers snapshot
|
|
103
|
+
Router.get(r, '/events', controller.eventsStream);
|
|
104
|
+
|
|
99
105
|
// Health monitoring
|
|
100
106
|
Router.post(r, '/:name/monitoring/start', controller.startMonitoring);
|
|
101
107
|
Router.post(r, '/:name/monitoring/stop', controller.stopMonitoring);
|
|
@@ -105,9 +111,6 @@ function registerMonitoringRoutes(r: IRouter): void {
|
|
|
105
111
|
|
|
106
112
|
// SLA monitoring
|
|
107
113
|
Router.get(r, '/:name/sla/status', controller.getSlaStatus);
|
|
108
|
-
|
|
109
|
-
// SSE events stream for monitoring + workers snapshot
|
|
110
|
-
Router.get(r, '/events', controller.eventsStream);
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
function registerVersioningRoutes(r: IRouter): void {
|
|
@@ -131,9 +134,9 @@ function registerWorkerLifecycleRoutes(router: IRouter, middleware?: ReadonlyArr
|
|
|
131
134
|
(r: IRouter) => {
|
|
132
135
|
Logger.info('Registering Worker Management Routes');
|
|
133
136
|
|
|
137
|
+
registerMonitoringRoutes(r); // ← Move FIRST - has /events
|
|
134
138
|
registerCoreWorkerRoutes(r);
|
|
135
139
|
registerWorkerQueryRoutes(r);
|
|
136
|
-
registerMonitoringRoutes(r);
|
|
137
140
|
registerVersioningRoutes(r);
|
|
138
141
|
registerUtilityRoutes(r);
|
|
139
142
|
},
|
|
@@ -141,6 +144,52 @@ function registerWorkerLifecycleRoutes(router: IRouter, middleware?: ReadonlyArr
|
|
|
141
144
|
);
|
|
142
145
|
}
|
|
143
146
|
|
|
147
|
+
function registerWorkerTelemetryRoutes(router: IRouter, middleware?: ReadonlyArray<string>): void {
|
|
148
|
+
const options = middleware ? { middleware } : undefined;
|
|
149
|
+
|
|
150
|
+
Router.group(
|
|
151
|
+
router,
|
|
152
|
+
'/api',
|
|
153
|
+
(r: IRouter) => {
|
|
154
|
+
Router.get(r, '/workers/system/summary', async (_req: IRequest, res: IResponse) => {
|
|
155
|
+
const workers = WorkerFactory.list();
|
|
156
|
+
const monitoringSummary = await HealthMonitor.getSummary();
|
|
157
|
+
const resourceUsage = ResourceMonitor.getCurrentUsage('system');
|
|
158
|
+
|
|
159
|
+
res.json({
|
|
160
|
+
ok: true,
|
|
161
|
+
summary: {
|
|
162
|
+
workers: workers.length,
|
|
163
|
+
monitoring: monitoringSummary,
|
|
164
|
+
resources: resourceUsage,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
Router.get(
|
|
170
|
+
r,
|
|
171
|
+
'/workers/system/monitoring/summary',
|
|
172
|
+
async (_req: IRequest, res: IResponse) => {
|
|
173
|
+
const summary = await HealthMonitor.getSummary();
|
|
174
|
+
res.json({ ok: true, summary });
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
Router.get(r, '/resources/current', async (_req: IRequest, res: IResponse) => {
|
|
179
|
+
const usage = ResourceMonitor.getCurrentUsage('system');
|
|
180
|
+
res.json({ ok: true, usage });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
Router.get(r, '/resources/trends', async (req: IRequest, res: IResponse) => {
|
|
184
|
+
const period = (req.getParam('period') ?? 'day') as 'hour' | 'day' | 'week';
|
|
185
|
+
const trends = ResourceMonitor.getAllTrends('system', period);
|
|
186
|
+
res.json({ ok: true, trends });
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
options
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
144
193
|
export function registerWorkerRoutes(
|
|
145
194
|
router: IRouter,
|
|
146
195
|
_options?: WorkerUiOptions,
|
|
@@ -148,7 +197,15 @@ export function registerWorkerRoutes(
|
|
|
148
197
|
): void {
|
|
149
198
|
registerStaticAssets(router, routeOptions?.middleware ?? []);
|
|
150
199
|
registerWorkerLifecycleRoutes(router, routeOptions?.middleware);
|
|
200
|
+
registerWorkerTelemetryRoutes(router, routeOptions?.middleware);
|
|
201
|
+
|
|
202
|
+
// Register Telemetry Dashboard
|
|
203
|
+
const dashboard = TelemetryDashboard.create({
|
|
204
|
+
basePath: '/telemetry',
|
|
205
|
+
});
|
|
206
|
+
dashboard.registerRoutes(router);
|
|
151
207
|
Logger.info('Worker routes registered at http://127.0.0.1:7777/workers');
|
|
208
|
+
Logger.info('Telemetry dashboard registered at http://127.0.0.1:7777/telemetry');
|
|
152
209
|
}
|
|
153
210
|
|
|
154
211
|
export default registerWorkerRoutes;
|
|
@@ -14,7 +14,8 @@ export type WorkerRecord = {
|
|
|
14
14
|
autoStart: boolean;
|
|
15
15
|
concurrency: number;
|
|
16
16
|
region: string | null;
|
|
17
|
-
|
|
17
|
+
processorSpec?: string | null;
|
|
18
|
+
activeStatus?: boolean;
|
|
18
19
|
features?: Record<string, unknown> | null;
|
|
19
20
|
infrastructure?: Record<string, unknown> | null;
|
|
20
21
|
datacenter?: Record<string, unknown> | null;
|
|
@@ -27,10 +28,16 @@ export type WorkerRecord = {
|
|
|
27
28
|
|
|
28
29
|
export type WorkerStore = {
|
|
29
30
|
init(): Promise<void>;
|
|
30
|
-
list(options?: {
|
|
31
|
+
list(options?: {
|
|
32
|
+
offset?: number;
|
|
33
|
+
limit?: number;
|
|
34
|
+
search?: string;
|
|
35
|
+
includeInactive?: boolean;
|
|
36
|
+
}): Promise<WorkerRecord[]>;
|
|
31
37
|
get(name: string): Promise<WorkerRecord | null>;
|
|
32
38
|
save(record: WorkerRecord): Promise<void>;
|
|
33
39
|
update(name: string, patch: Partial<WorkerRecord>): Promise<void>;
|
|
40
|
+
updateMany?: (names: string[], patch: Partial<WorkerRecord>) => Promise<void>;
|
|
34
41
|
remove(name: string): Promise<void>;
|
|
35
42
|
};
|
|
36
43
|
|
|
@@ -42,6 +49,12 @@ const mergeRecord = (current: WorkerRecord, patch: Partial<WorkerRecord>): Worke
|
|
|
42
49
|
updatedAt: patch.updatedAt ?? now(),
|
|
43
50
|
});
|
|
44
51
|
|
|
52
|
+
const toSqlDateTime = (value: Date | undefined | null): string | null => {
|
|
53
|
+
if (!value) return null;
|
|
54
|
+
// Use UTC and drop timezone for SQL DATETIME compatibility
|
|
55
|
+
return value.toISOString().slice(0, 19).replace('T', ' ');
|
|
56
|
+
};
|
|
57
|
+
|
|
45
58
|
const serializeDbWorker = (record: WorkerRecord): Record<string, unknown> => ({
|
|
46
59
|
name: record.name,
|
|
47
60
|
queue_name: record.queueName,
|
|
@@ -50,13 +63,14 @@ const serializeDbWorker = (record: WorkerRecord): Record<string, unknown> => ({
|
|
|
50
63
|
auto_start: record.autoStart,
|
|
51
64
|
concurrency: record.concurrency,
|
|
52
65
|
region: record.region,
|
|
53
|
-
|
|
66
|
+
processor_spec: record.processorSpec ?? null,
|
|
67
|
+
active_status: record.activeStatus ?? true,
|
|
54
68
|
features: record.features ? JSON.stringify(record.features) : null,
|
|
55
69
|
infrastructure: record.infrastructure ? JSON.stringify(record.infrastructure) : null,
|
|
56
70
|
datacenter: record.datacenter ? JSON.stringify(record.datacenter) : null,
|
|
57
|
-
created_at: record.createdAt,
|
|
58
|
-
updated_at: record.updatedAt,
|
|
59
|
-
last_health_check: record.lastHealthCheck ?? null,
|
|
71
|
+
created_at: toSqlDateTime(record.createdAt),
|
|
72
|
+
updated_at: toSqlDateTime(record.updatedAt),
|
|
73
|
+
last_health_check: toSqlDateTime(record.lastHealthCheck ?? null),
|
|
60
74
|
last_error: record.lastError ?? null,
|
|
61
75
|
connection_state: record.connectionState ?? null,
|
|
62
76
|
});
|
|
@@ -88,7 +102,8 @@ const deserializeDbWorker = (row: Record<string, unknown>): WorkerRecord => {
|
|
|
88
102
|
autoStart: Boolean(row['auto_start'] ?? false),
|
|
89
103
|
concurrency: Number(row['concurrency'] ?? 0),
|
|
90
104
|
region: row['region'] ? String(row['region']) : null,
|
|
91
|
-
|
|
105
|
+
processorSpec: String(row['processor_spec']),
|
|
106
|
+
activeStatus: row['active_status'] === undefined ? true : Boolean(row['active_status']),
|
|
92
107
|
features: parseJson(row['features']),
|
|
93
108
|
infrastructure: parseJson(row['infrastructure']),
|
|
94
109
|
datacenter: parseJson(row['datacenter']),
|
|
@@ -132,6 +147,13 @@ export const InMemoryWorkerStore = Object.freeze({
|
|
|
132
147
|
if (!current) return;
|
|
133
148
|
store.set(name, mergeRecord(current, patch));
|
|
134
149
|
},
|
|
150
|
+
async updateMany(names: string[], patch: Partial<WorkerRecord>): Promise<void> {
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const current = store.get(name);
|
|
153
|
+
if (!current) continue;
|
|
154
|
+
store.set(name, mergeRecord(current, patch));
|
|
155
|
+
}
|
|
156
|
+
},
|
|
135
157
|
async remove(name: string): Promise<void> {
|
|
136
158
|
store.delete(name);
|
|
137
159
|
},
|
|
@@ -168,7 +190,7 @@ export const RedisWorkerStore = Object.freeze({
|
|
|
168
190
|
},
|
|
169
191
|
async list(options?: { offset?: number; limit?: number }): Promise<WorkerRecord[]> {
|
|
170
192
|
const all = await client.hgetall(key);
|
|
171
|
-
let values = Object.values(all).map(deserialize);
|
|
193
|
+
let values = Object.values(all).map((element) => deserialize(element));
|
|
172
194
|
values.sort((a, b) => a.name.localeCompare(b.name));
|
|
173
195
|
if (options) {
|
|
174
196
|
const start = options.offset || 0;
|
|
@@ -189,6 +211,19 @@ export const RedisWorkerStore = Object.freeze({
|
|
|
189
211
|
if (!current) return;
|
|
190
212
|
await client.hset(key, name, serialize(mergeRecord(current, patch)));
|
|
191
213
|
},
|
|
214
|
+
async updateMany(names: string[], patch: Partial<WorkerRecord>): Promise<void> {
|
|
215
|
+
if (names.length === 0) return;
|
|
216
|
+
const entries = await client.hmget(key, ...names);
|
|
217
|
+
const updates: Array<string> = [];
|
|
218
|
+
entries.forEach((raw, index) => {
|
|
219
|
+
if (!raw) return;
|
|
220
|
+
const current = deserialize(raw);
|
|
221
|
+
const updated = mergeRecord(current, patch);
|
|
222
|
+
updates.push(names[index] as string, serialize(updated));
|
|
223
|
+
});
|
|
224
|
+
if (updates.length === 0) return;
|
|
225
|
+
await client.hset(key, ...updates);
|
|
226
|
+
},
|
|
192
227
|
async remove(name: string): Promise<void> {
|
|
193
228
|
await client.hdel(key, name);
|
|
194
229
|
},
|
|
@@ -207,7 +242,7 @@ export const DbWorkerStore = Object.freeze({
|
|
|
207
242
|
if (options?.limit) query.limit(options.limit);
|
|
208
243
|
if (options?.offset) query.offset(options.offset);
|
|
209
244
|
const rows = await query.get<Record<string, unknown>>();
|
|
210
|
-
return rows.map(deserializeDbWorker);
|
|
245
|
+
return rows.map((element) => deserializeDbWorker(element));
|
|
211
246
|
},
|
|
212
247
|
async get(name: string): Promise<WorkerRecord | null> {
|
|
213
248
|
const row = await db.table(table).where('name', '=', name).first<Record<string, unknown>>();
|
|
@@ -232,6 +267,21 @@ export const DbWorkerStore = Object.freeze({
|
|
|
232
267
|
const updated = mergeRecord(current, patch);
|
|
233
268
|
await db.table(table).where('name', '=', name).update(serializeDbWorker(updated));
|
|
234
269
|
},
|
|
270
|
+
async updateMany(names: string[], patch: Partial<WorkerRecord>): Promise<void> {
|
|
271
|
+
if (names.length === 0) return;
|
|
272
|
+
const update: Record<string, unknown> = {
|
|
273
|
+
updated_at: toSqlDateTime(patch.updatedAt ?? now()),
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
if (patch.status !== undefined) update['status'] = patch.status;
|
|
277
|
+
if (patch.lastError !== undefined) update['last_error'] = patch.lastError ?? null;
|
|
278
|
+
if (patch.lastHealthCheck !== undefined)
|
|
279
|
+
update['last_health_check'] = toSqlDateTime(patch.lastHealthCheck ?? null);
|
|
280
|
+
if (patch.connectionState !== undefined)
|
|
281
|
+
update['connection_state'] = patch.connectionState ?? null;
|
|
282
|
+
|
|
283
|
+
await db.table(table).whereIn('name', names).update(update);
|
|
284
|
+
},
|
|
235
285
|
async remove(name: string): Promise<void> {
|
|
236
286
|
await db.table(table).where('name', '=', name).delete();
|
|
237
287
|
},
|