@lensmcp/cluster 1.16.19 → 1.16.20
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/executors/gateway/otel-tracing.d.ts.map +1 -1
- package/executors/gateway/otel-tracing.js +3 -2
- package/executors/gateway/runtime/control.d.ts.map +1 -1
- package/executors/gateway/runtime/control.js +13 -0
- package/executors/gateway/runtime/lifecycle.d.ts.map +1 -1
- package/executors/gateway/runtime/lifecycle.js +20 -12
- package/executors/gateway/runtime/observability.d.ts +4 -1
- package/executors/gateway/runtime/observability.d.ts.map +1 -1
- package/executors/gateway/runtime/observability.js +4 -3
- package/executors/trust/trust.impl.d.ts.map +1 -1
- package/executors/trust/trust.impl.js +20 -7
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel-tracing.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/otel-tracing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAiD,KAAK,IAAI,EAA0C,MAAM,oBAAoB,CAAC;AAKtI,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,+EAA+E;AAC/E,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC;AAE7B,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC;IAC3G,sFAAsF;IACtF,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC;IAClG,2DAA2D;IAC3D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAcD;2EAC2E;AAC3E,wBAAgB,YAAY,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"otel-tracing.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/otel-tracing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAiD,KAAK,IAAI,EAA0C,MAAM,oBAAoB,CAAC;AAKtI,OAAO,KAAK,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEtE,+EAA+E;AAC/E,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC;AAE7B,MAAM,WAAW,aAAa;IAC5B,oFAAoF;IACpF,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,SAAS,CAAC;IAC3G,sFAAsF;IACtF,MAAM,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAAC;IAC5D,8EAA8E;IAC9E,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC;IAClG,2DAA2D;IAC3D,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,cAAc;IAC7B,wGAAwG;IACxG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0FAA0F;IAC1F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAcD;2EAC2E;AAC3E,wBAAgB,YAAY,CAAC,IAAI,GAAE,cAAmB,GAAG,aAAa,GAAG,SAAS,CAgCjF"}
|
|
@@ -46,10 +46,11 @@ function setupTracing(opts = {}) {
|
|
|
46
46
|
const url = /\/v1\/traces$/.test(base) ? base : `${base}/v1/traces`;
|
|
47
47
|
const exporter = new exporter_trace_otlp_http_1.OTLPTraceExporter({ url, ...(opts.headers ? { headers: opts.headers } : {}) });
|
|
48
48
|
const provider = new sdk_trace_base_1.BasicTracerProvider({
|
|
49
|
-
resource:
|
|
49
|
+
resource: (0, resources_1.resourceFromAttributes)({ 'service.name': opts.serviceName ?? process.env['OTEL_SERVICE_NAME'] ?? 'lensmcp-gateway' }),
|
|
50
50
|
sampler: new sdk_trace_base_1.ParentBasedSampler({ root: new sdk_trace_base_1.TraceIdRatioBasedSampler(opts.sampleRatio ?? 1) }),
|
|
51
|
+
// OTel 2.0: span processors are constructor config (the mutating `addSpanProcessor` was removed).
|
|
52
|
+
spanProcessors: [new sdk_trace_base_1.BatchSpanProcessor(exporter)],
|
|
51
53
|
});
|
|
52
|
-
provider.addSpanProcessor(new sdk_trace_base_1.BatchSpanProcessor(exporter));
|
|
53
54
|
const tracer = provider.getTracer('lensmcp-gateway');
|
|
54
55
|
const propagator = new core_1.W3CTraceContextPropagator();
|
|
55
56
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/control.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,+GAA+G;AAC/G,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,cAAc,CAAC;IACnB,+FAA+F;IAC/F,WAAW,EAAE,CAAC,GAAG,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClF,mGAAmG;IACnG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAsBD;qFACqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"control.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/control.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,+GAA+G;AAC/G,wBAAgB,iBAAiB,IAAI,MAAM,CAG1C;AAED,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,cAAc,CAAC;IACnB,+FAA+F;IAC/F,WAAW,EAAE,CAAC,GAAG,EAAE,iBAAiB,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClF,mGAAmG;IACnG,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACnD,YAAY,CAAC,EAAE,CAAC,QAAQ,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACtD;AAED,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAsBD;qFACqF;AACrF,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CAwEzE"}
|
|
@@ -70,6 +70,19 @@ function startControlServer(deps) {
|
|
|
70
70
|
}),
|
|
71
71
|
});
|
|
72
72
|
}
|
|
73
|
+
// A RICHER status than /list: the daemon's own identity + every registered workspace (flagged if it
|
|
74
|
+
// OWNS the daemon) + per-service live state (down|starting|up). The IDE plugin + `gateway status --json`
|
|
75
|
+
// consume this to render the active gateway, its workspaces, and each service's health at a glance.
|
|
76
|
+
if (req.method === 'GET' && url === '/status') {
|
|
77
|
+
return send(res, 200, {
|
|
78
|
+
daemon: { pid: process.pid, wsKey: rt.wsKey },
|
|
79
|
+
workspaces: rt.registry.keys().map((key) => {
|
|
80
|
+
const f = rt.registry.get(key);
|
|
81
|
+
return { key, root: f?.root, routes: f?.routes.length ?? 0, services: f?.services.length ?? 0, isDaemon: key === rt.wsKey };
|
|
82
|
+
}),
|
|
83
|
+
services: rt.registry.allServices().map((s) => ({ project: s.project, wsKey: s.wsKey, host: s.decl.host, state: s.state })),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
73
86
|
if (req.method === 'POST' && url === '/register') {
|
|
74
87
|
const body = (await readJsonBody(req));
|
|
75
88
|
if (!body.wsKey || !body.root)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAapE,OAAO,KAAK,EAAE,aAAa,EAAY,MAAM,iBAAiB,CAAC;AA4C/D;;sGAEsG;AACtG,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAuBtE;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,oBAAoB,CAAC,EACpG,GAAG,EAAE,MAAM,EACX,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,gBAAgB,GACpB,kBAAkB,GAAG,SAAS,GAAG,IAAI,CAQvC;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,MAAM,EACjB,aAAa,EAAE,MAAM,EACrB,kBAAkB,EAAE,MAAM,EAC1B,GAAG,EAAE,MAAM,EACX,GAAG,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAC7D,OAAO,CAMT;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAkRvF"}
|
|
@@ -140,6 +140,14 @@ function lensFrontendStale(spawnedAt, lastRecycleAt, sourceSetChangedAt, now, cf
|
|
|
140
140
|
}
|
|
141
141
|
function createServiceLayer(rt, obs) {
|
|
142
142
|
const { emit } = obs;
|
|
143
|
+
// Route a service's lifecycle events (starting/up/down/cold-start/…) to ITS OWNING workspace's bus —
|
|
144
|
+
// mirrors the pod runtime-event routing below (svc.wsKey === rt.wsKey ? rt.eventFile : <root>/.lensmcp).
|
|
145
|
+
// The daemon's own services stay on rt.eventFile; a REGISTERED workspace's go to its own bus, so each
|
|
146
|
+
// hosted dashboard's cluster view (/<key>/api/cluster) is scoped to ITS workspace, not the daemon's
|
|
147
|
+
// (foodguard/cluster was empty because every service event went to the daemon's tetros bus). Two
|
|
148
|
+
// workspaces can share a service NAME (both have `auth`), so this MUST key off svc.wsKey, not the project.
|
|
149
|
+
const serviceBus = (svc) => svc.wsKey === rt.wsKey ? undefined : path.join(svc.root, '.lensmcp', 'events.jsonl');
|
|
150
|
+
const emitSvc = (svc, severity, title, fingerprint, raw) => emit(severity, title, fingerprint, { wsKey: svc.wsKey, ...raw }, undefined, serviceBus(svc));
|
|
143
151
|
const scanNow = (pool) => { pool.scannedAt = 0; return (0, discovery_1.pickSock)(pool, rt.scanTtlMs) ? pool.socks.length : 0; };
|
|
144
152
|
const spawnService = (svc) => {
|
|
145
153
|
if (rt.stopped())
|
|
@@ -147,7 +155,7 @@ function createServiceLayer(rt, obs) {
|
|
|
147
155
|
const target = `${svc.project}:${svc.decl.serveTarget ?? 'serve-hmr'}`;
|
|
148
156
|
const min = Math.max(1, svc.decl.minPods ?? 1);
|
|
149
157
|
console.log(`[gateway] starting ${target} (pods=${min})…`);
|
|
150
|
-
|
|
158
|
+
emitSvc(svc, 'info', `service starting: ${svc.project} (${min} pod${min > 1 ? 's' : ''})`, `cluster-service:${svc.project}`, { kind: 'service-starting', project: svc.project, pods: min });
|
|
151
159
|
svc.state = 'starting';
|
|
152
160
|
svc.spawnAt = Date.now();
|
|
153
161
|
// childCount goes as a CLI option, NOT env: nx executes tasks via its
|
|
@@ -197,7 +205,7 @@ function createServiceLayer(rt, obs) {
|
|
|
197
205
|
if (clean !== svc.lastErrorEmitSig || now - (svc.lastErrorEmitAt ?? 0) >= types_1.SERVICE_ERROR_THROTTLE_MS) {
|
|
198
206
|
svc.lastErrorEmitSig = clean;
|
|
199
207
|
svc.lastErrorEmitAt = now;
|
|
200
|
-
|
|
208
|
+
emitSvc(svc, 'error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: clean });
|
|
201
209
|
}
|
|
202
210
|
}
|
|
203
211
|
if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
|
|
@@ -214,7 +222,7 @@ function createServiceLayer(rt, obs) {
|
|
|
214
222
|
if (rt.stopped())
|
|
215
223
|
return; // gateway is tearing down — its dying services' exit chatter is noise
|
|
216
224
|
console.log(`[gateway] ${target} exited (${code})`);
|
|
217
|
-
|
|
225
|
+
emitSvc(svc, code === 0 ? 'info' : 'warning', `service down: ${svc.project} (exit ${code})`, `cluster-service:${svc.project}`, { kind: 'service-down', project: svc.project, code });
|
|
218
226
|
// SELF-HEAL: respawn an UNEXPECTEDLY-crashed pod so a transient failure recovers with no manual
|
|
219
227
|
// restart. A clean exit (code 0) or a sniffed BUILD failure (a code error → a fix, not a transient
|
|
220
228
|
// crash) is left down — scale-from-zero (ensureUp) still spawns it on the next request. A crash-loop
|
|
@@ -225,7 +233,7 @@ function createServiceLayer(rt, obs) {
|
|
|
225
233
|
const now = Date.now();
|
|
226
234
|
svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
|
|
227
235
|
if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
|
|
228
|
-
|
|
236
|
+
emitSvc(svc, 'error', `service crash-looping: ${svc.project} — ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
239
|
svc.restarts.push(now);
|
|
@@ -234,7 +242,7 @@ function createServiceLayer(rt, obs) {
|
|
|
234
242
|
if (rt.stopped() || svc.proc || svc.state !== 'down')
|
|
235
243
|
return; // a request already respawned it, or teardown
|
|
236
244
|
console.log(`[gateway] self-heal: respawning ${svc.project} (restart ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
|
|
237
|
-
|
|
245
|
+
emitSvc(svc, 'info', `self-heal: respawning ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-respawn', project: svc.project, attempt: svc.restarts.length });
|
|
238
246
|
spawnService(svc);
|
|
239
247
|
}, backoffMs);
|
|
240
248
|
});
|
|
@@ -243,7 +251,7 @@ function createServiceLayer(rt, obs) {
|
|
|
243
251
|
const ensureUp = async (svc) => {
|
|
244
252
|
if (scanNow(svc.pool) > 0) {
|
|
245
253
|
if (svc.state !== 'up') {
|
|
246
|
-
|
|
254
|
+
emitSvc(svc, 'info', `service up: ${svc.project} (${svc.pool.socks.length} pods, adopted)`, `cluster-service:${svc.project}`, { kind: 'service-up', project: svc.project, pods: svc.pool.socks.length, adopted: true });
|
|
247
255
|
}
|
|
248
256
|
svc.state = 'up';
|
|
249
257
|
return true;
|
|
@@ -263,7 +271,7 @@ function createServiceLayer(rt, obs) {
|
|
|
263
271
|
svc.buildFailed = false;
|
|
264
272
|
const durationMs = svc.spawnAt ? Date.now() - svc.spawnAt : undefined;
|
|
265
273
|
console.log(`[gateway] ${svc.project} up (${svc.pool.socks.length} pods${durationMs ? `, cold start ${durationMs}ms` : ''})`);
|
|
266
|
-
|
|
274
|
+
emitSvc(svc, 'info', `cold start: ${svc.project} (${svc.pool.socks.length} pods, ${durationMs ?? '?'}ms)`, `cluster-service:${svc.project}`, { kind: 'cold-start', project: svc.project, pods: svc.pool.socks.length, durationMs });
|
|
267
275
|
return true;
|
|
268
276
|
}
|
|
269
277
|
if (svc.state === 'down')
|
|
@@ -288,14 +296,14 @@ function createServiceLayer(rt, obs) {
|
|
|
288
296
|
r.on('error', () => undefined);
|
|
289
297
|
r.end(JSON.stringify({ pods: cur + 1 }));
|
|
290
298
|
console.log(`[gateway] autoscale ${svc.project}: ${cur} → ${cur + 1} pods (inflight ${svc.inflight})`);
|
|
291
|
-
|
|
299
|
+
emitSvc(svc, 'info', `autoscale: ${svc.project} ${cur} → ${cur + 1} pods`, `cluster-service:${svc.project}`, { kind: 'autoscale', project: svc.project, from: cur, to: cur + 1, inflight: svc.inflight });
|
|
292
300
|
};
|
|
293
301
|
// Idle sweeper: kill gateway-spawned lazy services after idleKillSec.
|
|
294
302
|
const killSpawned = (svc, why) => {
|
|
295
303
|
if (!svc.proc)
|
|
296
304
|
return;
|
|
297
305
|
console.log(`[gateway] ${svc.project} ${why} — scaling to zero.`);
|
|
298
|
-
|
|
306
|
+
emitSvc(svc, 'info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
|
|
299
307
|
const dying = svc.proc;
|
|
300
308
|
// Detach the corpse's handlers BEFORE killing: a dying webpack/nx flushes
|
|
301
309
|
// buffered stdout/stderr as it exits, and those `sniff` callbacks would
|
|
@@ -341,13 +349,13 @@ function createServiceLayer(rt, obs) {
|
|
|
341
349
|
const now = Date.now();
|
|
342
350
|
svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
|
|
343
351
|
if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
|
|
344
|
-
|
|
352
|
+
emitSvc(svc, 'error', `service crash-looping: ${svc.project} — wedged ${types_1.SERVICE_MAX_RESTARTS}+×/min, not auto-recycling (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
|
|
345
353
|
killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request'); // svc.proc cleared → scale-from-zero can retry
|
|
346
354
|
return;
|
|
347
355
|
}
|
|
348
356
|
svc.restarts.push(now);
|
|
349
357
|
console.log(`[gateway] self-heal: recycling WEDGED ${svc.project} (live parent, 0 live pods; ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
|
|
350
|
-
|
|
358
|
+
emitSvc(svc, 'warning', `self-heal: recycling wedged ${svc.project} (0 live pods)`, `cluster-service:${svc.project}`, { kind: 'service-recycle', project: svc.project, attempt: svc.restarts.length });
|
|
351
359
|
killSpawned(svc, 'wedged: 0 live pods — recycling'); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
|
|
352
360
|
// A wedge is unexpected — bring it straight back (don't wait for a request). Respawn AFTER killSpawned's
|
|
353
361
|
// deferred sock cleanup (1200ms) so the fresh child binds a clean socket path.
|
|
@@ -388,7 +396,7 @@ function createServiceLayer(rt, obs) {
|
|
|
388
396
|
const recyclePod = (svc, reason) => {
|
|
389
397
|
svc.lastStaleRecycleAt = Date.now();
|
|
390
398
|
console.log(`[gateway] zombie recycle: ${svc.project} (${reason}) — stale fs view; fresh pod incoming`);
|
|
391
|
-
|
|
399
|
+
emitSvc(svc, 'warning', `zombie recycle: ${svc.project} (${reason})`, `cluster-service:${svc.project}`, { kind: 'pod-recycle', project: svc.project, reason });
|
|
392
400
|
const eager = !!svc.decl.eager;
|
|
393
401
|
killSpawned(svc, `${reason} — recycling stale pod`); // clears proc, state='down', unlinks socks (+1200ms)
|
|
394
402
|
if (eager)
|
|
@@ -11,7 +11,10 @@ export type Severity = 'info' | 'warning' | 'error';
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const LENS_DASHBOARD_PROJECT = "(lens-dashboard)";
|
|
13
13
|
export interface Observability {
|
|
14
|
-
|
|
14
|
+
/** Append a cluster event to the bus. `busFile` overrides the default daemon bus for THIS event — used to
|
|
15
|
+
* route a REGISTERED workspace's service-lifecycle events to ITS OWN bus, so each hosted dashboard's
|
|
16
|
+
* cluster view is scoped to its own workspace (not the daemon's). Omit for the daemon's own events. */
|
|
17
|
+
emit(severity: Severity, title: string, fingerprint: string, raw?: Record<string, unknown>, flowCtx?: Record<string, unknown>, busFile?: string): void;
|
|
15
18
|
eid(): string;
|
|
16
19
|
beginTrace(req: http.IncomingMessage): GwTrace | undefined;
|
|
17
20
|
traceStep(t: GwTrace | undefined, stepName: string, info?: Record<string, unknown>): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B;;4GAEwG;IACxG,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvJ,GAAG,IAAI,MAAM,CAAC;IACd,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3D,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1F,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5G,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,0FAA0F;IAC1F,gBAAgB,IAAI,MAAM,CAAC,OAAO,CAAC;CACpC;AAKD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAoGtG"}
|
|
@@ -29,10 +29,11 @@ exports.LENS_DASHBOARD_PROJECT = '(lens-dashboard)';
|
|
|
29
29
|
const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
|
|
30
30
|
function createObservability(eventFile, opts) {
|
|
31
31
|
const eid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
|
|
32
|
-
const emit = (severity, title, fingerprint, raw, flowCtx) => {
|
|
32
|
+
const emit = (severity, title, fingerprint, raw, flowCtx, busFile) => {
|
|
33
|
+
const target = busFile ?? eventFile; // a registered workspace's service events go to ITS bus, not the daemon's
|
|
33
34
|
try {
|
|
34
|
-
fs.mkdirSync(path.dirname(
|
|
35
|
-
fs.appendFileSync(
|
|
35
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
36
|
+
fs.appendFileSync(target, JSON.stringify({
|
|
36
37
|
id: eid(), sessionId: 'pending', timestamp: Date.now(),
|
|
37
38
|
source: 'gateway', category: 'cluster', severity,
|
|
38
39
|
context: { sessionId: 'pending', ...flowCtx }, fingerprint, title, raw,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trust.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/trust/trust.impl.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;YACE,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,OAAO,CAAC,EAAE;oBAAE,OAAO,CAAC,EAAE;wBAAE,MAAM,CAAC,EAAE,KAAK,CAAC;4BAAE,IAAI,CAAC,EAAE,MAAM,CAAA;yBAAE,CAAC,CAAA;qBAAE,CAAA;iBAAE,CAAA;aAAE,CAAC,CAAC;SAC7F,CACF,CAAC;KACH,CAAC;CACH;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,aAAa,CACzC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"trust.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/trust/trust.impl.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAEpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CACd,MAAM,EACN;YACE,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,OAAO,CAAC,EAAE;oBAAE,OAAO,CAAC,EAAE;wBAAE,MAAM,CAAC,EAAE,KAAK,CAAC;4BAAE,IAAI,CAAC,EAAE,MAAM,CAAA;yBAAE,CAAC,CAAA;qBAAE,CAAA;iBAAE,CAAA;aAAE,CAAC,CAAC;SAC7F,CACF,CAAC;KACH,CAAC;CACH;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAA8B,aAAa,CACzC,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAqF/B;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,mBAAmB,EAAE,OAAO,EAAE,eAAe,GAAG,MAAM,EAAE,CAmC7F"}
|
|
@@ -27,6 +27,11 @@ const basic_ssl_1 = require("../../basic-ssl");
|
|
|
27
27
|
async function trustExecutor(options, context) {
|
|
28
28
|
const cacheDir = path.join(context.root, 'node_modules', '.cache', 'davnx-webpack');
|
|
29
29
|
const caPath = (0, basic_ssl_1.ensureCaSync)(cacheDir);
|
|
30
|
+
// HOSTS-ONLY mode (set by the CLI when this workspace is a GUEST of a shared daemon): the daemon serves
|
|
31
|
+
// TLS with ITS own already-trusted CA, so this workspace's CA is unused — skip the keychain step + the
|
|
32
|
+
// (misleading) file-based CA verify, and set up /etc/hosts + DNS only. The CLI reports the REAL served-cert
|
|
33
|
+
// trust afterward.
|
|
34
|
+
const hostsOnly = process.env['LENSMCP_TRUST_HOSTS_ONLY'] === '1';
|
|
30
35
|
const hosts = collectHosts(options, context);
|
|
31
36
|
if (hosts.length === 0) {
|
|
32
37
|
console.log('[trust] no concrete hostnames found (only wildcards?) — keychain step only.');
|
|
@@ -38,11 +43,15 @@ async function trustExecutor(options, context) {
|
|
|
38
43
|
console.log(` • /etc/hosts: "127.0.0.1 ${h}" and "::1 ${h}"`);
|
|
39
44
|
return { success: true };
|
|
40
45
|
}
|
|
41
|
-
// --- 1) CA into the System keychain (skip when already there) -----------
|
|
42
|
-
const present =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
// --- 1) CA into the System keychain (skip when already there, or in hosts-only/guest mode) -----------
|
|
47
|
+
const present = hostsOnly ||
|
|
48
|
+
(0, node_child_process_1.spawnSync)('security', ['find-certificate', '-c', 'lensmcp local dev CA', '/Library/Keychains/System.keychain'], {
|
|
49
|
+
stdio: 'ignore',
|
|
50
|
+
}).status === 0;
|
|
51
|
+
if (hostsOnly) {
|
|
52
|
+
console.log('[trust] hosts-only: a shared daemon serves TLS with its own CA — skipping the keychain step.');
|
|
53
|
+
}
|
|
54
|
+
else if (present) {
|
|
46
55
|
console.log('[trust] CA already in the System keychain — skipping.');
|
|
47
56
|
}
|
|
48
57
|
else {
|
|
@@ -85,8 +94,12 @@ async function trustExecutor(options, context) {
|
|
|
85
94
|
(0, node_child_process_1.spawnSync)('sudo', ['sh', '-c', 'dscacheutil -flushcache; killall -HUP mDNSResponder'], { stdio: 'inherit' });
|
|
86
95
|
}
|
|
87
96
|
// --- 4) verify ------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
// Skip in hosts-only mode: verifying THIS workspace's (unused) CA file would falsely report "NOT TRUSTED"
|
|
98
|
+
// — the daemon serves with its own CA, and the CLI probes the real served cert's trust after this returns.
|
|
99
|
+
if (!hostsOnly) {
|
|
100
|
+
const verify = (0, node_child_process_1.spawnSync)('security', ['verify-cert', '-c', caPath, '-p', 'basic'], { stdio: 'ignore' });
|
|
101
|
+
console.log(`[trust] CA trust check: ${verify.status === 0 ? 'OK' : 'NOT TRUSTED YET (browser restart may be needed)'}`);
|
|
102
|
+
}
|
|
90
103
|
for (const h of hosts) {
|
|
91
104
|
console.log(`[trust] ${h} → https ready (hosts + cert SAN ride the gateway config)`);
|
|
92
105
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.16.
|
|
3
|
+
"version": "1.16.20",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"@fastify/reply-from": "^12.0.0",
|
|
34
34
|
"ioredis": "^5.0.0",
|
|
35
35
|
"@opentelemetry/api": "^1.9.0",
|
|
36
|
-
"@opentelemetry/core": "^
|
|
37
|
-
"@opentelemetry/resources": "^
|
|
38
|
-
"@opentelemetry/sdk-trace-base": "^
|
|
39
|
-
"@opentelemetry/exporter-trace-otlp-http": "^0.
|
|
36
|
+
"@opentelemetry/core": "^2.9.0",
|
|
37
|
+
"@opentelemetry/resources": "^2.9.0",
|
|
38
|
+
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
39
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.220.0"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"fastify": {
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@lensmcp/node-instrumentation": "1.16.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.16.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.16.20",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.20",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|