[HLCC-ICE-000001][ZY-CONTRIB-20260723-001] feat: 以来光者贡献链启用冰朔第五域个人子频道
This commit is contained in:
commit
5615453e4e
660 changed files with 122355 additions and 0 deletions
5
server-tools/state-change-sentinel/README.md
Normal file
5
server-tools/state-change-sentinel/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# 状态变化哨兵
|
||||
|
||||
这不是五分钟只读心跳,也不抓取页面内容。JD 每 15 分钟只请求两个自有健康入口,
|
||||
首次观察仅建立基线;状态完全不变时不发邮件。只有在线变异常、异常类型变化或恢复在线
|
||||
才发送提醒。整个节点直接断电时,本机无法主动上报,因此仍需由 JD 做低频外部判定。
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
[Unit]
|
||||
Description=Guanghu state-change-only node sentinel
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
User=guanghu-sentinel
|
||||
Group=guanghu-sentinel
|
||||
SupplementaryGroups=guanghu
|
||||
WorkingDirectory=/opt/guanghu/state-change-sentinel
|
||||
EnvironmentFile=/etc/guanghu/secrets/mail/qq-authorization.env
|
||||
Environment=SENTINEL_CONFIG=/etc/guanghu/state-change-sentinel.json
|
||||
Environment=SENTINEL_STATE=/var/lib/guanghu/state-change-sentinel/state.json
|
||||
ExecStart=/usr/bin/node /opt/guanghu/state-change-sentinel/sentinel.js
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/var/lib/guanghu/state-change-sentinel
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
[Unit]
|
||||
Description=Check Guanghu nodes for state transitions only
|
||||
|
||||
[Timer]
|
||||
OnBootSec=5min
|
||||
OnUnitInactiveSec=15min
|
||||
RandomizedDelaySec=2min
|
||||
Persistent=true
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
47
server-tools/state-change-sentinel/sentinel.js
Normal file
47
server-tools/state-change-sentinel/sentinel.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/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); });
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"targets": [
|
||||
{ "id": "BS-GZ-006-front-door", "url": "https://guanghulab.com/", "accept": [200], "timeout_ms": 8000 },
|
||||
{ "id": "JD-Lake-Lamp-public-route", "url": "https://guanghulab.com/authz/health", "accept": [200], "timeout_ms": 8000 }
|
||||
]
|
||||
}
|
||||
9
server-tools/state-change-sentinel/state.js
Normal file
9
server-tools/state-change-sentinel/state.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
"use strict";
|
||||
|
||||
function transition(previous, current) {
|
||||
if (!previous) return { notify: false, state: current, kind: "baseline" };
|
||||
if (previous.ok === current.ok && previous.detail === current.detail) return { notify: false, state: current, kind: "unchanged" };
|
||||
return { notify: true, state: current, kind: current.ok ? "recovered" : "anomaly" };
|
||||
}
|
||||
|
||||
module.exports = { transition };
|
||||
10
server-tools/state-change-sentinel/state.test.js
Normal file
10
server-tools/state-change-sentinel/state.test.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
"use strict";
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { transition } = require("./state");
|
||||
test("first observation records a baseline without email", () => assert.equal(transition(null, { ok: true, detail: "200" }).notify, false));
|
||||
test("steady online state sends no email", () => assert.equal(transition({ ok: true, detail: "200" }, { ok: true, detail: "200" }).notify, false));
|
||||
test("online to offline and offline to online both notify", () => {
|
||||
assert.deepEqual(transition({ ok: true, detail: "200" }, { ok: false, detail: "timeout" }).kind, "anomaly");
|
||||
assert.deepEqual(transition({ ok: false, detail: "timeout" }, { ok: true, detail: "200" }).kind, "recovered");
|
||||
});
|
||||
Loading…
Reference in a new issue