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 []; }
|
} 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) {
|
function getHealth(_req, res) {
|
||||||
const uptimeSec = process.uptime();
|
const uptimeSec = process.uptime();
|
||||||
const mem = process.memoryUsage();
|
const mem = process.memoryUsage();
|
||||||
@@ -672,6 +687,10 @@ function getHealth(_req, res) {
|
|||||||
let sseStats = { users: 0, guests: 0, connections: 0 };
|
let sseStats = { users: 0, guests: 0, connections: 0 };
|
||||||
try { sseStats = sse.stats(); } catch {}
|
try { sseStats = sse.stats(); } catch {}
|
||||||
|
|
||||||
|
// Активные health-проверки (Level 4): отклик БД и запись на диск.
|
||||||
|
const checks = _runChecks();
|
||||||
|
const recentErrorList = (() => { try { return _recentErrStmt.all(); } catch { return []; } })();
|
||||||
|
|
||||||
// Вердикт здоровья по порогам.
|
// Вердикт здоровья по порогам.
|
||||||
const reasons = [];
|
const reasons = [];
|
||||||
let status = 'ok';
|
let status = 'ok';
|
||||||
@@ -688,9 +707,13 @@ function getHealth(_req, res) {
|
|||||||
if (eventLoopLagMs > 200) crit(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
if (eventLoopLagMs > 200) crit(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
||||||
else if (eventLoopLagMs > 70) warn(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
else if (eventLoopLagMs > 70) warn(`Лаг event-loop ${eventLoopLagMs.toFixed(0)} мс`);
|
||||||
if (dbSizeBytes > 1.5e9) warn('БД >1.5 ГБ');
|
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({
|
res.json({
|
||||||
status, reasons,
|
status, reasons,
|
||||||
|
checks, recentErrorList,
|
||||||
uptime: uptimeSec,
|
uptime: uptimeSec,
|
||||||
startedAt: new Date(Date.now() - uptimeSec * 1000).toISOString(),
|
startedAt: new Date(Date.now() - uptimeSec * 1000).toISOString(),
|
||||||
memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
memory: { rss: mem.rss, heapUsed: mem.heapUsed, heapTotal: mem.heapTotal },
|
||||||
|
|||||||
@@ -388,6 +388,30 @@
|
|||||||
</div></div>`;
|
</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 = `
|
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 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>
|
<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}
|
${trendsHtml}
|
||||||
|
|
||||||
|
${diagHtml}
|
||||||
|
|
||||||
<div class="adm-panel" style="margin:14px 0 0">
|
<div class="adm-panel" style="margin:14px 0 0">
|
||||||
<div class="adm-panel-title">Крупнейшие таблицы БД</div>
|
<div class="adm-panel-title">Крупнейшие таблицы БД</div>
|
||||||
${(h.db.tables||[]).map(t=>`<div style="display:flex;align-items:center;gap:10px;margin:4px 0">
|
${(h.db.tables||[]).map(t=>`<div style="display:flex;align-items:center;gap:10px;margin:4px 0">
|
||||||
|
|||||||
Reference in New Issue
Block a user