Files

81 lines
3.6 KiB
TypeScript

/** # 脚本说明
* SERP 24h 监控采样器(在 admin 服务器本机运行,零额外负载:只读管理接口):
* nohup bun serp-monitor.ts > data/monitor.log 2>&1 &
* 每 60s 采样 /api/serp/stats(hours24)+ /api/serp/pool(浏览器池)+ /api/serp/cache(缓存池),
* 追加 JSONL 到 data/serp-monitor.jsonl;每 10 分钟输出一行摘要到 stdout(进 monitor.log)。
* 用途:跑一天边测边修的长期证据链(吞吐/错误率/门壳/池水位)。
*/
import fs from "node:fs";
/** # 成员变量 基址与令牌(本机) **/
const BASE = "http://127.0.0.1:53100";
const TOKEN = process.env.SERP_TOKEN ?? "";
if (!TOKEN) { console.error("缺少 SERP_TOKEN"); process.exit(1); }
/** # 成员变量 输出目录(data/,相对脚本) **/
const DATA = new URL("./data/", import.meta.url).pathname;
fs.mkdirSync(DATA, { recursive: true });
const JSONL = `${DATA}serp-monitor.jsonl`;
/** # 成员变量 上一次快照(用于计算每分钟增量) **/
let prev: { total: number; ok: number; failed: number; captcha: number; gate: number; at: number } | null = null;
/** # 成员变量 每 10 分钟摘要计数器 **/
let sinceSummary = 0;
/** # 函数
* 带超时 GET JSON。
*/
async function getJson(path: string): Promise<any> {
const res = await fetch(`${BASE}${path}`, { headers: { authorization: `Bearer ${TOKEN}` }, signal: AbortSignal.timeout(15000) });
if (!res.ok) throw new Error(`HTTP ${res.status} ${path}`);
const j = await res.json() as any;
return j?.data ?? j;
}
/** # 函数
* 单轮采样:统计/池/缓存 → 增量计算 → JSONL 追加 → 10 分钟摘要打印。
*/
async function sample(): Promise<void> {
const now = Date.now();
const [stats, pool, cache] = await Promise.all([
getJson("/api/serp/stats").catch(() => null),
getJson("/api/serp/pool").catch(() => null),
getJson("/api/serp/cache").catch(() => null),
]);
if (!stats) return;
const h = stats.hours24 ?? {};
const row = {
at: new Date(now).toISOString(),
h24: { total: h.total ?? 0, ok: h.ok ?? 0, failed: h.failed ?? 0, captcha: h.captcha ?? 0, gate: h.gate ?? 0, avg_ms: h.avg_ms ?? 0, p95_ms: h.p95_ms ?? 0 },
delta: null as any,
browserPool: null as any,
cachePool: null as any,
};
if (prev && now - prev.at > 30000) {
row.delta = {
total: (h.total ?? 0) - prev.total,
failed: (h.failed ?? 0) - prev.failed,
gate: (h.gate ?? 0) - prev.gate,
captcha: (h.captcha ?? 0) - prev.captcha,
};
}
prev = { total: h.total ?? 0, ok: h.ok ?? 0, failed: h.failed ?? 0, captcha: h.captcha ?? 0, gate: h.gate ?? 0, at: now };
if (pool?.totals) row.browserPool = { ...pool.totals, targets: pool.nodes.map((n: any) => `${n.node}:${n.total}/${n.target}`).join(",") };
if (cache) row.cachePool = { count: cache.count, maxCache: cache.maxCache };
fs.appendFileSync(JSONL, JSON.stringify(row) + "\n");
sinceSummary++;
if (sinceSummary >= 10) {
sinceSummary = 0;
const d = row.delta;
const errRate = d && d.total > 0 ? +(((d.failed) / d.total) * 100).toFixed(1) : null;
console.log(`${row.at} Δ10min total=${d?.total} failed=${d?.failed} gate=${d?.gate} captcha=${d?.captcha} err=${errRate}% avg=${row.h24.avg_ms}ms browserPool=${row.browserPool ? `${row.browserPool.total}(ready${row.browserPool.ready})` : "?"} cache=${row.cachePool?.count}/${row.cachePool?.maxCache}`);
}
}
/** # 主流程:60s 一轮,永久运行(systemd/nohup 托管) **/
console.log(`serp-monitor started ${new Date().toISOString()}`);
for (;;) {
await sample().catch((e) => console.error(`${new Date().toISOString()} sample error: ${String(e).slice(0, 160)}`));
await Bun.sleep(60000);
}