guanghulab/ops-agent/health-checker.js

414 lines
12 KiB
JavaScript
Raw Normal View History

/**
*
* 🔍 铸渊运维守卫 · 健康检查引擎
*
*
* 编号: ZY-OPS-HC-001
* 签发: 铸渊 · ICE-GL-ZY001
* 版权: 国作登字-2026-A-00037559
* 更新: 2026-04-23 · 修复 MCP 跨服务器检测 + 告警冷却
*
* 三层巡检:
* - 快速巡检(5分钟): 端口存活检测
* - 深度巡检(1小时): 内存/磁盘/PM2重启次数/错误日志
* - 全量体检(每天): 完整健康报告
*/
'use strict';
const http = require('http');
const { execSync } = require('child_process');
const os = require('os');
// ── 服务器 IP 配置(环境变量覆盖)─────────
const SVR_002_HOST = process.env.ZY_SVR_002_HOST || '127.0.0.1'; // 面孔服务器(本机)
const SVR_005_HOST = process.env.ZY_SVR_005_HOST || '127.0.0.1'; // 大脑服务器(需配置真实 IP
// ── 监控目标列表 ─────────────────────
const SERVICES = [
{ name: '铸渊主权服务器', host: SVR_002_HOST, port: 3800, path: '/api/health', critical: true, timeout: 8000 },
{ name: 'MCP大脑服务器', host: SVR_005_HOST, port: 3100, path: '/health', critical: true, timeout: 15000 },
{ name: 'GLADA自主开发Agent', host: SVR_002_HOST, port: 3900, path: '/api/glada/health', critical: false, timeout: 8000 },
{ name: '智库节点API', host: SVR_002_HOST, port: 4000, path: '/api/health', critical: false, timeout: 8000 },
{ name: '铸渊运维守卫', host: SVR_002_HOST, port: 3950, path: '/health', critical: false, timeout: 8000 },
];
const PM2_PROCESSES = [
'zhuyuan-server',
'zhuyuan-preview',
'novel-api',
'age-os-mcp',
'age-os-agents',
'glada-agent',
];
// ── 告警冷却机制(避免重复发邮件)───────
const alertCooldown = new Map(); // key: service name, value: { lastAlert: timestamp, count: number }
const ALERT_COOLDOWN_MS = 30 * 60 * 1000; // 30分钟内同一服务不重复告警
const MAX_ALERTS_PER_DAY = 6; // 每个服务每天最多6次告警
/**
* 检查是否应该发送告警冷却期内不重复发
* @param {string} serviceName
* @returns {boolean}
*/
function shouldAlert(serviceName) {
const now = Date.now();
const record = alertCooldown.get(serviceName);
if (!record) {
alertCooldown.set(serviceName, { lastAlert: now, count: 1, dayStart: now });
return true;
}
// 超过24小时重置计数
if (now - record.dayStart > 24 * 60 * 60 * 1000) {
alertCooldown.set(serviceName, { lastAlert: now, count: 1, dayStart: now });
return true;
}
// 每天超过最大次数,不再发
if (record.count >= MAX_ALERTS_PER_DAY) {
return false;
}
// 冷却期内不重复发
if (now - record.lastAlert < ALERT_COOLDOWN_MS) {
return false;
}
record.lastAlert = now;
record.count++;
return true;
}
/**
* 获取告警冷却状态调试用
*/
function getAlertCooldownStatus() {
const status = {};
for (const [name, record] of alertCooldown.entries()) {
status[name] = {
lastAlert: new Date(record.lastAlert).toISOString(),
alertsToday: record.count,
maxPerDay: MAX_ALERTS_PER_DAY,
cooldownRemaining: Math.max(0, ALERT_COOLDOWN_MS - (Date.now() - record.lastAlert))
};
}
return status;
}
// ── HTTP 健康检查 ────────────────────
function probeService(name, host, port, probePath, timeoutMs = 8000) {
return new Promise((resolve) => {
const start = Date.now();
const req = http.get({
hostname: host,
port,
path: probePath,
timeout: timeoutMs
}, (res) => {
let body = '';
res.on('data', (c) => { body += c; });
res.on('end', () => {
const latency = Date.now() - start;
let data = null;
try { data = JSON.parse(body); } catch { /* ignore */ }
resolve({
name,
host,
port,
status: 'online',
statusCode: res.statusCode,
latency,
data,
timestamp: new Date().toISOString()
});
});
});
req.on('error', (err) => {
resolve({
name,
host,
port,
status: 'offline',
statusCode: 0,
latency: Date.now() - start,
error: err.message,
timestamp: new Date().toISOString()
});
});
req.on('timeout', () => {
req.destroy();
resolve({
name,
host,
port,
status: 'timeout',
statusCode: 0,
latency: timeoutMs,
error: `连接超时(${timeoutMs}ms)`,
timestamp: new Date().toISOString()
});
});
});
}
// ── PM2 进程状态 ──────────────────
function getPM2Status() {
try {
const raw = execSync('pm2 jlist 2>/dev/null', { encoding: 'utf-8', timeout: 10000 });
const processes = JSON.parse(raw);
return processes.map(p => ({
name: p.name,
pm_id: p.pm_id,
status: p.pm2_env?.status || 'unknown',
restarts: p.pm2_env?.restart_time || 0,
uptime: p.pm2_env?.pm_uptime ? Date.now() - p.pm2_env.pm_uptime : 0,
memory: p.monit?.memory || 0,
cpu: p.monit?.cpu || 0,
pid: p.pid
}));
} catch (err) {
return [{ name: 'pm2', status: 'error', error: err.message }];
}
}
// ── 系统资源 ──────────────────────
function getSystemResources() {
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedPct = Math.round((1 - freeMem / totalMem) * 100);
let diskInfo = { total: 0, used: 0, available: 0, usePct: 0 };
try {
const dfOut = execSync('df -B1 / 2>/dev/null | tail -1', { encoding: 'utf-8', timeout: 5000 });
const parts = dfOut.trim().split(/\s+/);
if (parts.length >= 5) {
diskInfo = {
total: parseInt(parts[1]) || 0,
used: parseInt(parts[2]) || 0,
available: parseInt(parts[3]) || 0,
usePct: parseInt(parts[4]) || 0
};
}
} catch { /* ignore */ }
return {
memory: {
total_mb: Math.round(totalMem / 1024 / 1024),
free_mb: Math.round(freeMem / 1024 / 1024),
used_pct: usedPct
},
disk: {
total_gb: Math.round(diskInfo.total / 1024 / 1024 / 1024),
used_gb: Math.round(diskInfo.used / 1024 / 1024 / 1024),
available_gb: Math.round(diskInfo.available / 1024 / 1024 / 1024),
used_pct: diskInfo.usePct
},
load: os.loadavg(),
cpus: os.cpus().length,
uptime_hours: Math.round(os.uptime() / 3600),
hostname: os.hostname(),
platform: os.platform(),
node_version: process.version
};
}
// ── Nginx 状态 ─────────────────────
function getNginxStatus() {
try {
const status = execSync('systemctl is-active nginx 2>/dev/null', { encoding: 'utf-8', timeout: 5000 }).trim();
let configOk = false;
try {
execSync('nginx -t 2>&1', { encoding: 'utf-8', timeout: 5000 });
configOk = true;
} catch { /* config test failed */ }
return { status, configOk };
} catch {
return { status: 'unknown', configOk: false };
}
}
// ── 快速巡检5分钟一次────────────
async function quickCheck() {
const results = await Promise.all(
SERVICES.map(s => probeService(s.name, s.host, s.port, s.path, s.timeout))
);
const issues = [];
for (let i = 0; i < results.length; i++) {
const r = results[i];
const s = SERVICES[i];
if (r.status !== 'online') {
issues.push({
service: s.name,
host: s.host,
port: s.port,
status: r.status,
error: r.error,
critical: s.critical,
severity: s.critical ? 'high' : 'medium',
shouldNotify: shouldAlert(s.name)
});
}
}
return {
type: 'quick',
timestamp: new Date().toISOString(),
services: results,
issues,
// 只返回需要通知的问题(过滤掉冷却期内的)
notifiableIssues: issues.filter(i => i.shouldNotify),
healthy: issues.filter(i => i.critical).length === 0,
summary: issues.length === 0
? '✅ 所有服务正常运行'
: `⚠️ ${issues.length}个服务异常: ${issues.map(i => i.service).join(', ')}`
};
}
// ── 深度巡检1小时一次───────────
async function deepCheck() {
const quick = await quickCheck();
const pm2Status = getPM2Status();
const resources = getSystemResources();
const nginx = getNginxStatus();
const issues = [...quick.issues];
// 检查 PM2 进程异常
for (const proc of pm2Status) {
if (proc.status === 'errored' || proc.status === 'stopped') {
issues.push({
service: `PM2:${proc.name}`,
status: proc.status,
error: `进程${proc.status === 'errored' ? '出错' : '已停止'}`,
critical: PM2_PROCESSES.includes(proc.name),
severity: 'high',
shouldNotify: shouldAlert(`PM2:${proc.name}`)
});
}
if (proc.restarts > 10) {
issues.push({
service: `PM2:${proc.name}`,
status: 'unstable',
error: `频繁重启(${proc.restarts}次)`,
critical: false,
severity: 'medium',
shouldNotify: shouldAlert(`PM2:${proc.name}:restarts`)
});
}
}
// 检查资源告警
if (resources.memory.used_pct > 90) {
issues.push({
service: '系统内存',
status: 'warning',
error: `内存使用率 ${resources.memory.used_pct}% (>90%)`,
critical: true,
severity: 'high',
shouldNotify: shouldAlert('系统内存')
});
}
if (resources.disk.used_pct > 85) {
issues.push({
service: '磁盘空间',
status: 'warning',
error: `磁盘使用率 ${resources.disk.used_pct}% (>85%)`,
critical: true,
severity: resources.disk.used_pct > 95 ? 'critical' : 'high',
shouldNotify: shouldAlert('磁盘空间')
});
}
// Nginx 状态
if (nginx.status !== 'active') {
issues.push({
service: 'Nginx',
status: nginx.status,
error: 'Nginx 未运行',
critical: true,
severity: 'critical',
shouldNotify: shouldAlert('Nginx')
});
}
return {
type: 'deep',
timestamp: new Date().toISOString(),
services: quick.services,
pm2: pm2Status,
resources,
nginx,
issues,
notifiableIssues: issues.filter(i => i.shouldNotify),
healthy: issues.filter(i => i.critical).length === 0,
summary: issues.length === 0
? '✅ 深度巡检通过 · 所有服务和资源正常'
: `⚠️ 发现${issues.length}个问题: ${issues.map(i => `${i.service}(${i.error})`).join('; ')}`
};
}
// ── 全量体检(每天一次)───────────
async function fullReport() {
const deep = await deepCheck();
return {
type: 'daily_report',
report_id: `RPT-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}`,
...deep,
alertCooldownStatus: getAlertCooldownStatus(),
recommendations: generateRecommendations(deep)
};
}
function generateRecommendations(checkResult) {
const recs = [];
for (const issue of checkResult.issues) {
if (issue.service.includes('内存') && issue.status === 'warning') {
recs.push('建议: 检查是否有进程内存泄漏,考虑增加 max_memory_restart 配置');
}
if (issue.service.includes('磁盘')) {
recs.push('建议: 运行 pm2 flush 清理日志,检查 /tmp 和 data/logs/ 目录');
}
if (issue.error?.includes('频繁重启')) {
recs.push(`建议: ${issue.service} 频繁重启,需要检查错误日志定位根因`);
}
if (issue.service === 'Nginx') {
recs.push('紧急: Nginx 异常,所有外部访问将受影响');
}
if (issue.service === 'MCP大脑服务器' && issue.status === 'timeout') {
recs.push('提示: MCP服务器在远程机器(ZY-SVR-005),请检查: 1)大脑服务器是否运行 2)网络连接是否正常 3)防火墙端口是否开放');
}
}
if (recs.length === 0) {
recs.push('系统运行良好,无需额外操作');
}
return recs;
}
module.exports = {
SERVICES,
PM2_PROCESSES,
probeService,
getPM2Status,
getSystemResources,
getNginxStatus,
quickCheck,
deepCheck,
fullReport,
shouldAlert,
getAlertCooldownStatus,
};