121 lines
5.8 KiB
JavaScript
121 lines
5.8 KiB
JavaScript
// komari 服务器监控拉取: 公开 API (无需 key)
|
|
// GET /api/nodes 节点列表
|
|
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
|
|
// GET /api/records/load?uuid=... 1 分钟粒度历史 (保留 24h)
|
|
const mysql = require('mysql2/promise')
|
|
|
|
const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com'
|
|
const TIMEOUT = 20000
|
|
|
|
function parseUrl(url) {
|
|
const u = new URL(url)
|
|
return { host: u.hostname, port: Number(u.port) || 3306, user: decodeURIComponent(u.username), password: decodeURIComponent(u.password), database: u.pathname.replace(/^\//, '') }
|
|
}
|
|
|
|
async function jget(path) {
|
|
const ctrl = new AbortController()
|
|
setTimeout(() => ctrl.abort(), TIMEOUT)
|
|
const r = await fetch(BASE + path, { headers: { 'User-Agent': 'iboard-monitor/1.0' }, signal: ctrl.signal })
|
|
if (!r.ok) throw new Error('komari ' + path + ' HTTP ' + r.status)
|
|
const j = await r.json()
|
|
if (j.status && j.status !== 'success') throw new Error('komari ' + path + ' status: ' + j.status + ' ' + (j.message || ''))
|
|
return j.data
|
|
}
|
|
|
|
// 拉一次所有节点 + 最近状态 + 1 分钟历史, 入库
|
|
async function runOnce() {
|
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
|
const stats = { nodes: 0, snapshots: 0, history: 0, failed: 0 }
|
|
try {
|
|
const nodes = await jget('/api/nodes')
|
|
if (!Array.isArray(nodes)) throw new Error('nodes 格式异常')
|
|
stats.nodes = nodes.length
|
|
for (const n of nodes) {
|
|
const uuid = n.uuid
|
|
if (!uuid) continue
|
|
try {
|
|
const recent = await jget('/api/recent/' + uuid)
|
|
let snap = null
|
|
if (Array.isArray(recent) && recent.length) snap = recent[recent.length - 1]
|
|
const recordedAt = snap ? new Date(snap.updated_at) : new Date()
|
|
const ramTotal = snap && snap.ram ? Number(snap.ram.total) : Number(n.mem_total || 0)
|
|
const diskTotal = snap && snap.disk ? Number(snap.disk.total) : Number(n.disk_total || 0)
|
|
await conn.execute(
|
|
'INSERT INTO `ServerNode` (`uuid`,`name`,`cpuName`,`os`,`region`,`memTotal`,`diskTotal`,`cpuCores`,' +
|
|
'`cpuUsage`,`ramUsed`,`swapUsed`,`load1`,`load5`,`load15`,`diskUsed`,`netIn`,`netOut`,`uptime`,`process`,`connections`,`status`,`recordedAt`,`createdAt`,`updatedAt`) ' +
|
|
'VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW(3),NOW(3)) ' +
|
|
'ON DUPLICATE KEY UPDATE `name`=VALUES(`name`),`cpuName`=VALUES(`cpuName`),`os`=VALUES(`os`),`region`=VALUES(`region`),' +
|
|
'`memTotal`=VALUES(`memTotal`),`diskTotal`=VALUES(`diskTotal`),`cpuCores`=VALUES(`cpuCores`),' +
|
|
'`cpuUsage`=VALUES(`cpuUsage`),`ramUsed`=VALUES(`ramUsed`),`swapUsed`=VALUES(`swapUsed`),' +
|
|
'`load1`=VALUES(`load1`),`load5`=VALUES(`load5`),`load15`=VALUES(`load15`),`diskUsed`=VALUES(`diskUsed`),' +
|
|
'`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`uptime`=VALUES(`uptime`),' +
|
|
'`process`=VALUES(`process`),`connections`=VALUES(`connections`),`status`=VALUES(`status`),' +
|
|
'`recordedAt`=VALUES(`recordedAt`),`updatedAt`=NOW(3)',
|
|
[
|
|
uuid, n.name || '', n.cpu_name || '', n.os || '', n.region || '',
|
|
ramTotal, diskTotal, Number(n.cpu_cores || 0),
|
|
snap ? Number(snap.cpu?.usage || 0) : 0,
|
|
snap ? Number(snap.ram?.used || 0) : 0,
|
|
snap ? Number(snap.swap?.used || 0) : 0,
|
|
snap ? Number(snap.load?.load1 || 0) : 0,
|
|
snap ? Number(snap.load?.load5 || 0) : 0,
|
|
snap ? Number(snap.load?.load15 || 0) : 0,
|
|
snap ? Number(snap.disk?.used || 0) : 0,
|
|
snap ? Number(snap.network?.down || 0) : 0,
|
|
snap ? Number(snap.network?.up || 0) : 0,
|
|
snap ? Number(snap.uptime || 0) : 0,
|
|
snap ? Number(snap.process || 0) : 0,
|
|
snap ? Number(snap.connections?.tcp || 0) : 0,
|
|
snap ? 'online' : 'unknown',
|
|
recordedAt,
|
|
],
|
|
)
|
|
stats.snapshots++
|
|
|
|
let rec = null
|
|
try { rec = await jget('/api/records/load?uuid=' + uuid) } catch (e) { /* 节点未开启 record */ }
|
|
if (rec && Array.isArray(rec.records)) {
|
|
for (const r of rec.records) {
|
|
const period = new Date(r.time)
|
|
if (isNaN(period.getTime())) continue
|
|
const [res] = await conn.execute(
|
|
'INSERT INTO `ServerNodeHistory` (`uuid`,`period`,`cpu`,`ram`,`load`,`disk`,`netIn`,`netOut`,`process`,`connections`,`createdAt`) ' +
|
|
'VALUES (?,?,?,?,?,?,?,?,?,?,NOW(3)) ' +
|
|
'ON DUPLICATE KEY UPDATE `cpu`=VALUES(`cpu`),`ram`=VALUES(`ram`),`load`=VALUES(`load`),`disk`=VALUES(`disk`),' +
|
|
'`netIn`=VALUES(`netIn`),`netOut`=VALUES(`netOut`),`process`=VALUES(`process`),`connections`=VALUES(`connections`)',
|
|
[
|
|
uuid, period,
|
|
Number(r.cpu || 0), Number(r.ram || 0), Number(r.load || 0),
|
|
Number(r.disk || 0), Number(r.net_in || 0), Number(r.net_out || 0),
|
|
Number(r.process || 0), Number(r.connections || 0),
|
|
],
|
|
)
|
|
if (res.affectedRows === 1) stats.history++
|
|
}
|
|
}
|
|
} catch (e) {
|
|
stats.failed++
|
|
console.error('[komari] 节点 ' + uuid + ' 失败:', e.message)
|
|
}
|
|
}
|
|
} finally {
|
|
await conn.end()
|
|
}
|
|
return stats
|
|
}
|
|
|
|
// 清理 24h 之前的历史
|
|
async function cleanupOld() {
|
|
const conn = await mysql.createConnection(parseUrl(process.env.DATABASE_URL))
|
|
try {
|
|
const cutoff = new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
const [r] = await conn.execute('DELETE FROM `ServerNodeHistory` WHERE `period` < ?', [cutoff])
|
|
return r.affectedRows
|
|
} finally {
|
|
await conn.end()
|
|
}
|
|
}
|
|
|
|
module.exports = { runOnce, cleanupOld, BASE }
|
|
|