6a934ca6c6
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>
102 lines
4.1 KiB
JavaScript
102 lines
4.1 KiB
JavaScript
'use strict';
|
|
/*
|
|
* Лёгкие in-memory метрики HTTP-запросов для System Health (Level 2).
|
|
* Сбрасываются при перезапуске процесса. Память ограничена кольцевыми буферами.
|
|
* Группировка по шаблону маршрута (req.route.path), а не по конкретному URL —
|
|
* чтобы /api/biochem/molecules/:id не плодил тысячи ключей.
|
|
*/
|
|
const MAX_LAT = 3000; // окно последних латентностей для перцентилей
|
|
const WINDOW_MS = 60_000; // окно для req/min
|
|
|
|
let total = 0;
|
|
let totalServerErrors = 0;
|
|
const statusClasses = { '2xx': 0, '3xx': 0, '4xx': 0, '5xx': 0 };
|
|
const routeStats = new Map(); // "METHOD /path" -> { count, totalMs, maxMs, errors }
|
|
const latencies = []; // последние латентности (мс)
|
|
const recentTs = []; // метки времени последних запросов (для req/min)
|
|
const startedAt = Date.now();
|
|
|
|
function record(method, route, status, ms) {
|
|
total++;
|
|
const cls = Math.floor(status / 100) + 'xx';
|
|
if (statusClasses[cls] !== undefined) statusClasses[cls]++;
|
|
if (status >= 500) totalServerErrors++;
|
|
|
|
const key = method + ' ' + route;
|
|
let r = routeStats.get(key);
|
|
if (!r) { r = { count: 0, totalMs: 0, maxMs: 0, errors: 0 }; routeStats.set(key, r); }
|
|
r.count++; r.totalMs += ms; if (ms > r.maxMs) r.maxMs = ms;
|
|
if (status >= 400) r.errors++;
|
|
|
|
latencies.push(ms);
|
|
if (latencies.length > MAX_LAT) latencies.shift();
|
|
|
|
const now = Date.now();
|
|
recentTs.push(now);
|
|
const cutoff = now - WINDOW_MS;
|
|
while (recentTs.length && recentTs[0] < cutoff) recentTs.shift();
|
|
}
|
|
|
|
function _pct(sorted, p) {
|
|
if (!sorted.length) return 0;
|
|
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;
|
|
let reqPerMin = 0;
|
|
for (let i = recentTs.length - 1; i >= 0 && recentTs[i] >= cutoff; i--) reqPerMin++;
|
|
const sorted = [...latencies].sort((a, b) => a - b);
|
|
const avg = latencies.length ? latencies.reduce((s, x) => s + x, 0) / latencies.length : 0;
|
|
const routes = [...routeStats.entries()].map(([k, r]) => ({
|
|
route: k, count: r.count, avgMs: r.totalMs / r.count, maxMs: r.maxMs, errors: r.errors,
|
|
}));
|
|
return {
|
|
total, totalServerErrors, statusClasses,
|
|
reqPerMin,
|
|
avgMs: avg, p50: _pct(sorted, 50), p95: _pct(sorted, 95), p99: _pct(sorted, 99),
|
|
routeCount: routeStats.size,
|
|
sinceMs: now - startedAt,
|
|
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, sample, history };
|