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:
@@ -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 `🚫 도메인 추천 서비스에 일시적으로 접근할 수 없습니다. 잠시 후 다시 시도해주세요.`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user