#!/usr/bin/env node // Collect server metrics — called by cron every minute // Appends JSON lines to analytics.jsonl const { Rcon } = require("rcon-client"); const { execSync } = require("child_process"); const { appendFileSync, readFileSync, writeFileSync } = require("fs"); const ANALYTICS_FILE = "/home/minecraft/server/analytics.jsonl"; const RCON_HOST = "127.0.0.1"; const RCON_PORT = 25575; const RCON_PASS = "23991818cc169249f181436f2a29a013"; async function main() { // Check if server is running try { const active = execSync("systemctl is-active minecraft.service", { encoding: "utf8" }).trim(); if (active !== "active") process.exit(0); } catch { process.exit(0); } // Get MC PID let mcPid = ""; try { mcPid = execSync("pgrep -f 'java.*forge' | head -1", { encoding: "utf8" }).trim(); } catch {} let tps = 0; let playersOnline = 0; let players = []; // RCON queries try { const rcon = await Rcon.connect({ host: RCON_HOST, port: RCON_PORT, password: RCON_PASS }); // TPS try { const tpsRaw = await rcon.send("forge tps"); const match = tpsRaw.match(/Overall.*Mean TPS:\s*(\d+\.?\d*)/i) || tpsRaw.match(/Mean TPS:\s*(\d+\.?\d*)/); if (match) tps = parseFloat(match[1]); } catch {} // Players try { const listRaw = await rcon.send("list"); const countMatch = listRaw.match(/(\d+)\s+of/); if (countMatch) playersOnline = parseInt(countMatch[1]); if (playersOnline > 0) { const namesStr = listRaw.split(":").pop() || ""; players = namesStr.split(",").map(n => n.trim()).filter(Boolean); } } catch {} rcon.end(); } catch { // RCON not available } // RAM let ramUsedMB = 0; const ramTotalMB = 8192; if (mcPid) { try { const status = readFileSync(`/proc/${mcPid}/status`, "utf8"); const match = status.match(/VmRSS:\s+(\d+)/); if (match) ramUsedMB = Math.round(parseInt(match[1]) / 1024); } catch {} } // CPU (normalize to total system CPU — ps reports per-core) let cpuPercent = 0; const numCpus = parseInt(execSync("nproc", { encoding: "utf8" }).trim()) || 1; if (mcPid) { try { const cpu = execSync(`ps -p ${mcPid} -o %cpu --no-headers`, { encoding: "utf8" }).trim(); cpuPercent = (parseFloat(cpu) || 0) / numCpus; } catch {} } // Write const entry = { ts: new Date().toISOString(), tps: Math.round(tps * 10) / 10, ramUsedMB, ramTotalMB, cpuPercent: Math.round(cpuPercent * 10) / 10, playersOnline, players, }; appendFileSync(ANALYTICS_FILE, JSON.stringify(entry) + "\n"); // Prune to last 2880 lines (48h) try { const lines = readFileSync(ANALYTICS_FILE, "utf8").split("\n").filter(Boolean); if (lines.length > 3000) { writeFileSync(ANALYTICS_FILE, lines.slice(-2880).join("\n") + "\n"); } } catch {} } main().catch(() => process.exit(0));