'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 };