/ scripts / collect-system-metrics.js
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();