@camstack/server 1.1.24 → 1.1.25
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.
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Health endpoints — fast probe surface for monitoring (k8s, uptime,
|
|
4
|
+
* external watchdogs).
|
|
5
|
+
*
|
|
6
|
+
* Routes:
|
|
7
|
+
* - GET /health → hub self-health (in-memory registry ONLY, no RPC)
|
|
8
|
+
* - GET /health/agents → online agent node IDs (in-memory registry ONLY, no RPC)
|
|
9
|
+
* - GET /health/agents/:nodeId → forward to agent's `$agent.health`
|
|
10
|
+
* - GET /health/cluster → hub + every online agent in one shot
|
|
11
|
+
*
|
|
12
|
+
* `/health` and `/health/agents` are served purely from the hub's in-memory
|
|
13
|
+
* Moleculer node registry (`listNodeLiveness`) and NEVER fan out to agent
|
|
14
|
+
* nodes — a frozen/ghost agent cannot slow them. This is what makes `/health`
|
|
15
|
+
* safe as the admin-ui's 4s hub-liveness probe (#26): the old path went
|
|
16
|
+
* through `listNodes()` which RPCs `$agent.status`/`$process.list` at 5s/node.
|
|
17
|
+
* `/health/agents/:nodeId` and `/health/cluster` are the (intentional)
|
|
18
|
+
* forwarding surfaces backed by the `$agent.health` Moleculer action, so
|
|
19
|
+
* monitors talking to the hub and monitors talking directly to an agent
|
|
20
|
+
* (`http://<agent>:4444/health`) see identical payloads.
|
|
21
|
+
*/
|
|
2
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
23
|
exports.buildHubHealth = buildHubHealth;
|
|
4
24
|
exports.registerHealthRoutes = registerHealthRoutes;
|
|
@@ -7,9 +27,10 @@ function nowIso() {
|
|
|
7
27
|
return new Date().toISOString();
|
|
8
28
|
}
|
|
9
29
|
async function buildHubHealth(deps, proc = process) {
|
|
10
|
-
|
|
30
|
+
// Fan-out-free: in-memory registry snapshot only, never a per-node RPC (#26).
|
|
31
|
+
const nodes = deps.agentRegistry.listNodeLiveness();
|
|
11
32
|
const remote = nodes.filter((n) => !n.isHub);
|
|
12
|
-
const offlineNodes = remote.filter((n) => n.isOnline
|
|
33
|
+
const offlineNodes = remote.filter((n) => !n.isOnline);
|
|
13
34
|
const total = remote.length;
|
|
14
35
|
const online = total - offlineNodes.length;
|
|
15
36
|
const memUsage = proc.memoryUsage();
|
|
@@ -27,7 +48,7 @@ async function buildHubHealth(deps, proc = process) {
|
|
|
27
48
|
offline: offlineNodes.length,
|
|
28
49
|
// Named so health consumers (admin-ui connection banner) can say
|
|
29
50
|
// WHICH node is degraded, not only how many.
|
|
30
|
-
offlineIds: offlineNodes.map((n) => n.
|
|
51
|
+
offlineIds: offlineNodes.map((n) => n.id),
|
|
31
52
|
},
|
|
32
53
|
cpuPercent: 0,
|
|
33
54
|
memoryPercent,
|
|
@@ -55,10 +76,8 @@ function registerHealthRoutes(fastify, deps) {
|
|
|
55
76
|
return health;
|
|
56
77
|
});
|
|
57
78
|
fastify.get('/health/agents', async () => {
|
|
58
|
-
const nodes =
|
|
59
|
-
return {
|
|
60
|
-
agents: nodes.filter((n) => !n.isHub && n.isOnline !== false).map((n) => n.info.id),
|
|
61
|
-
};
|
|
79
|
+
const nodes = deps.agentRegistry.listNodeLiveness();
|
|
80
|
+
return { agents: nodes.filter((n) => !n.isHub && n.isOnline).map((n) => n.id) };
|
|
62
81
|
});
|
|
63
82
|
fastify.get('/health/agents/:nodeId', async (req, reply) => {
|
|
64
83
|
const { nodeId } = req.params;
|
|
@@ -73,9 +92,10 @@ function registerHealthRoutes(fastify, deps) {
|
|
|
73
92
|
});
|
|
74
93
|
fastify.get('/health/cluster', async () => {
|
|
75
94
|
const hub = await buildHubHealth(deps);
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
95
|
+
// Enumeration only (fan-out-free); the $agent.health fan-out below is intentional.
|
|
96
|
+
const nodes = deps.agentRegistry.listNodeLiveness();
|
|
97
|
+
const remote = nodes.filter((n) => !n.isHub && n.isOnline);
|
|
98
|
+
const agents = await Promise.all(remote.map((n) => fetchAgentHealth(deps, n.id)));
|
|
79
99
|
const ok = hub.ok && agents.every((a) => a.ok);
|
|
80
100
|
return { ok, hub, agents, checkedAt: nowIso() };
|
|
81
101
|
});
|
|
@@ -2337,6 +2337,33 @@ function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
|
|
|
2337
2337
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2338
2338
|
return p.setDeviceLinks(methodInput);
|
|
2339
2339
|
}),
|
|
2340
|
+
setDisplay: trpc_middleware_js_1.adminProcedure
|
|
2341
|
+
.input(types_38.deviceManagerCapability.methods.setDisplay.input.loose())
|
|
2342
|
+
.output(types_38.deviceManagerCapability.methods.setDisplay.output)
|
|
2343
|
+
.mutation(async ({ input, ctx }) => {
|
|
2344
|
+
const { nodeId, ...methodInput } = input;
|
|
2345
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2346
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2347
|
+
return p.setDisplay(methodInput);
|
|
2348
|
+
}),
|
|
2349
|
+
getRoleDisplayDefaults: trpc_middleware_js_1.protectedProcedure
|
|
2350
|
+
.input(types_38.deviceManagerCapability.methods.getRoleDisplayDefaults.input.loose())
|
|
2351
|
+
.output(types_38.deviceManagerCapability.methods.getRoleDisplayDefaults.output)
|
|
2352
|
+
.query(async ({ input, ctx }) => {
|
|
2353
|
+
const { nodeId, ...methodInput } = input;
|
|
2354
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2355
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2356
|
+
return p.getRoleDisplayDefaults(methodInput);
|
|
2357
|
+
}),
|
|
2358
|
+
setRoleDisplayDefaults: trpc_middleware_js_1.adminProcedure
|
|
2359
|
+
.input(types_38.deviceManagerCapability.methods.setRoleDisplayDefaults.input.loose())
|
|
2360
|
+
.output(types_38.deviceManagerCapability.methods.setRoleDisplayDefaults.output)
|
|
2361
|
+
.mutation(async ({ input, ctx }) => {
|
|
2362
|
+
const { nodeId, ...methodInput } = input;
|
|
2363
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2364
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2365
|
+
return p.setRoleDisplayDefaults(methodInput);
|
|
2366
|
+
}),
|
|
2340
2367
|
getWireableFields: trpc_middleware_js_1.protectedProcedure
|
|
2341
2368
|
.input(types_38.deviceManagerCapability.methods.getWireableFields.input.loose())
|
|
2342
2369
|
.output(types_38.deviceManagerCapability.methods.getWireableFields.output)
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AgentRegistryService = void 0;
|
|
37
|
+
exports.toNodeLiveness = toNodeLiveness;
|
|
37
38
|
const node_crypto_1 = require("node:crypto");
|
|
38
39
|
const os = __importStar(require("node:os"));
|
|
39
40
|
const system_1 = require("@camstack/system");
|
|
@@ -70,6 +71,16 @@ const AGENT_BOOTSTRAP_PACKAGES = new Set([
|
|
|
70
71
|
'@camstack/system',
|
|
71
72
|
'@camstack/addon-agent-ui',
|
|
72
73
|
]);
|
|
74
|
+
/**
|
|
75
|
+
* Map raw Moleculer registry nodes to the health-surface liveness rows.
|
|
76
|
+
* Child runner ids (`hub/foo`, `agent/bar`) are processes, not cluster
|
|
77
|
+
* nodes — excluded, same rule as `listNodes()` / `classifyNode`.
|
|
78
|
+
*/
|
|
79
|
+
function toNodeLiveness(nodes) {
|
|
80
|
+
return nodes
|
|
81
|
+
.filter((node) => !node.id.includes('/'))
|
|
82
|
+
.map((node) => ({ id: node.id, isHub: node.id === 'hub', isOnline: node.available }));
|
|
83
|
+
}
|
|
73
84
|
class AgentRegistryService {
|
|
74
85
|
eventBus;
|
|
75
86
|
moleculer;
|
|
@@ -339,6 +350,19 @@ class AgentRegistryService {
|
|
|
339
350
|
updateAgentName(nodeId, name) {
|
|
340
351
|
console.log(`[agent-registry] Agent renamed: "${nodeId}" → "${name}"`);
|
|
341
352
|
}
|
|
353
|
+
/**
|
|
354
|
+
* Fan-out-free liveness snapshot for the `/health` surface. Reads ONLY
|
|
355
|
+
* the hub's in-memory Moleculer node registry — never a per-node RPC —
|
|
356
|
+
* so a frozen/ghost agent can never slow the hub-liveness probe.
|
|
357
|
+
* (`listNodes()` fans `$agent.status`/`$process.list` out at 5s/node;
|
|
358
|
+
* the admin-ui probe aborts `/health` at 4s, which turned one frozen
|
|
359
|
+
* node into a false RED "Server unreachable" banner — #26.)
|
|
360
|
+
* `onlyAvailable: false` keeps recently-offline nodes listed with
|
|
361
|
+
* `available:false`, which is what populates `agents.offlineIds`.
|
|
362
|
+
*/
|
|
363
|
+
listNodeLiveness() {
|
|
364
|
+
return toNodeLiveness(this.moleculer.broker.registry.getNodeList({ onlyAvailable: false }));
|
|
365
|
+
}
|
|
342
366
|
async listNodes() {
|
|
343
367
|
// Get child processes for hub via $process.list
|
|
344
368
|
let hubProcesses = [];
|