48 lines
2.4 KiB
JavaScript
48 lines
2.4 KiB
JavaScript
#!/usr/bin/env node
|
|
"use strict";
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const { transition } = require("./state");
|
|
const { sendSmtpMail } = require("../lake-lamp-authz/smtp-mailer");
|
|
|
|
async function main() {
|
|
const config = JSON.parse(fs.readFileSync(process.env.SENTINEL_CONFIG || "/etc/guanghu/state-change-sentinel.json", "utf8"));
|
|
const stateFile = process.env.SENTINEL_STATE || "/var/lib/guanghu/state-change-sentinel/state.json";
|
|
const previous = fs.existsSync(stateFile) ? JSON.parse(fs.readFileSync(stateFile, "utf8")) : {};
|
|
const next = {};
|
|
const notices = [];
|
|
for (const target of config.targets) {
|
|
const current = await check(target);
|
|
next[target.id] = current;
|
|
const changed = transition(previous[target.id], current);
|
|
if (changed.notify) notices.push({ id: target.id, kind: changed.kind, detail: current.detail });
|
|
}
|
|
fs.mkdirSync(path.dirname(stateFile), { recursive: true, mode: 0o700 });
|
|
fs.writeFileSync(stateFile, JSON.stringify(next), { mode: 0o600 });
|
|
if (!notices.length) return;
|
|
const lines = notices.map(item => `${item.kind === "anomaly" ? "异常" : "恢复"}: ${item.id} · ${item.detail}`);
|
|
await sendSmtpMail({
|
|
to: process.env.AUTH_RECIPIENT,
|
|
subject: `光湖节点状态变化 · ${notices.map(item => item.id).join(", ")}`,
|
|
text: ["光湖节点状态发生变化。静止在线期间不会发送邮件。", "", ...lines, "", new Date().toISOString()].join("\n"),
|
|
smtpHost: process.env.SMTP_HOST || "smtp.qq.com",
|
|
smtpPort: Number(process.env.SMTP_PORT || 465),
|
|
smtpUser: process.env.SMTP_USER,
|
|
smtpPass: process.env.SMTP_PASS,
|
|
});
|
|
}
|
|
|
|
async function check(target) {
|
|
const controller = new AbortController();
|
|
const timer = setTimeout(() => controller.abort(), target.timeout_ms || 8000);
|
|
try {
|
|
const response = await fetch(target.url, { method: "GET", redirect: "manual", signal: controller.signal, headers: { "user-agent": "Guanghu-State-Change-Sentinel/1.0" } });
|
|
const ok = (target.accept || [200]).includes(response.status);
|
|
return { ok, detail: `HTTP ${response.status}`, observed_at: new Date().toISOString() };
|
|
} catch (error) {
|
|
return { ok: false, detail: error.name === "AbortError" ? "timeout" : "connection-failed", observed_at: new Date().toISOString() };
|
|
} finally { clearTimeout(timer); }
|
|
}
|
|
|
|
main().catch(error => { process.stderr.write(`state sentinel failed: ${error.message}\n`); process.exit(1); });
|