feat(admin/health): System Health Level 3 — тренды (сэмплинг + canvas-графики)
metrics.js: сэмплинг раз в минуту в кольцевой буфер (cap 24ч, unref) — ts/rss/heapUsed/reqPerMin/reqDelta/err5xx/p95; history() + поле history в snapshot (последние 180 точек). admin.js: секция «Тренды» с 4 мини-графиками (canvas): Память RSS, Запросы/мин, Ошибки 5xx, Латентность p95 — линия + заливка + подписи макс/последнее. Обновляются вместе с live-рефрешем. Проверено: сэмплер пишет, история в snapshot, графики рисуются (на старте — «накопление данных…», далее наполняются). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,39 @@ function _pct(sorted, p) {
|
||||
return sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))];
|
||||
}
|
||||
|
||||
/* ── Сэмплинг для трендов (Level 3): раз в минуту в кольцевой буфер ─────── */
|
||||
const HISTORY_CAP = 1440; // ~24 часа при 1 сэмпле/мин
|
||||
const samples = [];
|
||||
let _lastErr5xx = 0, _lastTotal = 0;
|
||||
|
||||
function sample() {
|
||||
const now = Date.now();
|
||||
const cutoff = now - WINDOW_MS;
|
||||
let rpm = 0;
|
||||
for (let i = recentTs.length - 1; i >= 0 && recentTs[i] >= cutoff; i--) rpm++;
|
||||
const sorted = [...latencies].sort((a, b) => a - b);
|
||||
const cur5xx = statusClasses['5xx'];
|
||||
const errDelta = Math.max(0, cur5xx - _lastErr5xx); _lastErr5xx = cur5xx;
|
||||
const reqDelta = Math.max(0, total - _lastTotal); _lastTotal = total;
|
||||
const mem = process.memoryUsage();
|
||||
samples.push({
|
||||
ts: now,
|
||||
rss: mem.rss,
|
||||
heapUsed: mem.heapUsed,
|
||||
reqPerMin: rpm,
|
||||
reqDelta,
|
||||
err5xx: errDelta,
|
||||
p95: _pct(sorted, 95),
|
||||
});
|
||||
if (samples.length > HISTORY_CAP) samples.shift();
|
||||
}
|
||||
|
||||
const _sampler = setInterval(sample, 60_000);
|
||||
if (_sampler.unref) _sampler.unref(); // не держим процесс живым
|
||||
sample(); // стартовая точка сразу
|
||||
|
||||
function history(limit = 180) { return samples.slice(-limit); }
|
||||
|
||||
function snapshot() {
|
||||
const now = Date.now();
|
||||
const cutoff = now - WINDOW_MS;
|
||||
@@ -61,7 +94,8 @@ function snapshot() {
|
||||
topBusy: [...routes].sort((a, b) => b.count - a.count).slice(0, 8),
|
||||
topSlow: [...routes].sort((a, b) => b.avgMs - a.avgMs).slice(0, 8),
|
||||
topErrors: routes.filter(r => r.errors > 0).sort((a, b) => b.errors - a.errors).slice(0, 8),
|
||||
history: history(180),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { record, snapshot };
|
||||
module.exports = { record, snapshot, sample, history };
|
||||
|
||||
Reference in New Issue
Block a user