feat(logs): 활동 로그 다이얼로그 상세 정보 표시 + 에러 정보 보강

- logs API: longtext로 저장된 details를 JSON 객체로 파싱해 반환
- 응답 스키마에 additionalProperties: true 추가 (fast-json-stringify가
  스키마 미정의 키를 제거하던 문제 해결)
- scheduler 에러 로그: err.cause / err.code / err.causeCode 함께 기록
  (fetch failed 등 모호한 메시지의 진짜 원인 식별 가능)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-05-26 17:02:53 +09:00
parent 9e3bff9762
commit 6a24b997dd
2 changed files with 29 additions and 4 deletions

View file

@ -9,6 +9,20 @@ const REDIS_PREFIX = 'bot:status:';
const TIMEZONE = 'Asia/Seoul'; const TIMEZONE = 'Asia/Seoul';
const MAX_CONSECUTIVE_ERRORS = 10; const MAX_CONSECUTIVE_ERRORS = 10;
/**
* 에러 객체에서 활동 로그용 details 구성
* - err.cause(Node fetch failed의 진짜 원인 ), err.code를 함께 포함
*/
function buildErrorDetails(err) {
const d = { error: err.message };
if (err.code) d.code = err.code;
if (err.cause) {
d.cause = err.cause.message || String(err.cause);
if (err.cause.code) d.causeCode = err.cause.code;
}
return d;
}
async function schedulerPlugin(fastify, opts) { async function schedulerPlugin(fastify, opts) {
const tasks = new Map(); const tasks = new Map();
const burstTimers = new Map(); // weekly 모드 내부 setInterval 핸들 const burstTimers = new Map(); // weekly 모드 내부 setInterval 핸들
@ -269,7 +283,7 @@ async function schedulerPlugin(fastify, opts) {
action: 'error', action: 'error',
category: 'sync', category: 'sync',
summary: `${botId} 동기화 오류: ${err.message}`, summary: `${botId} 동기화 오류: ${err.message}`,
details: { error: err.message }, details: buildErrorDetails(err),
}); });
} }
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
@ -279,7 +293,7 @@ async function schedulerPlugin(fastify, opts) {
action: 'stop', action: 'stop',
category: 'bot', category: 'bot',
summary: `${botId} 연속 ${MAX_CONSECUTIVE_ERRORS}회 실패로 자동 정지`, summary: `${botId} 연속 ${MAX_CONSECUTIVE_ERRORS}회 실패로 자동 정지`,
details: { error: err.message, consecutiveErrors }, details: { ...buildErrorDetails(err), consecutiveErrors },
}); });
try { try {
await stopBot(botId); await stopBot(botId);

View file

@ -77,7 +77,7 @@ export default async function logsRoutes(fastify) {
target_type: { type: 'string', nullable: true }, target_type: { type: 'string', nullable: true },
target_id: { type: 'integer', nullable: true }, target_id: { type: 'integer', nullable: true },
summary: { type: 'string' }, summary: { type: 'string' },
details: { type: 'object', nullable: true }, details: { type: 'object', nullable: true, additionalProperties: true },
created_at: { type: 'string' }, created_at: { type: 'string' },
}, },
}, },
@ -147,8 +147,19 @@ export default async function logsRoutes(fastify) {
[...params, limit, offset] [...params, limit, offset]
); );
// details는 longtext(JSON 문자열)로 저장되어 있으므로 객체로 파싱
const parsedLogs = logs.map(log => {
if (!log.details) return log;
if (typeof log.details === 'object') return log;
try {
return { ...log, details: JSON.parse(log.details) };
} catch {
return { ...log, details: { raw: log.details } };
}
});
return { return {
logs, logs: parsedLogs,
total, total,
page, page,
limit, limit,