@@ -33,36 +33,4 @@ function scheduleNews() {
|
||||
console.log('[cron] 已注册: 每天 03:00 拉取新闻 + 跨源报警检测')
|
||||
}
|
||||
|
||||
// 每 2 分钟: 拉取 komari 节点 + 服务自检
|
||||
function scheduleMonitors() {
|
||||
cron.schedule('*/2 * * * *', async () => {
|
||||
try {
|
||||
const k = require('./komari-monitor.cjs')
|
||||
const r = await k.runOnce()
|
||||
console.log('[cron] komari: nodes=' + r.nodes + ' snapshots=' + r.snapshots + ' history=' + r.history + ' failed=' + r.failed)
|
||||
} catch (e) { console.error('[cron] komari 任务失败:', e.message) }
|
||||
try {
|
||||
const h = require('./health-check.cjs')
|
||||
const arr = await h.runOnce()
|
||||
const down = arr.filter(x => x.status !== 'up').map(x => x.name)
|
||||
console.log('[cron] health: ' + arr.length + ' 项, down=' + (down.length ? down.join(',') : '无'))
|
||||
} catch (e) { console.error('[cron] health 任务失败:', e.message) }
|
||||
})
|
||||
console.log('[cron] 已注册: 每 2 分钟 komari + 服务自检')
|
||||
}
|
||||
|
||||
// 每小时: 清理 24h 前的 komari 历史
|
||||
function scheduleMonitorCleanup() {
|
||||
cron.schedule('0 * * * *', async () => {
|
||||
try {
|
||||
const k = require('./komari-monitor.cjs')
|
||||
const n = await k.cleanupOld()
|
||||
console.log('[cron] 监控历史清理: 删除 ' + n + ' 条')
|
||||
} catch (e) { console.error('[cron] 监控清理失败:', e.message) }
|
||||
})
|
||||
console.log('[cron] 已注册: 每小时清理 24h 前监控历史')
|
||||
}
|
||||
|
||||
scheduleNews()
|
||||
scheduleMonitors()
|
||||
scheduleMonitorCleanup()
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// ===== 已停用 =====
|
||||
// 服务自检的定时任务已从 server/cron.js 移除 (服务监控暂留空)。
|
||||
// 如恢复服务监控, 重新在 cron.js 注册 runOnce 并复用本文件。
|
||||
// 服务健康检查: 探测本应用各关键端点, 写入 ServiceCheck 表
|
||||
const mysql = require('mysql2/promise')
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import dataRouter from './routes/data.js'
|
||||
import newsRouter from './routes/news.js'
|
||||
import alertsRouter from './routes/alerts.js'
|
||||
import articlesRouter from './routes/articles.js'
|
||||
import monitorsRouter from './routes/monitors.js'
|
||||
|
||||
const PORT = Number(process.env.PORT) || 3001
|
||||
const DIST_DIR = path.join(process.cwd(), 'dist')
|
||||
@@ -26,6 +27,7 @@ app.use('/api', dataRouter)
|
||||
app.use('/api', newsRouter)
|
||||
app.use('/api', alertsRouter)
|
||||
app.use('/api', articlesRouter)
|
||||
app.use('/api', monitorsRouter)
|
||||
|
||||
// 静态资源 (Vite build 产物)
|
||||
if (HAS_DIST) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// komari 实时监控客户端: 公开 API, 无鉴权, 请求时实时获取, 不落库
|
||||
// GET /api/nodes 节点列表
|
||||
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
|
||||
// GET /api/records/load?uuid=... 1 分钟粒度历史 (komari 保留 24h)
|
||||
|
||||
const BASE = process.env.KOMARI_BASE || 'https://komari.honor3.com'
|
||||
const TIMEOUT = 15000
|
||||
|
||||
async function jget(path) {
|
||||
const ctrl = new AbortController()
|
||||
const timer = setTimeout(() => ctrl.abort(), TIMEOUT)
|
||||
try {
|
||||
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
|
||||
} finally {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
// 节点列表 + 每节点最新实时快照 (并行拉取, 单节点失败不拖垮整体)
|
||||
export async function fetchServers() {
|
||||
const nodes = await jget('/api/nodes')
|
||||
if (!Array.isArray(nodes)) return []
|
||||
const snaps = await Promise.all(
|
||||
nodes
|
||||
.filter((n) => n.uuid)
|
||||
.map(async (n) => {
|
||||
try {
|
||||
const recent = await jget('/api/recent/' + n.uuid)
|
||||
return { node: n, snap: Array.isArray(recent) && recent.length ? recent[recent.length - 1] : null }
|
||||
} catch (e) {
|
||||
console.error('[komari] 节点 ' + n.uuid + ' 快照失败:', e.message)
|
||||
return { node: n, snap: null }
|
||||
}
|
||||
}),
|
||||
)
|
||||
return snaps.map(({ node: n, snap }) => {
|
||||
const uuid = n.uuid
|
||||
return {
|
||||
uuid,
|
||||
name: n.name || uuid,
|
||||
cpuName: n.cpu_name || '',
|
||||
os: n.os || '',
|
||||
region: n.region || '',
|
||||
memTotal: snap ? Number(snap.ram?.total || 0) : Number(n.mem_total || 0),
|
||||
diskTotal: snap ? Number(snap.disk?.total || 0) : Number(n.disk_total || 0),
|
||||
cpuCores: Number(n.cpu_cores || 0),
|
||||
cpuUsage: snap ? Number(snap.cpu?.usage || 0) : 0,
|
||||
ramUsed: snap ? Number(snap.ram?.used || 0) : 0,
|
||||
swapUsed: snap ? Number(snap.swap?.used || 0) : 0,
|
||||
load1: snap ? Number(snap.load?.load1 || 0) : 0,
|
||||
load5: snap ? Number(snap.load?.load5 || 0) : 0,
|
||||
load15: snap ? Number(snap.load?.load15 || 0) : 0,
|
||||
diskUsed: snap ? Number(snap.disk?.used || 0) : 0,
|
||||
netIn: snap ? Number(snap.network?.down || 0) : 0,
|
||||
netOut: snap ? Number(snap.network?.up || 0) : 0,
|
||||
uptime: snap ? Number(snap.uptime || 0) : 0,
|
||||
process: snap ? Number(snap.process || 0) : 0,
|
||||
connections: snap ? Number(snap.connections?.tcp || 0) : 0,
|
||||
status: snap ? 'online' : 'unknown',
|
||||
recordedAt: snap && snap.updated_at ? new Date(snap.updated_at).toISOString() : new Date().toISOString(),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 节点时序: komari 保留最近 24h, 1 分钟粒度
|
||||
// 节点未开启 record 或已下线 (komari 404) 时返回空列表, 不向上抛 500
|
||||
export async function fetchHistory(uuid) {
|
||||
let data
|
||||
try {
|
||||
data = await jget('/api/records/load?uuid=' + encodeURIComponent(uuid))
|
||||
} catch (e) {
|
||||
console.error('[komari] ' + uuid + ' history 获取失败:', e.message)
|
||||
return []
|
||||
}
|
||||
const records = data && Array.isArray(data.records) ? data.records : []
|
||||
return records
|
||||
.map((r) => ({
|
||||
period: new Date(r.time).toISOString(),
|
||||
cpu: Number(r.cpu || 0),
|
||||
ram: Number(r.ram || 0),
|
||||
load: Number(r.load || 0),
|
||||
disk: Number(r.disk || 0),
|
||||
// net_in/net_out 为 komari 原始值 (字节口径), 前端展示时再按需换算
|
||||
netIn: Number(r.net_in || 0),
|
||||
netOut: Number(r.net_out || 0),
|
||||
process: Number(r.process || 0),
|
||||
connections: Number(r.connections || 0),
|
||||
}))
|
||||
.filter((r) => !isNaN(Date.parse(r.period)))
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
// ===== 已停用 =====
|
||||
// 监控数据不再落库 (用户要求实时从 komari 获取), 本脚本的定时入库调用已从 server/cron.js 移除。
|
||||
// 如需恢复"数据库记录监控数据"模式, 重新在 cron.js 注册 runOnce/cleanupOld 即可。
|
||||
// komari 服务器监控拉取: 公开 API (无需 key)
|
||||
// GET /api/nodes 节点列表
|
||||
// GET /api/recent/{uuid} 最近实时状态 (数组, 多个时间点)
|
||||
|
||||
@@ -1,35 +1,28 @@
|
||||
import { Router } from 'express'
|
||||
import { prisma } from '../db.js'
|
||||
import { authRequired } from '../middleware.js'
|
||||
import { fetchServers, fetchHistory } from '../komari-client.js'
|
||||
|
||||
const router = Router()
|
||||
router.use(authRequired)
|
||||
|
||||
// 服务器节点列表: 元数据 + 最新快照
|
||||
// 服务器节点列表: 实时从 komari 拉取 (不落库)
|
||||
router.get('/servers', async (_req, res, next) => {
|
||||
try {
|
||||
const rows = await prisma.serverNode.findMany({
|
||||
orderBy: { name: 'asc' },
|
||||
})
|
||||
res.json({ items: rows })
|
||||
const items = await fetchServers()
|
||||
res.json({ items })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// 节点时序: ?uuid=...&hours=24
|
||||
// 节点时序: komari 保留最近 24h, 1 分钟粒度 (不落库)
|
||||
router.get('/servers/:uuid/history', async (req, res, next) => {
|
||||
try {
|
||||
const hours = Math.min(Math.max(Number(req.query.hours) || 24, 1), 168)
|
||||
const since = new Date(Date.now() - hours * 3600 * 1000)
|
||||
const rows = await prisma.serverNodeHistory.findMany({
|
||||
where: { uuid: req.params.uuid, period: { gte: since } },
|
||||
orderBy: { period: 'asc' },
|
||||
take: 2000,
|
||||
})
|
||||
res.json({ items: rows })
|
||||
const items = await fetchHistory(req.params.uuid)
|
||||
res.json({ items })
|
||||
} catch (e) { next(e) }
|
||||
})
|
||||
|
||||
// 服务健康列表
|
||||
// 服务健康列表 (服务监控暂留空, 接口保留)
|
||||
router.get('/services', async (_req, res, next) => {
|
||||
try {
|
||||
const rows = await prisma.serviceCheck.findMany({
|
||||
|
||||
Reference in New Issue
Block a user