Files
open-isp/services/monitoringService.js
T
oxpoe eaf05f2811 OPEN-ISP v0.5 (pickle-lime) — rilis open-source perdana
Aplikasi billing & manajemen ISP (fiber optic) — PWA pelanggan, portal teknisi & kolektor,
admin panel lengkap, multi-vendor OLT, RADIUS, WhatsApp/Telegram, pembayaran QRIS.

Kredit & lisensi (ISC):
- Base kode & repo: alijayanet/billing-rtrw oleh Alijaya Net — terima kasih banyak
  atas base kode dan repo yang bermanfaat.
- Add-on: GoWA, LibreSpeed/OpenSpeedTest, Uptime Kuma, notification-forwarder.
- Dikembangkan menjadi OPEN-ISP oleh oxpoe — https://github.com/oxpoe/open-isp
2026-10-09 05:51:21 +07:00

423 lines
11 KiB
JavaScript

/**
* Service: Performance Monitoring System
* Memantau kesehatan sistem dan performa aplikasi
*/
const os = require('os');
const fs = require('fs');
const path = require('path');
const { logger } = require('../config/logger');
// Store metrics history
const metricsHistory = new Map();
const MAX_HISTORY_SIZE = 100;
// Track CPU time deltas for accurate percentage calculation
let previousCpuTimes = null;
function getCpuUsagePercent() {
const cpus = os.cpus();
if (!cpus || cpus.length === 0) return '0.00';
let user = 0, nice = 0, sys = 0, idle = 0, irq = 0;
for (const cpu of cpus) {
user += cpu.times.user;
nice += cpu.times.nice;
sys += cpu.times.sys;
idle += cpu.times.idle;
irq += cpu.times.irq;
}
const total = user + nice + sys + idle + irq;
if (!previousCpuTimes) {
previousCpuTimes = { user, nice, sys, idle, irq, total };
return '5.00'; // Default safe initial value
}
const totalDelta = total - previousCpuTimes.total;
const idleDelta = idle - previousCpuTimes.idle;
previousCpuTimes = { user, nice, sys, idle, irq, total };
if (totalDelta <= 0) return '0.00';
const usage = Math.min(100, Math.max(0, ((totalDelta - idleDelta) / totalDelta) * 100));
return usage.toFixed(2);
}
/**
* Get system metrics
*/
function getSystemMetrics() {
const cpus = os.cpus();
const totalMemory = os.totalmem();
const freeMemory = os.freemem();
const usedMemory = totalMemory - freeMemory;
// Calculate real CPU usage percentage
const cpuPercent = getCpuUsagePercent();
// Calculate memory usage percentage
const memoryPercent = (usedMemory / totalMemory) * 100;
// Get uptime
const uptime = process.uptime();
const uptimeHours = Math.floor(uptime / 3600);
const uptimeMinutes = Math.floor((uptime % 3600) / 60);
const uptimeSeconds = Math.floor(uptime % 60);
// Get load average (Fallback for Windows where loadavg is [0, 0, 0])
const loadAverage = os.loadavg();
let load1 = loadAverage[0];
let load5 = loadAverage[1];
let load15 = loadAverage[2];
if (os.platform() === 'win32' || load1 === 0) {
const estLoad = ((parseFloat(cpuPercent) / 100) * cpus.length).toFixed(2);
load1 = parseFloat(estLoad);
load5 = parseFloat(estLoad);
load15 = parseFloat(estLoad);
}
return {
timestamp: new Date().toISOString(),
cpu: {
usage: cpuPercent,
cores: cpus.length,
model: cpus[0] ? cpus[0].model : 'CPU',
speed: cpus[0] ? cpus[0].speed : 0
},
memory: {
total: formatBytes(totalMemory),
free: formatBytes(freeMemory),
used: formatBytes(usedMemory),
percentage: memoryPercent.toFixed(2)
},
uptime: {
seconds: uptime,
formatted: `${uptimeHours}h ${uptimeMinutes}m ${uptimeSeconds}s`
},
loadAverage: {
'1min': load1.toFixed(2),
'5min': load5.toFixed(2),
'15min': load15.toFixed(2)
},
platform: os.platform(),
arch: os.arch(),
nodeVersion: process.version
};
}
/**
* Get application metrics
*/
function getAppMetrics() {
const memoryUsage = process.memoryUsage();
return {
pid: process.pid,
memory: {
rss: formatBytes(memoryUsage.rss),
heapTotal: formatBytes(memoryUsage.heapTotal),
heapUsed: formatBytes(memoryUsage.heapUsed),
external: formatBytes(memoryUsage.external),
arrayBuffers: formatBytes(memoryUsage.arrayBuffers)
},
uptime: process.uptime(),
timestamp: new Date().toISOString()
};
}
/**
* Get disk usage (cross-platform with statfsSync)
*/
function getDiskUsage() {
try {
const targetPath = os.platform() === 'win32' ? 'C:\\' : '/';
if (typeof fs.statfsSync === 'function') {
const stat = fs.statfsSync(targetPath);
const total = stat.bsize * stat.blocks;
const free = stat.bsize * stat.bfree;
const used = Math.max(0, total - free);
const percentage = total > 0 ? ((used / total) * 100).toFixed(2) + '%' : 'N/A';
return {
total: formatBytes(total),
free: formatBytes(free),
used: formatBytes(used),
percentage,
timestamp: new Date().toISOString()
};
}
// Unix fallback
if (os.platform() !== 'win32') {
const { execSync } = require('child_process');
const output = execSync(`df -h ${targetPath}`).toString();
const lines = output.split('\n');
if (lines.length > 1) {
const parts = lines[1].split(/\s+/);
return {
total: parts[1],
used: parts[2],
free: parts[3],
percentage: parts[4],
timestamp: new Date().toISOString()
};
}
}
return {
total: 'N/A',
free: 'N/A',
used: 'N/A',
percentage: 'N/A',
timestamp: new Date().toISOString()
};
} catch (e) {
logger.error(`[Monitoring] Failed to get disk usage: ${e.message}`);
return {
total: 'N/A',
free: 'N/A',
used: 'N/A',
percentage: 'N/A',
timestamp: new Date().toISOString()
};
}
}
/**
* Get database metrics
*/
function getDatabaseMetrics() {
try {
const db = require('../config/database');
const dbPath = path.join(__dirname, '../database/billing.db');
let statsSize = 0;
try {
const stats = fs.statSync(dbPath);
statsSize = stats.size;
} catch {}
// Get table counts
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();
const tableCounts = {};
for (const table of tables) {
try {
const count = db.prepare(`SELECT COUNT(*) as count FROM ${table.name}`).get();
tableCounts[table.name] = count.count;
} catch (e) {
tableCounts[table.name] = 'N/A';
}
}
return {
size: formatBytes(statsSize),
path: dbPath,
tables: tableCounts,
totalTables: tables.length,
timestamp: new Date().toISOString()
};
} catch (e) {
logger.error(`[Monitoring] Failed to get database metrics: ${e.message}`);
return {
size: 'N/A',
path: 'N/A',
tables: {},
totalTables: 0,
timestamp: new Date().toISOString()
};
}
}
/**
* Get all metrics
*/
function getAllMetrics() {
const system = getSystemMetrics();
const app = getAppMetrics();
const disk = getDiskUsage();
const database = getDatabaseMetrics();
const allMetrics = {
system,
app,
disk,
database,
timestamp: new Date().toISOString()
};
// Store in history
storeMetricsHistory(allMetrics);
return allMetrics;
}
/**
* Store metrics in history
*/
function storeMetricsHistory(metrics) {
const key = metrics.timestamp;
metricsHistory.set(key, metrics);
// Keep only the last MAX_HISTORY_SIZE entries
if (metricsHistory.size > MAX_HISTORY_SIZE) {
const oldestKey = metricsHistory.keys().next().value;
metricsHistory.delete(oldestKey);
}
}
/**
* Get metrics history
*/
function getMetricsHistory(limit = 10) {
if (metricsHistory.size < 2) {
getAllMetrics(); // Guarantee at least 1 sample if history is thin
}
const history = Array.from(metricsHistory.values())
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
.slice(0, limit);
return history;
}
/**
* Get health status
*/
function getHealthStatus() {
const metrics = getAllMetrics();
const issues = [];
const warnings = [];
// Check CPU usage
const cpuUsage = parseFloat(metrics.system.cpu.usage);
if (cpuUsage > 85) {
issues.push(`Penggunaan CPU tinggi: ${cpuUsage}%`);
} else if (cpuUsage > 65) {
warnings.push(`Penggunaan CPU agak tinggi: ${cpuUsage}%`);
}
// Check memory usage
const memoryUsage = parseFloat(metrics.system.memory.percentage);
if (memoryUsage > 85) {
issues.push(`Penggunaan RAM tinggi: ${memoryUsage}%`);
} else if (memoryUsage > 70) {
warnings.push(`Penggunaan RAM agak tinggi: ${memoryUsage}%`);
}
// Check load average
const load1 = parseFloat(metrics.system.loadAverage['1min']);
const cores = metrics.system.cpu.cores;
if (load1 > cores * 2) {
issues.push(`Beban sistem (Load Average) tinggi: ${load1} (${cores} cores)`);
} else if (load1 > cores) {
warnings.push(`Beban sistem (Load Average) agak tinggi: ${load1} (${cores} cores)`);
}
// Check disk usage
const diskPercentage = metrics.disk.percentage;
if (diskPercentage !== 'N/A') {
const diskPercent = parseFloat(diskPercentage.replace('%', ''));
if (diskPercent > 90) {
issues.push(`Kapasitas Penyimpanan (Disk) Hampir Penuh: ${diskPercentage}`);
} else if (diskPercent > 75) {
warnings.push(`Kapasitas Penyimpanan (Disk) Terpakai: ${diskPercentage}`);
}
}
// Determine overall health status
let status = 'healthy';
if (issues.length > 0) {
status = 'critical';
} else if (warnings.length > 0) {
status = 'warning';
}
return {
status,
issues,
warnings,
metrics,
timestamp: new Date().toISOString()
};
}
/**
* Format bytes to human readable format
*/
function formatBytes(bytes) {
if (typeof bytes !== 'number' || isNaN(bytes) || bytes <= 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
}
/**
* Get performance summary
*/
function getPerformanceSummary() {
const history = getMetricsHistory(10);
if (history.length === 0) {
return {
timestamp: new Date().toISOString(),
summary: 'No historical data available'
};
}
// Calculate averages
const avgCpu = history.reduce((sum, m) => sum + parseFloat(m.system.cpu.usage || 0), 0) / history.length;
const avgMemory = history.reduce((sum, m) => sum + parseFloat(m.system.memory.percentage || 0), 0) / history.length;
const avgLoad1 = history.reduce((sum, m) => sum + parseFloat(m.system.loadAverage['1min'] || 0), 0) / history.length;
// Find max values
const maxCpu = Math.max(...history.map(m => parseFloat(m.system.cpu.usage || 0)));
const maxMemory = Math.max(...history.map(m => parseFloat(m.system.memory.percentage || 0)));
const maxLoad1 = Math.max(...history.map(m => parseFloat(m.system.loadAverage['1min'] || 0)));
return {
timestamp: new Date().toISOString(),
period: {
start: history[history.length - 1].timestamp,
end: history[0].timestamp,
samples: history.length
},
averages: {
cpu: avgCpu.toFixed(2),
memory: avgMemory.toFixed(2),
load1: avgLoad1.toFixed(2)
},
maximums: {
cpu: maxCpu.toFixed(2),
memory: maxMemory.toFixed(2),
load1: maxLoad1.toFixed(2)
}
};
}
/**
* Clear metrics history
*/
function clearMetricsHistory() {
metricsHistory.clear();
logger.info('[Monitoring] Metrics history cleared');
}
// Populate initial sample on module load
try {
getAllMetrics();
} catch {}
module.exports = {
getSystemMetrics,
getAppMetrics,
getDiskUsage,
getDatabaseMetrics,
getAllMetrics,
getMetricsHistory,
getHealthStatus,
getPerformanceSummary,
clearMetricsHistory
};