feat(admin/health): System Health Level 4 — диагностика + последние ошибки
adminController.getHealth: активные health-проверки — отклик БД (ping, мс) и
тест записи на диск рядом с БД; вердикт уходит в critical при недоступной БД
или диске, warning при медленном отклике БД (>100мс). Плюс recentErrorList —
последние 8 записей error_log (level/route/method/message/время).
admin.js: панель «Диагностика» — индикаторы БД/диска (зелёный/красный) +
лента последних ошибок с цветом по уровню.
Проверено: checks {dbOk,dbPingMs,diskWritable}, список ошибок отдаётся.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -648,6 +648,21 @@ function _dbTables() {
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
// Активные проверки: отклик БД (мс) и тест записи на диск рядом с БД.
|
||||
function _runChecks() {
|
||||
let dbPingMs = null, dbOk = false;
|
||||
try { const t = process.hrtime.bigint(); db.prepare('SELECT 1 AS ok').get(); dbPingMs = Number(process.hrtime.bigint() - t) / 1e6; dbOk = true; } catch {}
|
||||
let diskWritable = false;
|
||||
try {
|
||||
const f = path.join(path.dirname(path.resolve(DB_PATH)), '.health-write-test');
|
||||
fs.writeFileSync(f, 'ok'); fs.unlinkSync(f); diskWritable = true;
|
||||
} catch {}
|
||||
return { dbPingMs, dbOk, diskWritable };
|
||||
}
|
||||
const _recentErrStmt = db.prepare(
|
||||
"SELECT id, level, message, route, method, created_at FROM error_log ORDER BY id DESC LIMIT 8"
|
||||
);
|
||||
|
||||
function getHealth(_req, res) {
|
||||
const uptimeSec = process.uptime();
|
||||
const mem = process.memoryUsage();
|
||||
@@ -672,6 +687,10 @@ function getHealth(_req, res) {
|
||||
let sseStats = { users: 0, guests: 0, connections: 0 };
|
||||
try { sseStats = sse.stats(); } catch {}
|
||||
|
||||
// Активные health-проверки (Level 4): отклик БД и запись на диск.
|
||||
const checks = _runChecks();
|
||||
const recentErrorList = (() => { try { return _recentErrStmt.all(); } catch { return []; } })();
|
||||
|
||||
// Вердикт здоровья по порогам.
|
||||
const reasons = [];
|
||||
let status = 'ok';
|
||||
@@ -688,9 +707,13 @@ function getHealth(_req, res) {
|
||||
if (eventLoopLagMs > 200) crit(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
||||
else if (eventLoopLagMs > 70) warn(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
||||
if (dbSizeBytes > 1.5e9) warn('БД >1.5 ГБ');
|
||||
if (!checks.dbOk) crit('БД недоступна');
|
||||
if (!checks.diskWritable) crit('Диск недоступен для записи');
|
||||
if (checks.dbPingMs != null && checks.dbPingMs > 100) warn(`Медленный отклик БД ${checks.dbPingMs.toFixed(0)} мс`);
|
||||
|
||||
res.json({
|
||||
status, reasons,
|
||||
checks, recentErrorList,
|
||||
uptime: uptimeSec,
|
||||
startedAt: new Date(Date.now() - uptimeSec * 1000).toISOString(),
|
||||
memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
||||
|
||||
@@ -388,6 +388,30 @@
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
// ── панель диагностики (Level 4): health-чеки + последние ошибки ──
|
||||
let diagHtml = '';
|
||||
{
|
||||
const ch = h.checks || {};
|
||||
const okCol = '#4ade80', badCol = 'var(--pink)';
|
||||
const chip = (label, ok, extra) => `<div style="display:flex;align-items:center;gap:6px;font-size:.82rem"><span style="width:9px;height:9px;border-radius:50%;background:${ok?okCol:badCol}"></span>${label}${extra?` <span style="color:var(--text-3)">${extra}</span>`:''}</div>`;
|
||||
const errs = h.recentErrorList || [];
|
||||
const lvlCol = l => l==='error'||l==='fatal'?'var(--pink)':l==='warn'?'#facc15':'var(--text-3)';
|
||||
diagHtml = `<div class="adm-panel" style="margin:14px 0 0">
|
||||
<div class="adm-panel-title">Диагностика</div>
|
||||
<div style="display:flex;gap:20px;flex-wrap:wrap;margin:6px 0 14px">
|
||||
${chip('База данных', !!ch.dbOk, ch.dbPingMs!=null?ch.dbPingMs.toFixed(2)+' мс':'')}
|
||||
${chip('Запись на диск', !!ch.diskWritable, ch.diskWritable?'доступна':'НЕДОСТУПНА')}
|
||||
</div>
|
||||
<div style="font-size:.72rem;color:var(--text-3);font-weight:700;text-transform:uppercase;margin-bottom:5px">Последние ошибки</div>
|
||||
${errs.length ? errs.map(e=>`<div style="display:flex;align-items:baseline;gap:8px;font-size:.78rem;padding:3px 0;border-bottom:1px solid rgba(255,255,255,.04)">
|
||||
<span style="color:var(--text-3);white-space:nowrap;font-size:.72rem">${esc((e.created_at||'').replace('T',' ').slice(5,16))}</span>
|
||||
<span style="color:${lvlCol(e.level)};font-weight:700;white-space:nowrap">${esc(e.level||'')}</span>
|
||||
${e.route?`<span style="color:var(--text-3);white-space:nowrap">${esc(e.method||'')} ${esc(e.route)}</span>`:''}
|
||||
<span style="flex:1;color:var(--text-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(e.message||'')}</span>
|
||||
</div>`).join('') : `<div style="color:var(--green);font-size:.82rem">Ошибок нет</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="adm-panel" style="margin:0 0 16px;padding:14px 18px;display:flex;align-items:center;gap:14px;border-left:4px solid ${stColor}">
|
||||
<div style="width:13px;height:13px;border-radius:50%;background:${stColor};box-shadow:0 0 12px ${stColor};flex-shrink:0"></div>
|
||||
@@ -440,6 +464,8 @@
|
||||
|
||||
${trendsHtml}
|
||||
|
||||
${diagHtml}
|
||||
|
||||
<div class="adm-panel" style="margin:14px 0 0">
|
||||
<div class="adm-panel-title">Крупнейшие таблицы БД</div>
|
||||
${(h.db.tables||[]).map(t=>`<div style="display:flex;align-items:center;gap:10px;margin:4px 0">
|
||||
|
||||
Reference in New Issue
Block a user