guanghulab/skyeye/scripts/daily-scan.js

203 lines
6.0 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
/**
* skyeye/scripts/daily-scan.js
* 天眼日常扫描脚本 轻量级每日巡检感知 + 护卫 + 精算
* Covers Phase 1 steps ①②③ of the five-sense cycle
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.resolve(__dirname, '../..');
const SKYEYE_DIR = path.resolve(__dirname, '..');
const MANIFEST_PATH = path.join(SKYEYE_DIR, 'infra-manifest.json');
const LEDGER_PATH = path.join(SKYEYE_DIR, 'quota-ledger.json');
const GUARDS_DIR = path.join(SKYEYE_DIR, 'guards');
const LOGS_DIR = path.join(SKYEYE_DIR, 'logs');
function getTimestamp() {
return new Date().toISOString();
}
function getDateStr() {
return new Date().toISOString().slice(0, 10).replace(/-/g, '');
}
function loadJSON(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
} catch (e) {
return null;
}
}
function saveJSON(filePath, data) {
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
}
// ① 感知Sense— 扫描 infra-manifest确认节点状态
function sense() {
const result = { status: 'ok', nodes: [], issues: [] };
const manifest = loadJSON(MANIFEST_PATH);
if (!manifest) {
result.status = 'error';
result.issues.push('infra-manifest.json 不可读');
return result;
}
// Check last scan freshness (48h threshold)
if (manifest.last_scan) {
const lastScanTime = new Date(manifest.last_scan).getTime();
if (isNaN(lastScanTime)) {
result.issues.push('infra-manifest.last_scan 日期格式无效');
}
const lastScanAge = isNaN(lastScanTime) ? 0 : Date.now() - lastScanTime;
const hours = Math.round(lastScanAge / (1000 * 60 * 60));
if (!isNaN(lastScanTime) && hours > 48) {
result.issues.push(`infra-manifest 最后扫描距今 ${hours}h超过 48h 阈值`);
}
}
// Enumerate registered infrastructure nodes
if (manifest.infrastructure) {
for (const [key, svc] of Object.entries(manifest.infrastructure)) {
result.nodes.push({
id: key,
service: svc.service || key,
plan: svc.plan || 'unknown'
});
}
}
return result;
}
// ② 护卫Guard— 逐一检查五个 Guard 守卫状态
function guardCheck() {
const result = { total: 0, healthy: 0, issues: [], guards: [] };
if (!fs.existsSync(GUARDS_DIR)) {
result.issues.push('guards/ 目录不存在');
return result;
}
const files = fs.readdirSync(GUARDS_DIR)
.filter(f => f.endsWith('.json') && f !== 'guard-template.json');
result.total = files.length;
for (const file of files) {
const guard = loadJSON(path.join(GUARDS_DIR, file));
if (!guard) {
result.issues.push(`${file}: JSON 解析失败`);
result.guards.push({ file, status: 'error', reason: 'invalid_json' });
continue;
}
const entry = {
file,
guard_id: guard.guard_id,
status: guard.status,
mode: guard.mode,
last_check: guard.health_check?.last_check || null,
consecutive_failures: guard.health_check?.consecutive_failures || 0
};
if (guard.status === 'active' && (guard.health_check?.consecutive_failures || 0) < 3) {
result.healthy++;
} else {
result.issues.push(`${guard.guard_id}: status=${guard.status}, failures=${guard.health_check?.consecutive_failures || 0}`);
}
result.guards.push(entry);
}
return result;
}
// ③ 精算Audit— 读 quota-ledger检查配额消耗
function auditQuotas() {
const result = { status: 'healthy', alerts: [], services: {} };
const ledger = loadJSON(LEDGER_PATH);
if (!ledger) {
result.status = 'error';
result.alerts.push('quota-ledger.json 不可读');
return result;
}
if (ledger.services) {
for (const [name, data] of Object.entries(ledger.services)) {
let usagePct = 0;
if (data.limit && data.used !== undefined && data.used !== null) {
usagePct = Math.round((data.used / data.limit) * 100 * 10) / 10;
}
const entry = { usage_percent: usagePct, status: 'normal' };
if (usagePct > 95) {
entry.status = 'critical';
result.alerts.push(`${name}: 配额消耗 ${usagePct}% > 95%,需立即降频`);
result.status = 'critical';
} else if (usagePct > 80) {
entry.status = 'warning';
result.alerts.push(`${name}: 配额消耗 ${usagePct}% > 80%,配额告警`);
if (result.status !== 'critical') result.status = 'warning';
}
result.services[name] = entry;
}
}
return result;
}
function run() {
console.log(`[SkyEye Daily Scan] Starting daily scan`);
console.log(`[SkyEye Daily Scan] Timestamp: ${getTimestamp()}`);
const scanResult = {
scan_id: `DAILY-SCAN-${getDateStr()}`,
timestamp: getTimestamp(),
type: 'daily',
sense: sense(),
guard_check: guardCheck(),
quota_audit: auditQuotas(),
summary: {}
};
// Aggregate summary
const allIssues = [
...scanResult.sense.issues,
...scanResult.guard_check.issues,
...scanResult.quota_audit.alerts
];
scanResult.summary = {
nodes_registered: scanResult.sense.nodes.length,
guards_healthy: `${scanResult.guard_check.healthy}/${scanResult.guard_check.total}`,
quota_status: scanResult.quota_audit.status,
total_issues: allIssues.length,
issues: allIssues
};
// Write daily log
const logDir = path.join(LOGS_DIR, 'daily');
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
const logPath = path.join(logDir, `daily-scan-${getDateStr()}.json`);
saveJSON(logPath, scanResult);
console.log(`[SkyEye Daily Scan] Nodes: ${scanResult.summary.nodes_registered}`);
console.log(`[SkyEye Daily Scan] Guards: ${scanResult.summary.guards_healthy}`);
console.log(`[SkyEye Daily Scan] Quota: ${scanResult.summary.quota_status}`);
console.log(`[SkyEye Daily Scan] Issues: ${scanResult.summary.total_issues}`);
console.log(`[SkyEye Daily Scan] Log saved: ${logPath}`);
console.log('---DAILY_SCAN_JSON---');
console.log(JSON.stringify(scanResult, null, 2));
return scanResult;
}
run();