Add server status monitoring endpoint
- Add status.service.js with CPU, RAM, Disk, Backend stats - RAM calculation matches htop (reads /proc/meminfo) - Includes uploads folder size, DB table count - Returns both system and backend (process) uptime 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import * as adminService from '../services/admin.service.js';
|
import * as adminService from '../services/admin.service.js';
|
||||||
|
import * as statusService from '../services/status.service.js';
|
||||||
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
|
import { logUserCreation, logRoleChange } from '../services/audit.service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,3 +143,20 @@ export const deleteUser = async (req, res, next) => {
|
|||||||
return next(error);
|
return next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server status (admin only)
|
||||||
|
* GET /api/admin/server-status
|
||||||
|
*/
|
||||||
|
export const getServerStatus = async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const status = await statusService.getServerStatus();
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: status,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -50,4 +50,11 @@ router.delete(
|
|||||||
adminController.deleteUser
|
adminController.deleteUser
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server status
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Server status (CPU, RAM, Disk, Network, Uptime)
|
||||||
|
router.get('/server-status', adminController.getServerStatus);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
186
src/services/status.service.js
Normal file
186
src/services/status.service.js
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
import os from 'os';
|
||||||
|
import path from 'path';
|
||||||
|
import { execSync } from 'child_process';
|
||||||
|
import { pool } from '../config/database.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get CPU usage percentage
|
||||||
|
*/
|
||||||
|
const getCpuUsage = () => {
|
||||||
|
const cpus = os.cpus();
|
||||||
|
let totalIdle = 0;
|
||||||
|
let totalTick = 0;
|
||||||
|
|
||||||
|
cpus.forEach((cpu) => {
|
||||||
|
for (const type in cpu.times) {
|
||||||
|
totalTick += cpu.times[type];
|
||||||
|
}
|
||||||
|
totalIdle += cpu.times.idle;
|
||||||
|
});
|
||||||
|
|
||||||
|
const idle = totalIdle / cpus.length;
|
||||||
|
const total = totalTick / cpus.length;
|
||||||
|
const percent = Math.round(((total - idle) / total) * 100);
|
||||||
|
|
||||||
|
return { percent };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get memory usage (Linux - reads /proc/meminfo for accurate values)
|
||||||
|
*/
|
||||||
|
const getMemoryUsage = () => {
|
||||||
|
try {
|
||||||
|
// Read /proc/meminfo for accurate memory stats (like htop)
|
||||||
|
const meminfo = execSync('cat /proc/meminfo', { encoding: 'utf8' });
|
||||||
|
const lines = meminfo.split('\n');
|
||||||
|
|
||||||
|
const getValue = (key) => {
|
||||||
|
const line = lines.find(l => l.startsWith(key));
|
||||||
|
if (!line) return 0;
|
||||||
|
const match = line.match(/(\d+)/);
|
||||||
|
return match ? parseInt(match[1]) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
const totalKB = getValue('MemTotal');
|
||||||
|
const freeKB = getValue('MemFree');
|
||||||
|
const buffersKB = getValue('Buffers');
|
||||||
|
const cachedKB = getValue('Cached');
|
||||||
|
const sreclaimableKB = getValue('SReclaimable');
|
||||||
|
|
||||||
|
// Available = Free + Buffers + Cached + SReclaimable (like htop calculates)
|
||||||
|
const availableKB = freeKB + buffersKB + cachedKB + sreclaimableKB;
|
||||||
|
const usedKB = totalKB - availableKB;
|
||||||
|
|
||||||
|
// Convert to GB
|
||||||
|
const totalGB = Math.round((totalKB / (1024 ** 2)) * 10) / 10;
|
||||||
|
const usedGB = Math.round((usedKB / (1024 ** 2)) * 10) / 10;
|
||||||
|
|
||||||
|
return {
|
||||||
|
used: usedGB,
|
||||||
|
total: totalGB,
|
||||||
|
unit: 'GB',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// Fallback to os module
|
||||||
|
const totalBytes = os.totalmem();
|
||||||
|
const freeBytes = os.freemem();
|
||||||
|
const usedBytes = totalBytes - freeBytes;
|
||||||
|
|
||||||
|
return {
|
||||||
|
used: Math.round((usedBytes / (1024 ** 3)) * 10) / 10,
|
||||||
|
total: Math.round((totalBytes / (1024 ** 3)) * 10) / 10,
|
||||||
|
unit: 'GB',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get disk usage (Linux/macOS)
|
||||||
|
*/
|
||||||
|
const getDiskUsage = () => {
|
||||||
|
try {
|
||||||
|
const output = execSync('df -BG / | tail -1', { encoding: 'utf8' });
|
||||||
|
const parts = output.trim().split(/\s+/);
|
||||||
|
|
||||||
|
const totalGB = parseInt(parts[1]) || 0;
|
||||||
|
const usedGB = parseInt(parts[2]) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
used: usedGB,
|
||||||
|
total: totalGB,
|
||||||
|
unit: 'GB',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { used: 0, total: 0, unit: 'GB' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Node.js backend process stats (CPU + RAM)
|
||||||
|
*/
|
||||||
|
const getBackendStats = () => {
|
||||||
|
const memUsage = process.memoryUsage();
|
||||||
|
|
||||||
|
// Get CPU usage for this process
|
||||||
|
const cpuUsage = process.cpuUsage();
|
||||||
|
const cpuPercent = Math.round(((cpuUsage.user + cpuUsage.system) / 1000000) * 100) / 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
memoryMB: Math.round((memUsage.rss / (1024 ** 2)) * 10) / 10,
|
||||||
|
heapMB: Math.round((memUsage.heapUsed / (1024 ** 2)) * 10) / 10,
|
||||||
|
cpuTime: cpuPercent, // CPU time in seconds
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get uploads directory size
|
||||||
|
*/
|
||||||
|
const getUploadsSize = () => {
|
||||||
|
try {
|
||||||
|
const uploadsPath = path.join(process.cwd(), 'uploads');
|
||||||
|
const output = execSync(`du -sm "${uploadsPath}" 2>/dev/null || echo "0"`, { encoding: 'utf8' });
|
||||||
|
const sizeMB = parseInt(output.split('\t')[0]) || 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sizeMB,
|
||||||
|
unit: 'MB',
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return { sizeMB: 0, unit: 'MB' };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get database table count
|
||||||
|
*/
|
||||||
|
const getTableCount = async () => {
|
||||||
|
try {
|
||||||
|
const result = await pool.query(`
|
||||||
|
SELECT COUNT(*) as count
|
||||||
|
FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_type = 'BASE TABLE'
|
||||||
|
`);
|
||||||
|
return parseInt(result.rows[0]?.count) || 0;
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get system uptime in seconds
|
||||||
|
*/
|
||||||
|
const getSystemUptime = () => {
|
||||||
|
return Math.floor(os.uptime());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Node.js process uptime in seconds
|
||||||
|
*/
|
||||||
|
const getProcessUptime = () => {
|
||||||
|
return Math.floor(process.uptime());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get complete server status
|
||||||
|
*/
|
||||||
|
export const getServerStatus = async () => {
|
||||||
|
const tableCount = await getTableCount();
|
||||||
|
|
||||||
|
return {
|
||||||
|
cpu: getCpuUsage(),
|
||||||
|
memory: getMemoryUsage(),
|
||||||
|
disk: getDiskUsage(),
|
||||||
|
backend: getBackendStats(),
|
||||||
|
uploads: getUploadsSize(),
|
||||||
|
database: {
|
||||||
|
tableCount,
|
||||||
|
},
|
||||||
|
systemUptimeSec: getSystemUptime(),
|
||||||
|
backendUptimeSec: getProcessUptime(),
|
||||||
|
hostname: os.hostname(),
|
||||||
|
platform: os.platform(),
|
||||||
|
nodeVersion: process.version,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user