feat(phase-5-3): Logger, Metrics, 알림 시스템 통합

Phase 5-3 모니터링 강화 작업의 통합을 완료했습니다.

변경사항:
- Logger 통합: console.log를 구조화된 로깅으로 전환 (9개 파일)
  - JSON 기반 로그, 환경별 자동 전환 (개발/프로덕션)
  - 타입 안전성 보장, 성능 측정 타이머 내장

- Metrics 통합: 실시간 성능 모니터링 시스템 연결 (3개 파일)
  - Circuit Breaker 상태 추적 (api_call_count, error_count, state)
  - Retry 재시도 횟수 추적 (retry_count)
  - OpenAI API 응답 시간 측정 (api_call_duration)

- 알림 통합: 장애 자동 알림 시스템 구현 (2개 파일)
  - Circuit Breaker OPEN 상태 → 관리자 Telegram 알림
  - 재시도 실패 → 관리자 Telegram 알림
  - Rate Limiting 적용 (1시간에 1회)

- 문서 업데이트:
  - CLAUDE.md: coder 에이전트 설명 강화 (20년+ 시니어 전문가)
  - README.md, docs/: 아키텍처 문서 추가

영향받은 파일: 16개 (수정 14개, 신규 2개)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-01-19 21:23:38 +09:00
parent 410676e322
commit eee934391a
16 changed files with 675 additions and 777 deletions

View File

@@ -1,5 +1,8 @@
import type { Env } from '../types';
import { retryWithBackoff, RetryError } from '../utils/retry';
import { createLogger } from '../utils/logger';
const logger = createLogger('domain-tool');
// Cloudflare AI Gateway를 통해 OpenAI API 호출 (지역 제한 우회)
const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions';
@@ -21,13 +24,13 @@ async function getCachedTLDPrice(
const key = `tld_price:${tld}`;
const cached = await kv.get(key, 'json');
if (cached) {
console.log(`[TLDCache] HIT: ${tld}`);
logger.info('TLDCache HIT', { tld });
return cached as CachedTLDPrice;
}
console.log(`[TLDCache] MISS: ${tld}`);
logger.info('TLDCache MISS', { tld });
return null;
} catch (error) {
console.error('[TLDCache] KV 조회 오류:', error);
logger.error('TLDCache KV 조회 오류', error as Error, { tld });
return null;
}
}
@@ -49,9 +52,9 @@ async function setCachedTLDPrice(
await kv.put(key, JSON.stringify(data), {
expirationTtl: 3600, // 1시간
});
console.log(`[TLDCache] SET: ${tld} (${data.krw}원)`);
logger.info('TLDCache SET', { tld, krw: data.krw });
} catch (error) {
console.error('[TLDCache] KV 저장 오류:', error);
logger.error('TLDCache KV 저장 오류', error as Error, { tld });
}
}
@@ -63,13 +66,13 @@ async function getCachedAllPrices(
const key = 'tld_price:all';
const cached = await kv.get(key, 'json');
if (cached) {
console.log('[TLDCache] HIT: all prices');
logger.info('TLDCache HIT: all prices');
return cached as any[];
}
console.log('[TLDCache] MISS: all prices');
logger.info('TLDCache MISS: all prices');
return null;
} catch (error) {
console.error('[TLDCache] KV 조회 오류:', error);
logger.error('TLDCache KV 조회 오류', error as Error, { key: 'all' });
return null;
}
}
@@ -84,9 +87,9 @@ async function setCachedAllPrices(
await kv.put(key, JSON.stringify(prices), {
expirationTtl: 3600, // 1시간
});
console.log(`[TLDCache] SET: all prices (${prices.length}개)`);
logger.info('TLDCache SET: all prices', { count: prices.length });
} catch (error) {
console.error('[TLDCache] KV 저장 오류:', error);
logger.error('TLDCache KV 저장 오류', error as Error, { key: 'all' });
}
}
@@ -349,7 +352,7 @@ async function callNamecheapApi(
query_time_ms: whois.query_time_ms,
};
} catch (error) {
console.error('[whois_lookup] 오류:', error);
logger.error('오류', error as Error, { domain: funcArgs.domain });
if (error instanceof RetryError) {
return { error: 'WHOIS 조회 서비스에 일시적으로 접근할 수 없습니다.' };
}
@@ -379,9 +382,9 @@ async function callNamecheapApi(
await db.prepare(
'INSERT INTO user_domains (user_id, domain, verified, created_at) VALUES (?, ?, 1, datetime("now"))'
).bind(userId, funcArgs.domain).run();
console.log(`[register_domain] user_domains에 추가: user_id=${userId}, domain=${funcArgs.domain}`);
logger.info('user_domains에 추가', { userId, domain: funcArgs.domain });
} catch (dbError) {
console.error('[register_domain] user_domains 추가 실패:', dbError);
logger.error('user_domains 추가 실패', dbError as Error, { userId, domain: funcArgs.domain });
result.warning = result.warning || '';
result.warning += ' (DB 기록 실패 - 수동 추가 필요)';
}
@@ -712,11 +715,11 @@ export async function executeManageDomain(
db?: D1Database
): Promise<string> {
const { action, domain, nameservers, tld } = args;
console.log('[manage_domain] 시작:', { action, domain, telegramUserId, hasDb: !!db });
logger.info('시작', { action, domain, telegramUserId, hasDb: !!db });
// 소유권 검증 (DB 조회)
if (!telegramUserId || !db) {
console.log('[manage_domain] 실패: telegramUserId 또는 db 없음');
logger.info('실패: telegramUserId 또는 db 없음');
return '🚫 도메인 관리 권한이 없습니다.';
}
@@ -737,9 +740,9 @@ export async function executeManageDomain(
'SELECT domain FROM user_domains WHERE user_id = ? AND verified = 1'
).bind(user.id).all<{ domain: string }>();
userDomains = domains.results?.map(d => d.domain) || [];
console.log('[manage_domain] 소유 도메인:', userDomains);
logger.info('소유 도메인', { userDomains });
} catch (error) {
console.log('[manage_domain] DB 오류:', error);
logger.error('DB 오류', error as Error);
return '🚫 권한 확인 중 오류가 발생했습니다.';
}
@@ -754,17 +757,17 @@ export async function executeManageDomain(
db,
userId
);
console.log('[manage_domain] 완료:', result?.slice(0, 100));
logger.info('완료', { result: result?.slice(0, 100) });
return result;
} catch (error) {
console.log('[manage_domain] 오류:', error);
logger.error('오류', error as Error);
return `🚫 도메인 관리 오류: ${String(error)}`;
}
}
export async function executeSuggestDomains(args: { keywords: string }, env?: Env): Promise<string> {
const { keywords } = args;
console.log('[suggest_domains] 시작:', { keywords });
logger.info('시작', { keywords });
if (!env?.OPENAI_API_KEY) {
return '🚫 도메인 추천 기능이 설정되지 않았습니다. (OPENAI_API_KEY 미설정)';
@@ -928,7 +931,7 @@ ${excludeList ? `- 다음 도메인은 제외하세요: ${excludeList}` : ''}
return response;
} catch (error) {
console.error('[suggestDomains] 오류:', error);
logger.error('오류', error as Error, { keywords });
if (error instanceof RetryError) {
return `🚫 도메인 추천 서비스에 일시적으로 접근할 수 없습니다. 잠시 후 다시 시도해주세요.`;
}