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.
Files changed (98) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +577 -0
  3. package/docs/CHANGELOG.md +21 -0
  4. package/docs/CONTRIBUTING.md +41 -0
  5. package/docs/api/chaos.md +179 -0
  6. package/docs/api/dashboard.md +109 -0
  7. package/docs/api/isolation.md +191 -0
  8. package/docs/api/lifecycle.md +187 -0
  9. package/docs/api/monitoring.md +313 -0
  10. package/docs/api/plugins.md +217 -0
  11. package/docs/api/recovery.md +267 -0
  12. package/docs/api/safe-zone.md +236 -0
  13. package/docs/api/supervision.md +285 -0
  14. package/docs/guides/configuration.md +168 -0
  15. package/docs/guides/express.md +171 -0
  16. package/docs/guides/fastify.md +188 -0
  17. package/docs/guides/koa.md +102 -0
  18. package/docs/guides/nestjs.md +182 -0
  19. package/docs/guides/testing.md +91 -0
  20. package/examples/express-basic/index.ts +462 -0
  21. package/examples/express-basic/package.json +21 -0
  22. package/examples/express-basic/tsconfig.json +24 -0
  23. package/examples/fastify-microservice/index.ts +342 -0
  24. package/examples/fastify-microservice/package.json +22 -0
  25. package/examples/fastify-microservice/tsconfig.json +24 -0
  26. package/examples/invoice-service/data/invoices.db +0 -0
  27. package/examples/invoice-service/data/invoices.db-shm +0 -0
  28. package/examples/invoice-service/data/invoices.db-wal +0 -0
  29. package/examples/invoice-service/package.json +25 -0
  30. package/examples/invoice-service/public/index.html +5025 -0
  31. package/examples/invoice-service/src/db.ts +608 -0
  32. package/examples/invoice-service/src/pdf.ts +358 -0
  33. package/examples/invoice-service/src/server.ts +527 -0
  34. package/examples/invoice-service/src/store.ts +159 -0
  35. package/examples/invoice-service/src/types.ts +193 -0
  36. package/examples/invoice-service/tsconfig.json +23 -0
  37. package/examples/nestjs-enterprise/app.module.ts +561 -0
  38. package/examples/nestjs-enterprise/main.ts +67 -0
  39. package/examples/nestjs-enterprise/package.json +26 -0
  40. package/examples/nestjs-enterprise/tsconfig.json +27 -0
  41. package/immortal-js-1.0.0.tgz +0 -0
  42. package/package.json +33 -0
  43. package/packages/adapter-express/package.json +34 -0
  44. package/packages/adapter-express/src/index.ts +349 -0
  45. package/packages/adapter-express/tsconfig.json +14 -0
  46. package/packages/adapter-fastify/package.json +56 -0
  47. package/packages/adapter-fastify/src/plugin.ts +226 -0
  48. package/packages/adapter-fastify/tsconfig.json +14 -0
  49. package/packages/adapter-koa/package.json +55 -0
  50. package/packages/adapter-koa/src/index.ts +207 -0
  51. package/packages/adapter-koa/tsconfig.json +14 -0
  52. package/packages/adapter-nestjs/package.json +61 -0
  53. package/packages/adapter-nestjs/src/immortal.module.ts +313 -0
  54. package/packages/adapter-nestjs/src/index.ts +14 -0
  55. package/packages/adapter-nestjs/tsconfig.json +16 -0
  56. package/packages/core/package.json +56 -0
  57. package/packages/core/src/chaos/ChaosEngine.ts +249 -0
  58. package/packages/core/src/config/defaults.ts +200 -0
  59. package/packages/core/src/config/schema.ts +199 -0
  60. package/packages/core/src/event-bus.ts +168 -0
  61. package/packages/core/src/index.ts +164 -0
  62. package/packages/core/src/isolation/BulkheadPool.ts +279 -0
  63. package/packages/core/src/isolation/WorkerSandbox.ts +306 -0
  64. package/packages/core/src/isolation/index.ts +8 -0
  65. package/packages/core/src/lifecycle/GracefulShutdown.ts +161 -0
  66. package/packages/core/src/logger.ts +104 -0
  67. package/packages/core/src/monitoring/DiagnosticsChannel.ts +248 -0
  68. package/packages/core/src/monitoring/HealthMonitor.ts +191 -0
  69. package/packages/core/src/monitoring/MemoryLeakGuard.ts +340 -0
  70. package/packages/core/src/monitoring/MetricsCollector.ts +219 -0
  71. package/packages/core/src/monitoring/index.ts +10 -0
  72. package/packages/core/src/plugins/BuiltinPlugins.ts +269 -0
  73. package/packages/core/src/recovery/CircuitBreaker.ts +334 -0
  74. package/packages/core/src/recovery/FallbackCache.ts +328 -0
  75. package/packages/core/src/recovery/RetryEngine.ts +225 -0
  76. package/packages/core/src/recovery/Timeout.ts +97 -0
  77. package/packages/core/src/recovery/index.ts +11 -0
  78. package/packages/core/src/runtime.ts +242 -0
  79. package/packages/core/src/safe-zone/AsyncBoundary.ts +114 -0
  80. package/packages/core/src/safe-zone/ErrorTrap.ts +347 -0
  81. package/packages/core/src/safe-zone/SafeWrapper.ts +317 -0
  82. package/packages/core/src/safe-zone/index.ts +23 -0
  83. package/packages/core/src/supervision/ClusterManager.ts +243 -0
  84. package/packages/core/src/supervision/RestartStrategy.ts +68 -0
  85. package/packages/core/src/supervision/Supervisor.ts +311 -0
  86. package/packages/core/src/supervision/index.ts +11 -0
  87. package/packages/core/src/types.ts +470 -0
  88. package/packages/core/test/bulkhead.test.ts +310 -0
  89. package/packages/core/test/circuit-breaker.test.ts +153 -0
  90. package/packages/core/test/memory-guard.test.ts +213 -0
  91. package/packages/core/test/retry.test.ts +110 -0
  92. package/packages/core/test/safe-zone.test.ts +271 -0
  93. package/packages/core/test/supervisor.test.ts +310 -0
  94. package/packages/core/tsconfig.json +13 -0
  95. package/packages/dashboard/package.json +56 -0
  96. package/packages/dashboard/server/DashboardServer.ts +454 -0
  97. package/packages/dashboard/tsconfig.json +14 -0
  98. package/tsconfig.json +25 -0
