immortal-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +577 -0
- package/docs/CHANGELOG.md +21 -0
- package/docs/CONTRIBUTING.md +41 -0
- package/docs/api/chaos.md +179 -0
- package/docs/api/dashboard.md +109 -0
- package/docs/api/isolation.md +191 -0
- package/docs/api/lifecycle.md +187 -0
- package/docs/api/monitoring.md +313 -0
- package/docs/api/plugins.md +217 -0
- package/docs/api/recovery.md +267 -0
- package/docs/api/safe-zone.md +236 -0
- package/docs/api/supervision.md +285 -0
- package/docs/guides/configuration.md +168 -0
- package/docs/guides/express.md +171 -0
- package/docs/guides/fastify.md +188 -0
- package/docs/guides/koa.md +102 -0
- package/docs/guides/nestjs.md +182 -0
- package/docs/guides/testing.md +91 -0
- package/examples/express-basic/index.ts +462 -0
- package/examples/express-basic/package.json +21 -0
- package/examples/express-basic/tsconfig.json +24 -0
- package/examples/fastify-microservice/index.ts +342 -0
- package/examples/fastify-microservice/package.json +22 -0
- package/examples/fastify-microservice/tsconfig.json +24 -0
- package/examples/invoice-service/data/invoices.db +0 -0
- package/examples/invoice-service/data/invoices.db-shm +0 -0
- package/examples/invoice-service/data/invoices.db-wal +0 -0
- package/examples/invoice-service/package.json +25 -0
- package/examples/invoice-service/public/index.html +5025 -0
- package/examples/invoice-service/src/db.ts +608 -0
- package/examples/invoice-service/src/pdf.ts +358 -0
- package/examples/invoice-service/src/server.ts +527 -0
- package/examples/invoice-service/src/store.ts +159 -0
- package/examples/invoice-service/src/types.ts +193 -0
- package/examples/invoice-service/tsconfig.json +23 -0
- package/examples/nestjs-enterprise/app.module.ts +561 -0
- package/examples/nestjs-enterprise/main.ts +67 -0
- package/examples/nestjs-enterprise/package.json +26 -0
- package/examples/nestjs-enterprise/tsconfig.json +27 -0
- package/immortal-js-1.0.0.tgz +0 -0
- package/package.json +33 -0
- package/packages/adapter-express/package.json +34 -0
- package/packages/adapter-express/src/index.ts +349 -0
- package/packages/adapter-express/tsconfig.json +14 -0
- package/packages/adapter-fastify/package.json +56 -0
- package/packages/adapter-fastify/src/plugin.ts +226 -0
- package/packages/adapter-fastify/tsconfig.json +14 -0
- package/packages/adapter-koa/package.json +55 -0
- package/packages/adapter-koa/src/index.ts +207 -0
- package/packages/adapter-koa/tsconfig.json +14 -0
- package/packages/adapter-nestjs/package.json +61 -0
- package/packages/adapter-nestjs/src/immortal.module.ts +313 -0
- package/packages/adapter-nestjs/src/index.ts +14 -0
- package/packages/adapter-nestjs/tsconfig.json +16 -0
- package/packages/core/package.json +56 -0
- package/packages/core/src/chaos/ChaosEngine.ts +249 -0
- package/packages/core/src/config/defaults.ts +200 -0
- package/packages/core/src/config/schema.ts +199 -0
- package/packages/core/src/event-bus.ts +168 -0
- package/packages/core/src/index.ts +164 -0
- package/packages/core/src/isolation/BulkheadPool.ts +279 -0
- package/packages/core/src/isolation/WorkerSandbox.ts +306 -0
- package/packages/core/src/isolation/index.ts +8 -0
- package/packages/core/src/lifecycle/GracefulShutdown.ts +161 -0
- package/packages/core/src/logger.ts +104 -0
- package/packages/core/src/monitoring/DiagnosticsChannel.ts +248 -0
- package/packages/core/src/monitoring/HealthMonitor.ts +191 -0
- package/packages/core/src/monitoring/MemoryLeakGuard.ts +340 -0
- package/packages/core/src/monitoring/MetricsCollector.ts +219 -0
- package/packages/core/src/monitoring/index.ts +10 -0
- package/packages/core/src/plugins/BuiltinPlugins.ts +269 -0
- package/packages/core/src/recovery/CircuitBreaker.ts +334 -0
- package/packages/core/src/recovery/FallbackCache.ts +328 -0
- package/packages/core/src/recovery/RetryEngine.ts +225 -0
- package/packages/core/src/recovery/Timeout.ts +97 -0
- package/packages/core/src/recovery/index.ts +11 -0
- package/packages/core/src/runtime.ts +242 -0
- package/packages/core/src/safe-zone/AsyncBoundary.ts +114 -0
- package/packages/core/src/safe-zone/ErrorTrap.ts +347 -0
- package/packages/core/src/safe-zone/SafeWrapper.ts +317 -0
- package/packages/core/src/safe-zone/index.ts +23 -0
- package/packages/core/src/supervision/ClusterManager.ts +243 -0
- package/packages/core/src/supervision/RestartStrategy.ts +68 -0
- package/packages/core/src/supervision/Supervisor.ts +311 -0
- package/packages/core/src/supervision/index.ts +11 -0
- package/packages/core/src/types.ts +470 -0
- package/packages/core/test/bulkhead.test.ts +310 -0
- package/packages/core/test/circuit-breaker.test.ts +153 -0
- package/packages/core/test/memory-guard.test.ts +213 -0
- package/packages/core/test/retry.test.ts +110 -0
- package/packages/core/test/safe-zone.test.ts +271 -0
- package/packages/core/test/supervisor.test.ts +310 -0
- package/packages/core/tsconfig.json +13 -0
- package/packages/dashboard/package.json +56 -0
- package/packages/dashboard/server/DashboardServer.ts +454 -0
- package/packages/dashboard/tsconfig.json +14 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file MemoryLeakGuard.ts
|
|
3
|
+
* @description Memory leak detection and proactive restart
|
|
4
|
+
*
|
|
5
|
+
* Detection strategy (multi-signal, not just heap size):
|
|
6
|
+
* 1. Linear regression on heap growth across multiple windows
|
|
7
|
+
* 2. GC frequency trends (GC running more = struggling to reclaim)
|
|
8
|
+
* 3. Old generation growth rate (new gen is expected to be volatile)
|
|
9
|
+
* 4. RSS growth (overall process memory, includes native allocations)
|
|
10
|
+
* 5. WeakRef tracking for specific objects (optional, developer-controlled)
|
|
11
|
+
*
|
|
12
|
+
* Avoids false positives by:
|
|
13
|
+
* - Requiring N consecutive windows of growth (not just one spike)
|
|
14
|
+
* - Ignoring normal GC-driven sawtooth patterns
|
|
15
|
+
* - Using absolute thresholds as final override (when memory is genuinely critical)
|
|
16
|
+
*
|
|
17
|
+
* Proactive restart:
|
|
18
|
+
* Restarting before OOM is far safer than letting it crash:
|
|
19
|
+
* - Controlled: Supervisor orchestrates restart with zero-downtime
|
|
20
|
+
* - Uncontrolled: OOM kill leaves requests in-flight with no response
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import * as v8 from "node:v8";
|
|
24
|
+
import type { MemoryGuardConfig } from "../types.js";
|
|
25
|
+
import { DEFAULT_MEMORY_GUARD } from "../config/defaults.js";
|
|
26
|
+
import { getLogger } from "../logger.js";
|
|
27
|
+
import type { ImmortalEventBus } from "../event-bus.js";
|
|
28
|
+
import * as os from "node:os";
|
|
29
|
+
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
+
// TYPES
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
interface HeapWindow {
|
|
35
|
+
timestamp: number;
|
|
36
|
+
heapUsedMb: number;
|
|
37
|
+
heapTotalMb: number;
|
|
38
|
+
rssMb: number;
|
|
39
|
+
externalMb: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface LeakSignal {
|
|
43
|
+
type: "heap-growth" | "rss-growth" | "gc-pressure" | "absolute-threshold";
|
|
44
|
+
severity: "warning" | "critical";
|
|
45
|
+
value: number;
|
|
46
|
+
threshold: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
// MEMORY LEAK GUARD CLASS
|
|
51
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
export class MemoryLeakGuard {
|
|
54
|
+
private readonly config: Required<MemoryGuardConfig>;
|
|
55
|
+
private readonly bus: ImmortalEventBus;
|
|
56
|
+
private interval?: ReturnType<typeof setInterval>;
|
|
57
|
+
private running = false;
|
|
58
|
+
private windows: HeapWindow[] = [];
|
|
59
|
+
private consecutiveGrowthWindows = 0;
|
|
60
|
+
private lastRestartTime = 0;
|
|
61
|
+
private readonly minRestartIntervalMs = 5 * 60 * 1000; // Don't restart more than once per 5 minutes
|
|
62
|
+
|
|
63
|
+
// Adaptive thresholds based on available system RAM
|
|
64
|
+
private readonly adaptedWarningMb: number;
|
|
65
|
+
private readonly adaptedRestartMb: number;
|
|
66
|
+
|
|
67
|
+
constructor(config: MemoryGuardConfig = {}, bus: ImmortalEventBus) {
|
|
68
|
+
this.config = { ...DEFAULT_MEMORY_GUARD, ...config };
|
|
69
|
+
this.bus = bus;
|
|
70
|
+
|
|
71
|
+
// Adaptive thresholds: use configured values or 40%/60% of system RAM
|
|
72
|
+
if (this.config.adaptiveThresholds) {
|
|
73
|
+
const systemRamMb = os.totalmem() / 1024 / 1024;
|
|
74
|
+
this.adaptedWarningMb = Math.min(this.config.warningThresholdMb, systemRamMb * 0.40);
|
|
75
|
+
this.adaptedRestartMb = Math.min(this.config.restartThresholdMb, systemRamMb * 0.60);
|
|
76
|
+
} else {
|
|
77
|
+
this.adaptedWarningMb = this.config.warningThresholdMb;
|
|
78
|
+
this.adaptedRestartMb = this.config.restartThresholdMb;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Start memory monitoring
|
|
84
|
+
*/
|
|
85
|
+
start(): void {
|
|
86
|
+
if (this.running) return;
|
|
87
|
+
this.running = true;
|
|
88
|
+
|
|
89
|
+
this.interval = setInterval(() => {
|
|
90
|
+
this.sample();
|
|
91
|
+
}, this.config.checkIntervalMs).unref();
|
|
92
|
+
|
|
93
|
+
getLogger().info("MemoryLeakGuard started", {
|
|
94
|
+
checkIntervalMs: this.config.checkIntervalMs,
|
|
95
|
+
warningThresholdMb: `${this.adaptedWarningMb.toFixed(0)}MB`,
|
|
96
|
+
restartThresholdMb: `${this.adaptedRestartMb.toFixed(0)}MB`,
|
|
97
|
+
adaptive: this.config.adaptiveThresholds,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Stop monitoring
|
|
103
|
+
*/
|
|
104
|
+
stop(): void {
|
|
105
|
+
if (!this.running) return;
|
|
106
|
+
this.running = false;
|
|
107
|
+
|
|
108
|
+
if (this.interval) {
|
|
109
|
+
clearInterval(this.interval);
|
|
110
|
+
delete this.interval;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
getLogger().info("MemoryLeakGuard stopped");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Track a specific object for GC verification (optional)
|
|
118
|
+
* If the object is NOT garbage collected within timeoutMs, logs a warning
|
|
119
|
+
*/
|
|
120
|
+
trackObject(
|
|
121
|
+
obj: object,
|
|
122
|
+
name: string,
|
|
123
|
+
expectedGcMs = 30_000
|
|
124
|
+
): void {
|
|
125
|
+
if (!this.config.enableObjectTracking) return;
|
|
126
|
+
|
|
127
|
+
const ref = new WeakRef(obj);
|
|
128
|
+
const registry = new FinalizationRegistry((objectName: string) => {
|
|
129
|
+
getLogger().debug(`MemoryLeakGuard: Object '${objectName}' was garbage collected ✓`);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
registry.register(obj, name);
|
|
133
|
+
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (ref.deref() !== undefined) {
|
|
136
|
+
getLogger().warn(`MemoryLeakGuard: Object '${name}' was NOT garbage collected after ${expectedGcMs}ms`, {
|
|
137
|
+
hint: "This may indicate a memory leak (event listener not removed, connection not closed, etc.)",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
this.bus.emit("memory:leak-detected", {
|
|
141
|
+
data: { objectName: name, timeoutMs: expectedGcMs },
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}, expectedGcMs).unref();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
148
|
+
// SAMPLING LOGIC
|
|
149
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
private sample(): void {
|
|
152
|
+
const heapStats = v8.getHeapStatistics();
|
|
153
|
+
const memUsage = process.memoryUsage();
|
|
154
|
+
|
|
155
|
+
const window: HeapWindow = {
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
heapUsedMb: heapStats.used_heap_size / 1024 / 1024,
|
|
158
|
+
heapTotalMb: heapStats.heap_size_limit / 1024 / 1024,
|
|
159
|
+
rssMb: memUsage.rss / 1024 / 1024,
|
|
160
|
+
externalMb: memUsage.external / 1024 / 1024,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
this.windows.push(window);
|
|
164
|
+
|
|
165
|
+
// Keep only last 30 windows
|
|
166
|
+
if (this.windows.length > 30) {
|
|
167
|
+
this.windows.shift();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const signals = this.detectLeakSignals(window);
|
|
171
|
+
this.processSignals(signals, window);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private detectLeakSignals(current: HeapWindow): LeakSignal[] {
|
|
175
|
+
const signals: LeakSignal[] = [];
|
|
176
|
+
|
|
177
|
+
// ── Signal 1: Absolute threshold (immediate response) ──────────────
|
|
178
|
+
if (current.heapUsedMb > this.adaptedRestartMb) {
|
|
179
|
+
signals.push({
|
|
180
|
+
type: "absolute-threshold",
|
|
181
|
+
severity: "critical",
|
|
182
|
+
value: current.heapUsedMb,
|
|
183
|
+
threshold: this.adaptedRestartMb,
|
|
184
|
+
});
|
|
185
|
+
} else if (current.heapUsedMb > this.adaptedWarningMb) {
|
|
186
|
+
signals.push({
|
|
187
|
+
type: "absolute-threshold",
|
|
188
|
+
severity: "warning",
|
|
189
|
+
value: current.heapUsedMb,
|
|
190
|
+
threshold: this.adaptedWarningMb,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Need at least 5 windows for trend analysis
|
|
195
|
+
if (this.windows.length < 5) return signals;
|
|
196
|
+
|
|
197
|
+
// ── Signal 2: Heap growth trend (linear regression) ──────────────
|
|
198
|
+
const heapGrowthRate = this.computeGrowthRate(this.windows.map((w) => w.heapUsedMb));
|
|
199
|
+
if (heapGrowthRate > 50) {
|
|
200
|
+
// Growing > 50MB/hour
|
|
201
|
+
signals.push({
|
|
202
|
+
type: "heap-growth",
|
|
203
|
+
severity: "critical",
|
|
204
|
+
value: heapGrowthRate,
|
|
205
|
+
threshold: 50,
|
|
206
|
+
});
|
|
207
|
+
} else if (heapGrowthRate > 20) {
|
|
208
|
+
// Growing > 20MB/hour
|
|
209
|
+
signals.push({
|
|
210
|
+
type: "heap-growth",
|
|
211
|
+
severity: "warning",
|
|
212
|
+
value: heapGrowthRate,
|
|
213
|
+
threshold: 20,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ── Signal 3: RSS growth ───────────────────────────────────────────
|
|
218
|
+
const rssGrowthRate = this.computeGrowthRate(this.windows.map((w) => w.rssMb));
|
|
219
|
+
if (rssGrowthRate > 100) {
|
|
220
|
+
signals.push({
|
|
221
|
+
type: "rss-growth",
|
|
222
|
+
severity: "critical",
|
|
223
|
+
value: rssGrowthRate,
|
|
224
|
+
threshold: 100,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return signals;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private processSignals(signals: LeakSignal[], current: HeapWindow): void {
|
|
232
|
+
const logger = getLogger();
|
|
233
|
+
const hasCritical = signals.some((s) => s.severity === "critical");
|
|
234
|
+
const hasWarning = signals.some((s) => s.severity === "warning");
|
|
235
|
+
|
|
236
|
+
if (hasCritical) {
|
|
237
|
+
this.consecutiveGrowthWindows++;
|
|
238
|
+
} else if (hasWarning) {
|
|
239
|
+
this.consecutiveGrowthWindows = Math.max(0, this.consecutiveGrowthWindows - 1);
|
|
240
|
+
} else {
|
|
241
|
+
this.consecutiveGrowthWindows = 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Emit appropriate events
|
|
245
|
+
if (hasCritical) {
|
|
246
|
+
this.bus.emit("memory:critical", {
|
|
247
|
+
data: {
|
|
248
|
+
heapUsedMb: current.heapUsedMb,
|
|
249
|
+
rssMb: current.rssMb,
|
|
250
|
+
signals: signals.filter((s) => s.severity === "critical"),
|
|
251
|
+
consecutiveWindows: this.consecutiveGrowthWindows,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
logger.error("Memory CRITICAL threshold reached", {
|
|
256
|
+
heapMb: `${current.heapUsedMb.toFixed(1)}MB`,
|
|
257
|
+
rssMb: `${current.rssMb.toFixed(1)}MB`,
|
|
258
|
+
signals: signals.map((s) => s.type),
|
|
259
|
+
});
|
|
260
|
+
} else if (hasWarning) {
|
|
261
|
+
this.bus.emit("memory:warning", {
|
|
262
|
+
data: {
|
|
263
|
+
heapUsedMb: current.heapUsedMb,
|
|
264
|
+
signals: signals.filter((s) => s.severity === "warning"),
|
|
265
|
+
},
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
logger.warn("Memory warning threshold reached", {
|
|
269
|
+
heapMb: `${current.heapUsedMb.toFixed(1)}MB`,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Trigger proactive restart after N consecutive critical windows
|
|
274
|
+
const shouldRestart =
|
|
275
|
+
hasCritical &&
|
|
276
|
+
this.consecutiveGrowthWindows >= this.config.growthWindowCount &&
|
|
277
|
+
(Date.now() - this.lastRestartTime) > this.minRestartIntervalMs;
|
|
278
|
+
|
|
279
|
+
if (shouldRestart) {
|
|
280
|
+
this.triggerProactiveRestart(current, signals);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private triggerProactiveRestart(current: HeapWindow, signals: LeakSignal[]): void {
|
|
285
|
+
const logger = getLogger();
|
|
286
|
+
this.lastRestartTime = Date.now();
|
|
287
|
+
|
|
288
|
+
logger.error("MemoryLeakGuard: Triggering proactive restart before OOM", {
|
|
289
|
+
heapMb: `${current.heapUsedMb.toFixed(1)}MB`,
|
|
290
|
+
rssMb: `${current.rssMb.toFixed(1)}MB`,
|
|
291
|
+
consecutiveWindows: this.consecutiveGrowthWindows,
|
|
292
|
+
signals: signals.map((s) => `${s.type}:${s.severity}`),
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
this.bus.emit("memory:proactive-restart", {
|
|
296
|
+
data: {
|
|
297
|
+
heapUsedMb: current.heapUsedMb,
|
|
298
|
+
rssMb: current.rssMb,
|
|
299
|
+
signals,
|
|
300
|
+
reason: "memory-leak-guard",
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// If no supervisor is listening to the restart event, force exit
|
|
305
|
+
// so the external process manager (PM2, K8s) handles the restart
|
|
306
|
+
setTimeout(() => {
|
|
307
|
+
logger.error("MemoryLeakGuard: No restart handler responded — forcing process exit");
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}, 10_000).unref();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Simple linear regression to compute growth rate across time series
|
|
314
|
+
* Returns MB per hour
|
|
315
|
+
*/
|
|
316
|
+
private computeGrowthRate(values: number[]): number {
|
|
317
|
+
const n = values.length;
|
|
318
|
+
if (n < 2) return 0;
|
|
319
|
+
|
|
320
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
321
|
+
for (let i = 0; i < n; i++) {
|
|
322
|
+
sumX += i;
|
|
323
|
+
sumY += values[i]!;
|
|
324
|
+
sumXY += i * values[i]!;
|
|
325
|
+
sumX2 += i * i;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const denominator = n * sumX2 - sumX * sumX;
|
|
329
|
+
if (denominator === 0) return 0;
|
|
330
|
+
|
|
331
|
+
// slope = MB per interval
|
|
332
|
+
const slope = (n * sumXY - sumX * sumY) / denominator;
|
|
333
|
+
|
|
334
|
+
// Convert to MB per hour based on check interval
|
|
335
|
+
const intervalsPerHour = 3_600_000 / this.config.checkIntervalMs;
|
|
336
|
+
const ratePerHour = slope * intervalsPerHour;
|
|
337
|
+
|
|
338
|
+
return isFinite(ratePerHour) ? ratePerHour : 0;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file MetricsCollector.ts
|
|
3
|
+
* @description Central metrics aggregation hub
|
|
4
|
+
* Collects, stores, and exposes system-wide metrics as a coherent snapshot
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as v8 from "node:v8";
|
|
8
|
+
import * as process from "node:process";
|
|
9
|
+
import type {
|
|
10
|
+
SystemMetrics,
|
|
11
|
+
CircuitBreakerStatus,
|
|
12
|
+
BulkheadStatus,
|
|
13
|
+
WorkerStatus,
|
|
14
|
+
} from "../types.js";
|
|
15
|
+
|
|
16
|
+
export interface RequestMetrics {
|
|
17
|
+
total: number;
|
|
18
|
+
active: number;
|
|
19
|
+
errors: number;
|
|
20
|
+
timestamps: number[]; // for per-second calculation
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface EventLoopMetrics {
|
|
24
|
+
p50: number;
|
|
25
|
+
p95: number;
|
|
26
|
+
p99: number;
|
|
27
|
+
max: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface CpuMetrics {
|
|
31
|
+
percentUser: number;
|
|
32
|
+
percentSystem: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
+
// METRICS COLLECTOR
|
|
37
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export class MetricsCollector {
|
|
40
|
+
private eventLoopMetrics: EventLoopMetrics = { p50: 0, p95: 0, p99: 0, max: 0 };
|
|
41
|
+
private cpuMetrics: CpuMetrics = { percentUser: 0, percentSystem: 0 };
|
|
42
|
+
private requestMetrics: RequestMetrics = {
|
|
43
|
+
total: 0,
|
|
44
|
+
active: 0,
|
|
45
|
+
errors: 0,
|
|
46
|
+
timestamps: [],
|
|
47
|
+
};
|
|
48
|
+
private circuits: Record<string, CircuitBreakerStatus> = {};
|
|
49
|
+
private bulkheads: Record<string, BulkheadStatus> = {};
|
|
50
|
+
private workers: WorkerStatus[] = [];
|
|
51
|
+
private lastCpuUsage = process.cpuUsage();
|
|
52
|
+
private lastCpuTime = Date.now();
|
|
53
|
+
private heapGrowthHistory: number[] = [];
|
|
54
|
+
private lastHeapMb = 0;
|
|
55
|
+
private gcCount = 0;
|
|
56
|
+
private lastGcCountTime = Date.now();
|
|
57
|
+
|
|
58
|
+
constructor() {
|
|
59
|
+
this.initGcTracking();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Update event loop lag metrics (called by HealthMonitor) */
|
|
63
|
+
updateEventLoopMetrics(metrics: EventLoopMetrics): void {
|
|
64
|
+
this.eventLoopMetrics = metrics;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Update CPU metrics */
|
|
68
|
+
updateCpuMetrics(): void {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const elapsed = (now - this.lastCpuTime) / 1000;
|
|
71
|
+
const usage = process.cpuUsage(this.lastCpuUsage);
|
|
72
|
+
|
|
73
|
+
this.cpuMetrics = {
|
|
74
|
+
percentUser: (usage.user / 1000 / elapsed) / 100,
|
|
75
|
+
percentSystem: (usage.system / 1000 / elapsed) / 100,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
this.lastCpuUsage = process.cpuUsage();
|
|
79
|
+
this.lastCpuTime = now;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Record a request started */
|
|
83
|
+
recordRequestStart(): void {
|
|
84
|
+
this.requestMetrics.total++;
|
|
85
|
+
this.requestMetrics.active++;
|
|
86
|
+
this.requestMetrics.timestamps.push(Date.now());
|
|
87
|
+
|
|
88
|
+
// Keep only last 60 seconds of timestamps
|
|
89
|
+
const cutoff = Date.now() - 60_000;
|
|
90
|
+
this.requestMetrics.timestamps = this.requestMetrics.timestamps.filter((t) => t > cutoff);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Record a request finished */
|
|
94
|
+
recordRequestEnd(wasError = false): void {
|
|
95
|
+
this.requestMetrics.active = Math.max(0, this.requestMetrics.active - 1);
|
|
96
|
+
if (wasError) this.requestMetrics.errors++;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Update circuit breaker statuses */
|
|
100
|
+
updateCircuits(circuits: Record<string, CircuitBreakerStatus>): void {
|
|
101
|
+
this.circuits = circuits;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Update bulkhead statuses */
|
|
105
|
+
updateBulkheads(bulkheads: Record<string, BulkheadStatus>): void {
|
|
106
|
+
this.bulkheads = bulkheads;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Update worker statuses */
|
|
110
|
+
updateWorkers(workers: WorkerStatus[]): void {
|
|
111
|
+
this.workers = workers;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Track heap growth for MemoryLeakGuard */
|
|
115
|
+
recordHeapSample(heapMb: number): number {
|
|
116
|
+
const growthMb = heapMb - this.lastHeapMb;
|
|
117
|
+
this.lastHeapMb = heapMb;
|
|
118
|
+
|
|
119
|
+
this.heapGrowthHistory.push(growthMb);
|
|
120
|
+
if (this.heapGrowthHistory.length > 20) {
|
|
121
|
+
this.heapGrowthHistory.shift();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return growthMb;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Get heap growth rate (MB/minute) using linear regression */
|
|
128
|
+
getHeapGrowthRate(): number {
|
|
129
|
+
if (this.heapGrowthHistory.length < 3) return 0;
|
|
130
|
+
|
|
131
|
+
const n = this.heapGrowthHistory.length;
|
|
132
|
+
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < n; i++) {
|
|
135
|
+
sumX += i;
|
|
136
|
+
sumY += this.heapGrowthHistory[i]!;
|
|
137
|
+
sumXY += i * this.heapGrowthHistory[i]!;
|
|
138
|
+
sumX2 += i * i;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
142
|
+
return isFinite(slope) ? slope * 60 : 0; // Convert to per-minute
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Get GC frequency (GCs per minute) */
|
|
146
|
+
getGcFrequency(): number {
|
|
147
|
+
const elapsed = (Date.now() - this.lastGcCountTime) / 60_000;
|
|
148
|
+
return elapsed > 0 ? this.gcCount / elapsed : 0;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get a complete system metrics snapshot
|
|
153
|
+
*/
|
|
154
|
+
getSnapshot(): SystemMetrics {
|
|
155
|
+
const heapStats = v8.getHeapStatistics();
|
|
156
|
+
const heapUsedMb = heapStats.used_heap_size / 1024 / 1024;
|
|
157
|
+
const heapTotalMb = heapStats.heap_size_limit / 1024 / 1024;
|
|
158
|
+
const memUsage = process.memoryUsage();
|
|
159
|
+
|
|
160
|
+
this.updateCpuMetrics();
|
|
161
|
+
|
|
162
|
+
// Per-second request rate (last 10 seconds)
|
|
163
|
+
const tenSecondsAgo = Date.now() - 10_000;
|
|
164
|
+
const recentRequests = this.requestMetrics.timestamps.filter((t) => t > tenSecondsAgo);
|
|
165
|
+
const perSecond = recentRequests.length / 10;
|
|
166
|
+
|
|
167
|
+
// Error rate
|
|
168
|
+
const errorRate = this.requestMetrics.total > 0
|
|
169
|
+
? this.requestMetrics.errors / this.requestMetrics.total
|
|
170
|
+
: 0;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
eventLoopLag: this.eventLoopMetrics,
|
|
175
|
+
memory: {
|
|
176
|
+
heapUsedMb: Math.round(heapUsedMb * 100) / 100,
|
|
177
|
+
heapTotalMb: Math.round(heapTotalMb * 100) / 100,
|
|
178
|
+
externalMb: Math.round(memUsage.external / 1024 / 1024 * 100) / 100,
|
|
179
|
+
rssMb: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
|
|
180
|
+
heapGrowthRate: Math.round(this.getHeapGrowthRate() * 100) / 100,
|
|
181
|
+
gcFrequency: Math.round(this.getGcFrequency() * 10) / 10,
|
|
182
|
+
},
|
|
183
|
+
cpu: {
|
|
184
|
+
percentUser: Math.round(this.cpuMetrics.percentUser * 100) / 100,
|
|
185
|
+
percentSystem: Math.round(this.cpuMetrics.percentSystem * 100) / 100,
|
|
186
|
+
},
|
|
187
|
+
handles: {
|
|
188
|
+
active: (process as NodeJS.Process & { _getActiveHandles?: () => unknown[] })._getActiveHandles?.().length ?? 0,
|
|
189
|
+
},
|
|
190
|
+
requests: {
|
|
191
|
+
total: this.requestMetrics.total,
|
|
192
|
+
active: this.requestMetrics.active,
|
|
193
|
+
perSecond: Math.round(perSecond * 10) / 10,
|
|
194
|
+
errorRate: Math.round(errorRate * 1000) / 1000,
|
|
195
|
+
},
|
|
196
|
+
circuits: this.circuits,
|
|
197
|
+
bulkheads: this.bulkheads,
|
|
198
|
+
workers: this.workers,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private initGcTracking(): void {
|
|
203
|
+
try {
|
|
204
|
+
// Use PerformanceObserver for GC events (Node 18+)
|
|
205
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
206
|
+
const { PerformanceObserver } = require("node:perf_hooks") as typeof import("perf_hooks");
|
|
207
|
+
const observer = new PerformanceObserver((list) => {
|
|
208
|
+
for (const entry of list.getEntries()) {
|
|
209
|
+
if (entry.entryType === "gc") {
|
|
210
|
+
this.gcCount++;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
observer.observe({ entryTypes: ["gc"] });
|
|
215
|
+
} catch {
|
|
216
|
+
// GC tracking not available in this environment
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file monitoring/index.ts
|
|
3
|
+
* @description Monitoring layer public exports
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { HealthMonitor } from "./HealthMonitor.js";
|
|
7
|
+
export { MemoryLeakGuard } from "./MemoryLeakGuard.js";
|
|
8
|
+
export { MetricsCollector } from "./MetricsCollector.js";
|
|
9
|
+
export type { RequestMetrics, EventLoopMetrics, CpuMetrics } from "./MetricsCollector.js";
|
|
10
|
+
export { DiagnosticsChannel, IMMORTAL_CHANNELS } from "./DiagnosticsChannel.js";
|