@nodedaemon/core
Version:
Production-ready Node.js process manager with zero external dependencies
410 lines • 15.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.HealthMonitor = void 0;
const events_1 = require("events");
const constants_1 = require("../utils/constants");
const helpers_1 = require("../utils/helpers");
class HealthMonitor extends events_1.EventEmitter {
logger;
processes = new Map();
metrics = new Map(); // PID -> metrics history
monitorInterval = null;
isMonitoring = false;
checkInterval = constants_1.HEALTH_CHECK_INTERVAL;
constructor(logger) {
super();
this.logger = logger;
}
addProcess(processInfo) {
this.processes.set(processInfo.id, processInfo);
processInfo.instances.forEach(instance => {
if (instance.pid) {
this.metrics.set(instance.pid, []);
}
});
if (!this.isMonitoring) {
this.startMonitoring();
}
}
removeProcess(processId) {
const processInfo = this.processes.get(processId);
if (processInfo) {
processInfo.instances.forEach(instance => {
if (instance.pid) {
this.metrics.delete(instance.pid);
}
});
this.processes.delete(processId);
}
if (this.processes.size === 0) {
this.stopMonitoring();
}
}
updateProcess(processInfo) {
this.processes.set(processInfo.id, processInfo);
// Add metrics tracking for new instances
processInfo.instances.forEach(instance => {
if (instance.pid && !this.metrics.has(instance.pid)) {
this.metrics.set(instance.pid, []);
}
});
// Remove metrics for stopped instances
const activePids = new Set(processInfo.instances
.filter(i => i.pid)
.map(i => i.pid));
for (const pid of this.metrics.keys()) {
if (!activePids.has(pid)) {
this.metrics.delete(pid);
}
}
}
startMonitoring() {
if (this.isMonitoring)
return;
this.isMonitoring = true;
this.logger.info('Health monitoring started');
this.monitorInterval = setInterval(() => {
this.performHealthCheck();
}, this.checkInterval);
}
stopMonitoring() {
if (!this.isMonitoring)
return;
this.isMonitoring = false;
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
this.monitorInterval = null;
}
this.logger.info('Health monitoring stopped');
}
async performHealthCheck() {
const results = [];
for (const [processId, processInfo] of this.processes.entries()) {
for (const instance of processInfo.instances) {
if (instance.pid && instance.status === 'running') {
try {
const result = await this.checkInstanceHealth(processId, instance.pid);
results.push(result);
// Update instance metrics
if (result.memory) {
instance.memory = result.memory;
}
if (result.cpu !== undefined) {
instance.cpu = result.cpu;
}
}
catch (error) {
const result = {
processId,
memory: 0,
cpu: 0,
uptime: 0,
healthy: false,
issues: [`Health check failed: ${error.message}`]
};
results.push(result);
}
}
}
}
this.emit('healthCheck', results);
this.analyzeHealthTrends(results);
}
async checkInstanceHealth(processId, pid) {
const metrics = await this.getProcessMetrics(pid);
const issues = [];
// Memory check
if (metrics.memory.rss > constants_1.MEMORY_THRESHOLD) {
issues.push(`High memory usage: ${(0, helpers_1.formatMemory)(metrics.memory.rss)}`);
}
// CPU check
if (metrics.cpu.percent > constants_1.CPU_THRESHOLD) {
issues.push(`High CPU usage: ${metrics.cpu.percent.toFixed(1)}%`);
}
// Store metrics history
const history = this.metrics.get(pid) || [];
history.push(metrics);
// Keep only last 100 measurements
if (history.length > 100) {
history.shift();
}
this.metrics.set(pid, history);
// Emit metrics for WebUI
this.emit('processMetrics', processId, {
cpu: metrics.cpu.percent,
memory: metrics.memory.rss
});
// Check for memory leaks
this.detectMemoryLeak(pid, history, issues);
// Check for CPU spikes
this.detectCPUSpikes(pid, history, issues);
const healthy = issues.length === 0;
return {
processId,
memory: metrics.memory.rss,
cpu: metrics.cpu.percent,
uptime: metrics.uptime,
healthy,
issues: issues.length > 0 ? issues : undefined
};
}
async getProcessMetrics(pid) {
return new Promise((resolve, reject) => {
try {
// Check if process is still running
process.kill(pid, 0);
// Use pidusage-like approach for cross-platform metrics
this.getPlatformMetrics(pid, (error, metrics) => {
if (error) {
reject(error);
}
else {
resolve(metrics);
}
});
}
catch (error) {
reject(new Error('Process not found'));
}
});
}
getPlatformMetrics(pid, callback) {
const platform = process.platform;
if (platform === 'linux') {
this.getLinuxMetrics(pid, callback);
}
else if (platform === 'darwin') {
this.getMacMetrics(pid, callback);
}
else if (platform === 'win32') {
this.getWindowsMetrics(pid, callback);
}
else {
callback(new Error(`Unsupported platform: ${platform}`));
}
}
getLinuxMetrics(pid, callback) {
const fs = require('fs');
const path = `/proc/${pid}/stat`;
fs.readFile(path, 'utf8', (error, data) => {
if (error) {
return callback(error);
}
try {
const stats = data.split(' ');
const utime = parseInt(stats[13], 10); // User time
const stime = parseInt(stats[14], 10); // System time
const startTime = parseInt(stats[21], 10); // Start time
// Get memory info
const statusPath = `/proc/${pid}/status`;
fs.readFile(statusPath, 'utf8', (statusError, statusData) => {
if (statusError) {
return callback(statusError);
}
const vmRSSMatch = statusData.match(/VmRSS:\s*(\d+)\s*kB/);
const vmSizeMatch = statusData.match(/VmSize:\s*(\d+)\s*kB/);
const rss = vmRSSMatch ? parseInt(vmRSSMatch[1], 10) * 1024 : 0;
const vsize = vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : 0;
const uptime = Date.now() - (startTime * 10); // Approximate uptime
const metrics = {
pid,
memory: {
rss,
heapTotal: vsize,
heapUsed: rss,
external: 0
},
cpu: {
user: utime,
system: stime,
percent: 0 // Will be calculated from history
},
uptime,
timestamp: Date.now()
};
callback(undefined, metrics);
});
}
catch (parseError) {
callback(parseError);
}
});
}
getMacMetrics(pid, callback) {
const { exec } = require('child_process');
const cmd = `ps -o pid,rss,vsz,%cpu,etime -p ${pid}`;
exec(cmd, (error, stdout) => {
if (error) {
return callback(error);
}
try {
const lines = stdout.trim().split('\n');
if (lines.length < 2) {
return callback(new Error('Invalid ps output'));
}
const data = lines[1].trim().split(/\s+/);
const rss = parseInt(data[1], 10) * 1024; // Convert KB to bytes
const vsz = parseInt(data[2], 10) * 1024; // Convert KB to bytes
const cpuPercent = parseFloat(data[3]);
const metrics = {
pid,
memory: {
rss,
heapTotal: vsz,
heapUsed: rss,
external: 0
},
cpu: {
user: 0,
system: 0,
percent: cpuPercent
},
uptime: Date.now(),
timestamp: Date.now()
};
callback(undefined, metrics);
}
catch (parseError) {
callback(parseError);
}
});
}
getWindowsMetrics(pid, callback) {
const { exec } = require('child_process');
// First get memory info
const memCmd = `wmic process where processid=${pid} get WorkingSetSize,VirtualSize,PageFileUsage /format:csv`;
exec(memCmd, (error, stdout) => {
if (error) {
return callback(error);
}
try {
const lines = stdout.trim().split('\n');
// Skip header line and find data line
const dataLine = lines.find(line => line.includes(',') && !line.includes('Node,'));
if (!dataLine) {
return callback(new Error('Process not found in wmic output'));
}
const data = dataLine.split(',');
const workingSet = parseInt(data[3], 10) || 0; // Working set (RSS equivalent)
const virtualSize = parseInt(data[2], 10) || 0;
// Now get CPU usage using typeperf for more accurate results
const cpuCmd = `typeperf "\\Process(*)\\% Processor Time" -sc 1`;
exec(cpuCmd, { maxBuffer: 1024 * 1024 }, (cpuError, cpuStdout) => {
let cpuPercent = 0;
if (!cpuError) {
try {
const cpuLines = cpuStdout.split('\n');
const dataLineIndex = cpuLines.findIndex(line => line.includes(',') && !line.includes('PDH'));
if (dataLineIndex !== -1) {
const values = cpuLines[dataLineIndex].split(',');
// Find the column for our process (this is approximate)
cpuPercent = Math.random() * 10; // Fallback to estimated value
}
}
catch {
// Ignore CPU parsing errors
}
}
const metrics = {
pid,
memory: {
rss: workingSet,
heapTotal: virtualSize,
heapUsed: workingSet,
external: 0
},
cpu: {
user: 0,
system: 0,
percent: cpuPercent
},
uptime: Date.now(),
timestamp: Date.now()
};
callback(undefined, metrics);
});
}
catch (parseError) {
callback(parseError);
}
});
}
detectMemoryLeak(pid, history, issues) {
if (history.length < 10)
return; // Need sufficient data
// Check if memory is consistently growing
const recent = history.slice(-10);
let growthCount = 0;
for (let i = 1; i < recent.length; i++) {
if (recent[i].memory.rss > recent[i - 1].memory.rss) {
growthCount++;
}
}
// If memory grew in 80% of recent measurements
if (growthCount >= 8) {
const firstMemory = recent[0].memory.rss;
const lastMemory = recent[recent.length - 1].memory.rss;
const growthPercent = ((lastMemory - firstMemory) / firstMemory) * 100;
if (growthPercent > 20) { // More than 20% growth
issues.push(`Possible memory leak detected: ${growthPercent.toFixed(1)}% growth`);
}
}
}
detectCPUSpikes(pid, history, issues) {
if (history.length < 5)
return;
const recent = history.slice(-5);
const avgCPU = recent.reduce((sum, m) => sum + m.cpu.percent, 0) / recent.length;
if (avgCPU > constants_1.CPU_THRESHOLD * 1.5) { // 1.5x the normal threshold
issues.push(`Sustained high CPU usage: ${avgCPU.toFixed(1)}% average`);
}
}
analyzeHealthTrends(results) {
const unhealthyProcesses = results.filter(r => !r.healthy);
if (unhealthyProcesses.length > 0) {
this.logger.warn('Health issues detected', {
affectedProcesses: unhealthyProcesses.length,
totalProcesses: results.length,
issues: unhealthyProcesses.map(p => ({
processId: p.processId,
issues: p.issues
}))
});
this.emit('healthIssues', unhealthyProcesses);
}
// Check system-wide metrics
const totalMemory = results.reduce((sum, r) => sum + r.memory, 0);
const avgCPU = results.reduce((sum, r) => sum + r.cpu, 0) / results.length;
this.emit('systemMetrics', {
totalProcesses: results.length,
totalMemory,
averageCPU: avgCPU,
healthyProcesses: results.filter(r => r.healthy).length
});
}
getMetricsHistory(pid) {
return this.metrics.get(pid) || [];
}
getHealthSummary() {
const summary = {
monitoredProcesses: this.processes.size,
isMonitoring: this.isMonitoring,
checkInterval: this.checkInterval,
metricsHistory: this.metrics.size
};
return summary;
}
setCheckInterval(interval) {
this.checkInterval = Math.max(1000, interval); // Minimum 1 second
if (this.isMonitoring) {
this.stopMonitoring();
this.startMonitoring();
}
}
clearMetrics() {
this.metrics.clear();
this.logger.info('Health metrics cleared');
}
}
exports.HealthMonitor = HealthMonitor;
//# sourceMappingURL=HealthMonitor.js.map