collect-system-metrics.js
1 #!/usr/bin/env node 2 /** 3 * System Metrics Collection Script 4 * 5 * Collects CPU, disk I/O, and memory utilization every minute and stores in database. 6 * Designed to run as a cron job for correlating resource usage with cron job execution. 7 * 8 * Usage: node scripts/collect-system-metrics.js 9 */ 10 11 import { createDatabaseConnection } from '../src/utils/db.js'; 12 import os from 'os'; 13 import fs from 'fs'; 14 import { promisify } from 'util'; 15 16 const readFile = promisify(fs.readFile); 17 const dbPath = process.env.DATABASE_PATH || './db/sites.db'; 18 19 /** 20 * Get CPU usage percentage over a 1-second sample period 21 * @returns {Promise<number>} CPU usage percentage (0-100) 22 */ 23 async function getCpuUsage() { 24 const cpus = os.cpus(); 25 26 // Get initial CPU times 27 const startTimes = cpus.map(cpu => { 28 const { times } = cpu; 29 return { 30 idle: times.idle, 31 total: times.user + times.nice + times.sys + times.irq + times.idle, 32 }; 33 }); 34 35 // Wait 1 second 36 await new Promise(resolve => setTimeout(resolve, 1000)); 37 38 // Get CPU times after 1 second 39 const endCpus = os.cpus(); 40 const endTimes = endCpus.map(cpu => { 41 const { times } = cpu; 42 return { 43 idle: times.idle, 44 total: times.user + times.nice + times.sys + times.irq + times.idle, 45 }; 46 }); 47 48 // Calculate average CPU usage across all cores 49 let totalIdle = 0; 50 let totalTick = 0; 51 52 for (let i = 0; i < startTimes.length; i++) { 53 const startCpu = startTimes[i]; 54 const endCpu = endTimes[i]; 55 56 const idleDiff = endCpu.idle - startCpu.idle; 57 const totalDiff = endCpu.total - startCpu.total; 58 59 totalIdle += idleDiff; 60 totalTick += totalDiff; 61 } 62 63 const cpuPercent = 100 - (100 * totalIdle) / totalTick; 64 return Math.round(cpuPercent * 10) / 10; // Round to 1 decimal place 65 } 66 67 /** 68 * Get memory usage percentage 69 * @returns {number} Memory usage percentage (0-100) 70 */ 71 function getMemoryUsage() { 72 const totalMem = os.totalmem(); 73 const freeMem = os.freemem(); 74 const usedMem = totalMem - freeMem; 75 const memPercent = (usedMem / totalMem) * 100; 76 return Math.round(memPercent * 10) / 10; // Round to 1 decimal place 77 } 78 79 /** 80 * Get disk I/O statistics (Linux only) 81 * @returns {Promise<{readMBps: number, writeMBps: number}>} Disk read/write in MB/s 82 */ 83 async function getDiskIO() { 84 try { 85 // Read /proc/diskstats for disk I/O counters 86 // Format: major minor name reads read_merges read_sectors read_ticks writes write_merges write_sectors write_ticks ... 87 const diskstats = await readFile('/proc/diskstats', 'utf-8'); 88 const lines = diskstats.trim().split('\n'); 89 90 // Track previous stats for rate calculation 91 const prevStats = getDiskIO.prevStats || {}; 92 const prevTime = getDiskIO.prevTime || Date.now(); 93 const currentTime = Date.now(); 94 const timeDiff = (currentTime - prevTime) / 1000; // seconds 95 96 // Sum up all disk activity (filter to physical disks, not partitions) 97 let totalReadSectors = 0; 98 let totalWriteSectors = 0; 99 100 for (const line of lines) { 101 const parts = line.trim().split(/\s+/); 102 const deviceName = parts[2]; 103 104 // Skip partition names (sda1, nvme0n1p1) and loop devices 105 // Focus on main disks (sda, nvme0n1, vda, etc.) 106 if ( 107 deviceName.match(/\d$/) || 108 deviceName.startsWith('loop') || 109 deviceName.startsWith('ram') 110 ) { 111 continue; 112 } 113 114 const readSectors = parseInt(parts[5], 10) || 0; // sectors read 115 const writeSectors = parseInt(parts[9], 10) || 0; // sectors written 116 117 totalReadSectors += readSectors; 118 totalWriteSectors += writeSectors; 119 } 120 121 // Calculate rate if we have previous stats 122 let readMBps = 0; 123 let writeMBps = 0; 124 125 if (prevStats.readSectors !== undefined && timeDiff > 0) { 126 const readSectorsDiff = totalReadSectors - prevStats.readSectors; 127 const writeSectorsDiff = totalWriteSectors - prevStats.writeSectors; 128 129 // Convert sectors to MB (sector = 512 bytes) 130 readMBps = (readSectorsDiff * 512) / (1024 * 1024) / timeDiff; 131 writeMBps = (writeSectorsDiff * 512) / (1024 * 1024) / timeDiff; 132 133 readMBps = Math.max(0, Math.round(readMBps * 100) / 100); // Round to 2 decimals, ensure non-negative 134 writeMBps = Math.max(0, Math.round(writeMBps * 100) / 100); 135 } 136 137 // Store current stats for next iteration 138 getDiskIO.prevStats = { 139 readSectors: totalReadSectors, 140 writeSectors: totalWriteSectors, 141 }; 142 getDiskIO.prevTime = currentTime; 143 144 return { readMBps, writeMBps }; 145 } catch (err) { 146 // If /proc/diskstats is not available (non-Linux), return 0 147 return { readMBps: 0, writeMBps: 0 }; 148 } 149 } 150 151 /** 152 * Collect and record system metrics 153 */ 154 async function collectMetrics() { 155 try { 156 console.log('Collecting system metrics...'); 157 158 // Collect all metrics 159 const [cpuPercent, memoryPercent, diskIO] = await Promise.all([ 160 getCpuUsage(), 161 Promise.resolve(getMemoryUsage()), 162 getDiskIO(), 163 ]); 164 165 console.log(`CPU: ${cpuPercent}%`); 166 console.log(`Memory: ${memoryPercent}%`); 167 console.log(`Disk Read: ${diskIO.readMBps} MB/s`); 168 console.log(`Disk Write: ${diskIO.writeMBps} MB/s`); 169 170 // Store in database 171 const db = createDatabaseConnection(dbPath); 172 173 try { 174 const insert = db.prepare(` 175 INSERT INTO tel.system_metrics (cpu_percent, disk_read_mb, disk_write_mb, memory_percent, recorded_at) 176 VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) 177 `); 178 179 insert.run(cpuPercent, diskIO.readMBps, diskIO.writeMBps, memoryPercent); 180 181 console.log('✓ Metrics recorded successfully'); 182 183 // Clean up old metrics (keep last 7 days) 184 const cleanup = db.prepare( 185 `DELETE FROM tel.system_metrics WHERE recorded_at < datetime('now', '-7 days')` 186 ); 187 const result = cleanup.run(); 188 189 if (result.changes > 0) { 190 console.log(`✓ Cleaned up ${result.changes} old metrics`); 191 } 192 } finally { 193 db.close(); 194 } 195 } catch (err) { 196 console.error('Error collecting metrics:', err.message); 197 process.exit(1); 198 } 199 } 200 201 // Run collection 202 collectMetrics();