forked from admin/zeropost-engine
feat: error logs API
routes/admin.js: GET /logs — объединённые ошибки из 3 источников: generation_jobs (status=failed), ai_usage (!succeeded), scheduled_posts (status=failed) Сортировка по времени, топ-5 частых ошибок, группировка по типу
This commit is contained in:
@@ -326,3 +326,82 @@ router.delete('/queue/stuck', async (req, res) => {
|
|||||||
res.json({ ok: true, cleared: rows.length });
|
res.json({ ok: true, cleared: rows.length });
|
||||||
} catch (err) { res.status(500).json({ error: err.message }); }
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── ERROR LOGS ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
// GET /api/admin/logs — последние ошибки из всех источников
|
||||||
|
router.get('/logs', async (req, res) => {
|
||||||
|
if (!await requireAdmin(req, res)) return;
|
||||||
|
const limit = Math.min(parseInt(req.query.limit || 50), 200);
|
||||||
|
try {
|
||||||
|
const [genFailed, aiErrors, scheduledFailed] = await Promise.all([
|
||||||
|
// Ошибки генерации
|
||||||
|
query(`
|
||||||
|
SELECT
|
||||||
|
'generation' as source,
|
||||||
|
j.id::text as entity_id,
|
||||||
|
j.type as operation,
|
||||||
|
j.error as message,
|
||||||
|
j.topic as context,
|
||||||
|
u.email as user_email,
|
||||||
|
j.created_at
|
||||||
|
FROM generation_jobs j
|
||||||
|
LEFT JOIN users u ON u.id = j.user_id
|
||||||
|
WHERE j.status = 'failed' AND j.error IS NOT NULL
|
||||||
|
ORDER BY j.created_at DESC LIMIT $1
|
||||||
|
`, [limit]),
|
||||||
|
|
||||||
|
// Ошибки AI провайдеров
|
||||||
|
query(`
|
||||||
|
SELECT
|
||||||
|
'ai_provider' as source,
|
||||||
|
id::text as entity_id,
|
||||||
|
(provider || '/' || request_type) as operation,
|
||||||
|
error_message as message,
|
||||||
|
left(model, 60) as context,
|
||||||
|
NULL as user_email,
|
||||||
|
created_at
|
||||||
|
FROM ai_usage
|
||||||
|
WHERE NOT succeeded AND error_message IS NOT NULL
|
||||||
|
ORDER BY created_at DESC LIMIT $1
|
||||||
|
`, [limit]),
|
||||||
|
|
||||||
|
// Ошибки публикации постов
|
||||||
|
query(`
|
||||||
|
SELECT
|
||||||
|
'publish' as source,
|
||||||
|
sp.id::text as entity_id,
|
||||||
|
(c.platform || ' publish') as operation,
|
||||||
|
'Failed scheduled post' as message,
|
||||||
|
left(sp.custom_text, 60) as context,
|
||||||
|
NULL as user_email,
|
||||||
|
sp.scheduled_at as created_at
|
||||||
|
FROM scheduled_posts sp
|
||||||
|
JOIN channels c ON c.id = sp.channel_id
|
||||||
|
WHERE sp.status = 'failed'
|
||||||
|
ORDER BY sp.scheduled_at DESC LIMIT 20
|
||||||
|
`),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Объединяем и сортируем
|
||||||
|
const all = [
|
||||||
|
...genFailed.rows,
|
||||||
|
...aiErrors.rows,
|
||||||
|
...scheduledFailed.rows,
|
||||||
|
].sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||||
|
.slice(0, limit);
|
||||||
|
|
||||||
|
// Группируем по типу ошибки для статистики
|
||||||
|
const errorGroups = {};
|
||||||
|
for (const e of all) {
|
||||||
|
const key = e.message?.split('\n')[0]?.slice(0, 80) || 'unknown';
|
||||||
|
errorGroups[key] = (errorGroups[key] || 0) + 1;
|
||||||
|
}
|
||||||
|
const topErrors = Object.entries(errorGroups)
|
||||||
|
.sort((a, b) => b[1] - a[1])
|
||||||
|
.slice(0, 5)
|
||||||
|
.map(([msg, cnt]) => ({ msg, cnt }));
|
||||||
|
|
||||||
|
res.json({ errors: all, total: all.length, topErrors });
|
||||||
|
} catch (err) { res.status(500).json({ error: err.message }); }
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user