@@ -0,0 +1,248 @@
1
+ /**
2
+ * @file DiagnosticsChannel.ts
3
+ * @description Node.js diagnostics_channel integration + OpenTelemetry export bridge
4
+ *
5
+ * Provides:
6
+ * 1. Publishing Immortal events to Node.js diagnostics_channel (for APM tool integration)
7
+ * 2. Prometheus-compatible metrics exposition
8
+ * 3. OpenTelemetry OTLP metrics export (when configured)
9
+ */
10
+
11
+ import diagnostics_channel from "node:diagnostics_channel";
12
+ import type { ImmortalEvent, SystemMetrics } from "../types.js";
13
+ import type { ImmortalEventBus } from "../event-bus.js";
14
+ import { getLogger } from "../logger.js";
15
+
16
+ // ─────────────────────────────────────────────────────────────────────────────
17
+ // CHANNEL NAMES
18
+ // ─────────────────────────────────────────────────────────────────────────────
19
+
20
+ export const IMMORTAL_CHANNELS = {
21
+ ERROR: "immortal:error",
22
+ CIRCUIT: "immortal:circuit",
23
+ RETRY: "immortal:retry",
24
+ BULKHEAD: "immortal:bulkhead",
25
+ WORKER: "immortal:worker",
26
+ MEMORY: "immortal:memory",
27
+ METRICS: "immortal:metrics",
28
+ } as const;
29
+
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // DIAGNOSTICS CHANNEL CLASS
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ export class DiagnosticsChannel {
35
+ private channels: Map<string, ReturnType<typeof diagnostics_channel.channel>>;
36
+ private readonly bus: ImmortalEventBus;
37
+ private metricsInterval?: ReturnType<typeof setInterval>;
38
+ private otlpEndpoint?: string;
39
+ private prometheusEnabled: boolean;
40
+ private metricsSnapshot: SystemMetrics | null = null;
41
+
42
+ constructor(bus: ImmortalEventBus, options: {
43
+ otlpEndpoint?: string;
44
+ enablePrometheus?: boolean;
45
+ } = {}) {
46
+ this.bus = bus;
47
+ if (options.otlpEndpoint !== undefined) this.otlpEndpoint = options.otlpEndpoint;
48
+ this.prometheusEnabled = options.enablePrometheus ?? true;
49
+
50
+ this.channels = new Map(
51
+ Object.values(IMMORTAL_CHANNELS).map((name) => [
52
+ name,
53
+ diagnostics_channel.channel(name),
54
+ ])
55
+ );
56
+
57
+ this.wireEventBus();
58
+ }
59
+
60
+ /**
61
+ * Update the current metrics snapshot (called by MetricsCollector)
62
+ */
63
+ updateMetrics(metrics: SystemMetrics): void {
64
+ this.metricsSnapshot = metrics;
65
+
66
+ if (this.channels.get(IMMORTAL_CHANNELS.METRICS)?.hasSubscribers) {
67
+ this.channels.get(IMMORTAL_CHANNELS.METRICS)?.publish(metrics);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get Prometheus-format metrics text
73
+ * Compatible with Prometheus scraping and Grafana datasource
74
+ */
75
+ getPrometheusMetrics(): string {
76
+ if (!this.prometheusEnabled || !this.metricsSnapshot) {
77
+ return "# Immortal.js metrics not available\n";
78
+ }
79
+
80
+ const m = this.metricsSnapshot;
81
+ const lines: string[] = [
82
+ "# HELP immortal_event_loop_lag_ms Event loop lag percentiles in milliseconds",
83
+ "# TYPE immortal_event_loop_lag_ms gauge",
84
+ `immortal_event_loop_lag_ms{quantile="0.5"} ${m.eventLoopLag.p50}`,
85
+ `immortal_event_loop_lag_ms{quantile="0.95"} ${m.eventLoopLag.p95}`,
86
+ `immortal_event_loop_lag_ms{quantile="0.99"} ${m.eventLoopLag.p99}`,
87
+ `immortal_event_loop_lag_ms{quantile="max"} ${m.eventLoopLag.max}`,
88
+ "",
89
+ "# HELP immortal_heap_used_mb Heap used in megabytes",
90
+ "# TYPE immortal_heap_used_mb gauge",
91
+ `immortal_heap_used_mb ${m.memory.heapUsedMb}`,
92
+ "",
93
+ "# HELP immortal_heap_total_mb Heap total limit in megabytes",
94
+ "# TYPE immortal_heap_total_mb gauge",
95
+ `immortal_heap_total_mb ${m.memory.heapTotalMb}`,
96
+ "",
97
+ "# HELP immortal_rss_mb Resident Set Size in megabytes",
98
+ "# TYPE immortal_rss_mb gauge",
99
+ `immortal_rss_mb ${m.memory.rssMb}`,
100
+ "",
101
+ "# HELP immortal_heap_growth_rate_mb_per_min Heap growth rate",
102
+ "# TYPE immortal_heap_growth_rate_mb_per_min gauge",
103
+ `immortal_heap_growth_rate_mb_per_min ${m.memory.heapGrowthRate}`,
104
+ "",
105
+ "# HELP immortal_cpu_percent CPU utilization percentage",
106
+ "# TYPE immortal_cpu_percent gauge",
107
+ `immortal_cpu_percent{type="user"} ${m.cpu.percentUser}`,
108
+ `immortal_cpu_percent{type="system"} ${m.cpu.percentSystem}`,
109
+ "",
110
+ "# HELP immortal_requests_total Total HTTP requests handled",
111
+ "# TYPE immortal_requests_total counter",
112
+ `immortal_requests_total ${m.requests.total}`,
113
+ "",
114
+ "# HELP immortal_requests_active Currently active requests",
115
+ "# TYPE immortal_requests_active gauge",
116
+ `immortal_requests_active ${m.requests.active}`,
117
+ "",
118
+ "# HELP immortal_requests_per_second Request rate per second",
119
+ "# TYPE immortal_requests_per_second gauge",
120
+ `immortal_requests_per_second ${m.requests.perSecond}`,
121
+ "",
122
+ "# HELP immortal_error_rate Request error rate (0-1)",
123
+ "# TYPE immortal_error_rate gauge",
124
+ `immortal_error_rate ${m.requests.errorRate}`,
125
+ "",
126
+ ];
127
+
128
+ // Circuit breaker metrics
129
+ for (const [name, circuit] of Object.entries(m.circuits)) {
130
+ const stateValue = circuit.state === "CLOSED" ? 0 : circuit.state === "HALF_OPEN" ? 1 : 2;
131
+ lines.push(`immortal_circuit_state{circuit="${name}"} ${stateValue}`);
132
+ lines.push(`immortal_circuit_failure_rate{circuit="${name}"} ${circuit.failureRate}`);
133
+ lines.push(`immortal_circuit_total_requests{circuit="${name}"} ${circuit.totalRequests}`);
134
+ }
135
+
136
+ if (Object.keys(m.circuits).length > 0) {
137
+ lines.unshift(
138
+ "# HELP immortal_circuit_state Circuit state (0=CLOSED, 1=HALF_OPEN, 2=OPEN)",
139
+ "# TYPE immortal_circuit_state gauge"
140
+ );
141
+ }
142
+
143
+ // Bulkhead metrics
144
+ for (const [name, bh] of Object.entries(m.bulkheads)) {
145
+ lines.push(`immortal_bulkhead_active{pool="${name}"} ${bh.active}`);
146
+ lines.push(`immortal_bulkhead_queued{pool="${name}"} ${bh.queued}`);
147
+ lines.push(`immortal_bulkhead_rejected{pool="${name}"} ${bh.rejected}`);
148
+ }
149
+
150
+ return lines.join("\n") + "\n";
151
+ }
152
+
153
+ /**
154
+ * Subscribe to a specific diagnostics channel (for APM integration)
155
+ */
156
+ subscribe(channelName: string, callback: (data: unknown) => void): void {
157
+ diagnostics_channel.subscribe(channelName, callback);
158
+ }
159
+
160
+ /**
161
+ * Stop all periodic exports
162
+ */
163
+ stop(): void {
164
+ if (this.metricsInterval) {
165
+ clearInterval(this.metricsInterval);
166
+ delete this.metricsInterval;
167
+ }
168
+ }
169
+
170
+ // ─────────────────────────────────────────────────────────────────────────
171
+ // PRIVATE
172
+ // ─────────────────────────────────────────────────────────────────────────
173
+
174
+ private wireEventBus(): void {
175
+ // Bridge Immortal events to diagnostics channels
176
+ this.bus.onAny((event: ImmortalEvent) => {
177
+ const channelName = this.getChannelForEvent(event.type);
178
+ const channel = this.channels.get(channelName);
179
+
180
+ if (channel?.hasSubscribers) {
181
+ channel.publish({ event, timestamp: Date.now() });
182
+ }
183
+ });
184
+ }
185
+
186
+ private getChannelForEvent(type: string): string {
187
+ if (type.startsWith("error:")) return IMMORTAL_CHANNELS.ERROR;
188
+ if (type.startsWith("circuit:")) return IMMORTAL_CHANNELS.CIRCUIT;
189
+ if (type.startsWith("retry:")) return IMMORTAL_CHANNELS.RETRY;
190
+ if (type.startsWith("bulkhead:")) return IMMORTAL_CHANNELS.BULKHEAD;
191
+ if (type.startsWith("worker:")) return IMMORTAL_CHANNELS.WORKER;
192
+ if (type.startsWith("memory:")) return IMMORTAL_CHANNELS.MEMORY;
193
+ return IMMORTAL_CHANNELS.METRICS;
194
+ }
195
+
196
+ /**
197
+ * Export metrics to OTLP endpoint (OpenTelemetry)
198
+ * Format: OTLP/HTTP JSON
199
+ */
200
+ private async exportToOTLP(metrics: SystemMetrics): Promise<void> {
201
+ if (!this.otlpEndpoint) return;
202
+
203
+ const otlpPayload = this.buildOTLPPayload(metrics);
204
+
205
+ try {
206
+ await fetch(this.otlpEndpoint, {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify(otlpPayload),
210
+ });
211
+ } catch (err) {
212
+ getLogger().debug("OTLP export failed", { error: (err as Error).message });
213
+ }
214
+ }
215
+
216
+ private buildOTLPPayload(metrics: SystemMetrics): Record<string, unknown> {
217
+ // Simplified OTLP/HTTP JSON format
218
+ return {
219
+ resourceMetrics: [{
220
+ resource: {
221
+ attributes: [
222
+ { key: "service.name", value: { stringValue: "immortal-js" } },
223
+ { key: "service.version", value: { stringValue: "1.0.0" } },
224
+ ],
225
+ },
226
+ scopeMetrics: [{
227
+ metrics: [
228
+ {
229
+ name: "immortal.event_loop.lag.p99",
230
+ unit: "ms",
231
+ gauge: { dataPoints: [{ asDouble: metrics.eventLoopLag.p99, timeUnixNano: metrics.timestamp * 1e6 }] },
232
+ },
233
+ {
234
+ name: "immortal.memory.heap_used",
235
+ unit: "MB",
236
+ gauge: { dataPoints: [{ asDouble: metrics.memory.heapUsedMb, timeUnixNano: metrics.timestamp * 1e6 }] },
237
+ },
238
+ {
239
+ name: "immortal.requests.per_second",
240
+ unit: "req/s",
241
+ gauge: { dataPoints: [{ asDouble: metrics.requests.perSecond, timeUnixNano: metrics.timestamp * 1e6 }] },
242
+ },
243
+ ],
244
+ }],
245
+ }],
246
+ };
247
+ }
248
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * @file HealthMonitor.ts
3
+ * @description Real-time system health monitoring
4
+ *
5
+ * Monitors:
6
+ * - Event Loop Delay/Lag (via perf_hooks histogram — most accurate method)
7
+ * - CPU utilization (via process.cpuUsage)
8
+ * - Open handles count (via process._getActiveHandles)
9
+ * - Memory usage trends
10
+ *
11
+ * Key insight on event loop lag:
12
+ * High event loop lag means synchronous CPU work is blocking the loop —
13
+ * the server literally cannot respond to new requests during that time.
14
+ * Detecting > threshold is a signal to restart the worker (not just log).
15
+ */
16
+
17
+ import { monitorEventLoopDelay } from "node:perf_hooks";
18
+ import type { HealthMonitorConfig } from "../types.js";
19
+ import { DEFAULT_HEALTH_MONITOR } from "../config/defaults.js";
20
+ import { getLogger } from "../logger.js";
21
+ import type { ImmortalEventBus } from "../event-bus.js";
22
+ import type { MetricsCollector } from "./MetricsCollector.js";
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // HEALTH MONITOR CLASS
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ export class HealthMonitor {
29
+ private readonly config: Required<HealthMonitorConfig>;
30
+ private readonly bus: ImmortalEventBus;
31
+ private readonly metrics: MetricsCollector;
32
+ private interval?: ReturnType<typeof setInterval>;
33
+ private histogram: ReturnType<typeof monitorEventLoopDelay>;
34
+ private running = false;
35
+ private consecutiveCriticalLag = 0;
36
+ private consecutiveCriticalCpu = 0;
37
+ private restartRequested = false;
38
+ private supervisorRef?: { requestGracefulRestart: (id: string, reason?: string) => void };
39
+
40
+ constructor(
41
+ config: HealthMonitorConfig = {},
42
+ bus: ImmortalEventBus,
43
+ metrics: MetricsCollector
44
+ ) {
45
+ this.config = { ...DEFAULT_HEALTH_MONITOR, ...config };
46
+ this.bus = bus;
47
+ this.metrics = metrics;
48
+
49
+ // Initialize event loop histogram (20ms resolution = balance of accuracy vs overhead)
50
+ this.histogram = monitorEventLoopDelay({ resolution: 20 });
51
+ }
52
+
53
+ /**
54
+ * Wire a supervisor reference for proactive restarts
55
+ */
56
+ setSupervisor(supervisor: { requestGracefulRestart: (id: string, reason?: string) => void }): void {
57
+ this.supervisorRef = supervisor;
58
+ }
59
+
60
+ /**
61
+ * Start monitoring
62
+ */
63
+ start(): void {
64
+ if (this.running) return;
65
+
66
+ this.histogram.enable();
67
+ this.running = true;
68
+
69
+ this.interval = setInterval(() => {
70
+ this.collect();
71
+ }, this.config.intervalMs).unref();
72
+
73
+ getLogger().info("HealthMonitor started", {
74
+ intervalMs: this.config.intervalMs,
75
+ eventLoopLagThreshold: `${this.config.eventLoopLagMs}ms`,
76
+ criticalLagThreshold: `${this.config.criticalLagMs}ms`,
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Stop monitoring
82
+ */
83
+ stop(): void {
84
+ if (!this.running) return;
85
+
86
+ this.running = false;
87
+
88
+ if (this.interval) {
89
+ clearInterval(this.interval);
90
+ delete this.interval;
91
+ }
92
+
93
+ this.histogram.disable();
94
+ getLogger().info("HealthMonitor stopped");
95
+ }
96
+
97
+ /**
98
+ * Force an immediate health check (for testing)
99
+ */
100
+ checkNow(): ReturnType<MetricsCollector["getSnapshot"]> {
101
+ this.collect();
102
+ return this.metrics.getSnapshot();
103
+ }
104
+
105
+ // ─────────────────────────────────────────────────────────────────────────
106
+ // COLLECTION LOGIC
107
+ // ─────────────────────────────────────────────────────────────────────────
108
+
109
+ private collect(): void {
110
+ const logger = getLogger();
111
+
112
+ // ── Event Loop Lag ────────────────────────────────────────────────────
113
+ const p50 = this.histogram.percentile(50) / 1e6; // Convert nanoseconds to ms
114
+ const p95 = this.histogram.percentile(95) / 1e6;
115
+ const p99 = this.histogram.percentile(99) / 1e6;
116
+ const maxLag = this.histogram.max / 1e6;
117
+
118
+ this.metrics.updateEventLoopMetrics({ p50, p95, p99, max: maxLag });
119
+ this.histogram.reset();
120
+
121
+ // ── Event Loop Thresholds ─────────────────────────────────────────────
122
+ if (p99 > this.config.criticalLagMs) {
123
+ this.consecutiveCriticalLag++;
124
+
125
+ this.bus.emit("eventloop:lag-critical", {
126
+ data: { p50, p95, p99, max: maxLag, consecutive: this.consecutiveCriticalLag },
127
+ });
128
+
129
+ logger.error("Event loop CRITICAL lag detected", {
130
+ p99: `${p99.toFixed(1)}ms`,
131
+ threshold: `${this.config.criticalLagMs}ms`,
132
+ consecutive: this.consecutiveCriticalLag,
133
+ });
134
+
135
+ // After 2 consecutive critical readings, trigger a proactive restart
136
+ if (this.consecutiveCriticalLag >= 2 && !this.restartRequested) {
137
+ this.restartRequested = true;
138
+ logger.error("Requesting graceful restart due to sustained critical event loop lag");
139
+ this.supervisorRef?.requestGracefulRestart(
140
+ `worker-${process.pid}`,
141
+ "critical-event-loop-lag"
142
+ );
143
+ // Reset after request
144
+ setTimeout(() => { this.restartRequested = false; }, 30_000).unref();
145
+ }
146
+ } else if (p99 > this.config.eventLoopLagMs) {
147
+ this.consecutiveCriticalLag = 0;
148
+
149
+ this.bus.emit("eventloop:lag-warning", {
150
+ data: { p50, p95, p99, max: maxLag },
151
+ });
152
+
153
+ logger.warn("Event loop lag elevated", {
154
+ p99: `${p99.toFixed(1)}ms`,
155
+ threshold: `${this.config.eventLoopLagMs}ms`,
156
+ });
157
+ } else {
158
+ this.consecutiveCriticalLag = 0;
159
+ }
160
+
161
+ // ── CPU Usage ────────────────────────────────────────────────────────
162
+ this.metrics.updateCpuMetrics();
163
+ const snapshot = this.metrics.getSnapshot();
164
+ const totalCpu = snapshot.cpu.percentUser + snapshot.cpu.percentSystem;
165
+
166
+ if (totalCpu > this.config.cpuCriticalPercent) {
167
+ this.consecutiveCriticalCpu++;
168
+
169
+ this.bus.emit("cpu:critical", {
170
+ data: { totalCpu, user: snapshot.cpu.percentUser, system: snapshot.cpu.percentSystem },
171
+ });
172
+
173
+ if (this.consecutiveCriticalCpu >= 3) {
174
+ logger.error("Sustained critical CPU usage", {
175
+ cpu: `${totalCpu.toFixed(1)}%`,
176
+ consecutive: this.consecutiveCriticalCpu,
177
+ });
178
+ }
179
+ } else if (totalCpu > this.config.cpuWarningPercent) {
180
+ this.consecutiveCriticalCpu = 0;
181
+
182
+ this.bus.emit("cpu:warning", {
183
+ data: { totalCpu },
184
+ });
185
+
186
+ logger.warn("High CPU usage", { cpu: `${totalCpu.toFixed(1)}%` });
187
+ } else {
188
+ this.consecutiveCriticalCpu = 0;
189
+ }
190
+ }
191
+ }