diff --git a/src/n8n-service.ts b/src/n8n-service.ts index 7cc27b9..6b32425 100644 --- a/src/n8n-service.ts +++ b/src/n8n-service.ts @@ -75,7 +75,7 @@ JSON:`; }; } } catch (error) { - console.error('Intent analysis error:', error); + logger.error('Intent analysis error', error as Error); } // 기본값: 일반 대화 @@ -114,7 +114,12 @@ export async function callN8n( }); if (!response.ok) { - console.error('n8n error:', response.status, await response.text()); + const errorText = await response.text(); + logger.error('n8n API error', new Error(`Status ${response.status}: ${errorText}`), { + status: response.status, + userId, + type + }); return { error: `n8n 호출 실패 (${response.status})` }; } @@ -128,7 +133,7 @@ export async function callN8n( return parseResult.data; } catch (error) { - console.error('n8n fetch error:', error); + logger.error('n8n fetch error', error as Error, { userId, type }); return { error: 'n8n 연결 실패' }; } } diff --git a/src/routes/api.ts b/src/routes/api.ts index 6345d5a..dd5ed03 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { Hono } from 'hono'; import { cors } from 'hono/cors'; +import { createMiddleware } from 'hono/factory'; import { Env } from '../types'; import { sendMessage } from '../telegram'; import { @@ -44,17 +45,20 @@ const ChatApiBodySchema = z.object({ }); /** - * API Key 인증 검증 (Timing-safe comparison으로 타이밍 공격 방지) - * @returns 인증 실패 시 Response, 성공 시 null + * API Key 인증 미들웨어 (Timing-safe comparison으로 타이밍 공격 방지) + * X-API-Key 헤더로 DEPOSIT_API_SECRET 검증 */ -function requireApiKey(request: Request, env: Env): Response | null { - const apiSecret = env.DEPOSIT_API_SECRET; - const authHeader = request.headers.get('X-API-Key'); +const apiKeyAuth = createMiddleware<{ Bindings: Env }>(async (c, next) => { + const apiSecret = c.env.DEPOSIT_API_SECRET; + const authHeader = c.req.header('X-API-Key'); + if (!apiSecret || !timingSafeEqual(authHeader, apiSecret)) { - return Response.json({ error: 'Unauthorized' }, { status: 401 }); + logger.warn('API Key 인증 실패', { hasApiKey: !!authHeader }); + return c.json({ error: 'Unauthorized' }, 401); } - return null; -} + + return await next(); +}); /** * CORS 헤더 생성 @@ -101,17 +105,12 @@ async function getOrCreateUser( /** * GET /api/deposit/balance - 잔액 조회 (namecheap-api 전용) * - * @param request - HTTP Request * @param env - Environment bindings * @param url - Parsed URL * @returns JSON response with balance */ -async function handleDepositBalance(request: Request, env: Env, url: URL): Promise { +async function handleDepositBalance(env: Env, url: URL): Promise { try { - // API Key 인증 - const authError = requireApiKey(request, env); - if (authError) return authError; - const telegramId = url.searchParams.get('telegram_id'); if (!telegramId) { return Response.json({ error: 'telegram_id required' }, { status: 400 }); @@ -150,10 +149,6 @@ async function handleDepositBalance(request: Request, env: Env, url: URL): Promi */ async function handleDepositDeduct(request: Request, env: Env): Promise { try { - // API Key 인증 - const authError = requireApiKey(request, env); - if (authError) return authError; - // JSON 파싱 (별도 에러 핸들링) let jsonData: unknown; try { @@ -869,14 +864,14 @@ api.use('/contact', cors({ allowHeaders: ['Content-Type'], })); -// GET /deposit/balance - 잔액 조회 (namecheap-api 전용) -api.get('/deposit/balance', async (c) => { +// GET /deposit/balance - 잔액 조회 (namecheap-api 전용, API Key 필요) +api.get('/deposit/balance', apiKeyAuth, async (c) => { const url = new URL(c.req.url); - return await handleDepositBalance(c.req.raw, c.env, url); + return await handleDepositBalance(c.env, url); }); -// POST /deposit/deduct - 잔액 차감 (namecheap-api 전용) -api.post('/deposit/deduct', async (c) => { +// POST /deposit/deduct - 잔액 차감 (namecheap-api 전용, API Key 필요) +api.post('/deposit/deduct', apiKeyAuth, async (c) => { return await handleDepositDeduct(c.req.raw, c.env); }); diff --git a/src/routes/handlers/message-handler.ts b/src/routes/handlers/message-handler.ts index 51f40c2..fc0aa38 100644 --- a/src/routes/handlers/message-handler.ts +++ b/src/routes/handlers/message-handler.ts @@ -4,8 +4,11 @@ import { handleCommand } from '../../commands'; import { UserService } from '../../services/user-service'; import { ConversationService } from '../../services/conversation-service'; import { ERROR_MESSAGES } from '../../constants/messages'; +import { createLogger } from '../../utils/logger'; import type { Env, TelegramUpdate } from '../../types'; +const logger = createLogger('message-handler'); + /** * 메시지 처리 핸들러 */ @@ -44,7 +47,7 @@ export async function handleMessage( message.from.username ); } catch (dbError) { - console.error('[handleMessage] 사용자 DB 오류:', dbError); + logger.error('사용자 DB 오류', dbError as Error, { telegramUserId }); await sendMessage( env.BOT_TOKEN, chatId, @@ -80,7 +83,7 @@ export async function handleMessage( await sendMessage(env.BOT_TOKEN, chatId, result.message); return; } catch (error) { - console.error('[handleMessage] 서버 삭제 처리 오류:', error); + logger.error('서버 삭제 처리 오류', error as Error, { orderId: JSON.parse(deleteSessionData).orderId }); await sendMessage( env.BOT_TOKEN, chatId, @@ -107,7 +110,7 @@ export async function handleMessage( return; } } catch (error) { - console.error('[handleMessage] 삭제 세션 취소 오류:', error); + logger.error('삭제 세션 취소 오류', error as Error, { telegramUserId }); } } @@ -202,7 +205,7 @@ export async function handleMessage( ); return; } catch (error) { - console.error('[handleMessage] 서버 신청 처리 오류:', error); + logger.error('서버 신청 처리 오류', error as Error, { telegramUserId }); await sendMessage( env.BOT_TOKEN, chatId, @@ -248,7 +251,7 @@ export async function handleMessage( // 10. 응답 전송 (키보드 포함 여부 확인) if (result.keyboardData) { - console.log('[Webhook] Keyboard data received:', result.keyboardData.type); + logger.info('Keyboard data received', { type: result.keyboardData.type }); if (result.keyboardData.type === 'domain_register') { const { domain, price } = result.keyboardData; const callbackData = `domain_reg:${domain}:${price}`; @@ -272,7 +275,7 @@ export async function handleMessage( ]); } else { // TypeScript exhaustiveness check - should never reach here - console.warn('[Webhook] Unknown keyboard type:', (result.keyboardData as { type: string }).type); + logger.warn('Unknown keyboard type', { type: (result.keyboardData as { type: string }).type }); await sendMessage(env.BOT_TOKEN, chatId, finalResponse); } } else { @@ -280,7 +283,7 @@ export async function handleMessage( } } catch (error) { - console.error('[handleMessage] 처리 오류:', error); + logger.error('처리 오류', error as Error, { chatId, telegramUserId }); await sendMessage( env.BOT_TOKEN, chatId, diff --git a/src/routes/webhook.ts b/src/routes/webhook.ts index b6bb823..74c302f 100644 --- a/src/routes/webhook.ts +++ b/src/routes/webhook.ts @@ -2,6 +2,9 @@ import type { Env } from '../types'; import { validateWebhookRequest } from '../security'; import { handleCallbackQuery } from './handlers/callback-handler'; import { handleMessage } from './handlers/message-handler'; +import { createLogger } from '../utils/logger'; + +const logger = createLogger('webhook'); /** * Telegram Webhook 요청 처리 @@ -10,7 +13,7 @@ export async function handleWebhook(request: Request, env: Env): Promise(); if (!pendingTx) { - console.log('[matchPendingDeposit] 매칭되는 pending 거래 없음'); + logger.info('매칭되는 pending 거래 없음', { + depositorName: notification.depositorName.slice(0, 7), + amount: notification.amount + }); return null; } - console.log('[matchPendingDeposit] 매칭 발견:', pendingTx); + logger.info('매칭 발견', { + transactionId: pendingTx.id, + userId: pendingTx.user_id, + amount: pendingTx.amount + }); try { // Optimistic Locking으로 동시성 안전한 매칭 처리 @@ -108,7 +115,7 @@ export async function matchPendingDeposit( }); }); - console.log('[matchPendingDeposit] 매칭 완료:', { + logger.info('매칭 완료', { transactionId: pendingTx.id, userId: pendingTx.user_id, amount: pendingTx.amount, @@ -125,8 +132,12 @@ export async function matchPendingDeposit( transactionId: pendingTx.id, userId: pendingTx.user_id, }); + } else { + logger.error('DB 업데이트 실패', error as Error, { + transactionId: pendingTx.id, + userId: pendingTx.user_id, + }); } - console.error('[matchPendingDeposit] DB 업데이트 실패:', error); throw error; } } diff --git a/src/utils/circuit-breaker.ts b/src/utils/circuit-breaker.ts index 5ac7830..4fd280d 100644 --- a/src/utils/circuit-breaker.ts +++ b/src/utils/circuit-breaker.ts @@ -1,5 +1,8 @@ import { metrics } from './metrics'; import { notifyAdmin, NotificationOptions } from '../services/notification'; +import { createLogger } from './logger'; + +const logger = createLogger('circuit-breaker'); /** * Circuit Breaker pattern implementation @@ -99,7 +102,7 @@ export class CircuitBreaker { this.serviceName = options?.serviceName ?? 'unknown'; this.notification = options?.notification; - console.log('[CircuitBreaker] Initialized', { + logger.info('Initialized', { serviceName: this.serviceName, failureThreshold: this.failureThreshold, resetTimeoutMs: this.resetTimeoutMs, @@ -146,7 +149,7 @@ export class CircuitBreaker { * Manually reset the circuit to closed state */ reset(): void { - console.log('[CircuitBreaker] Manual reset'); + logger.info('Manual reset', { service: this.serviceName }); this.state = CircuitState.CLOSED; this.failures = []; this.openedAt = null; @@ -178,7 +181,10 @@ export class CircuitBreaker { const elapsed = now - this.openedAt; if (elapsed >= this.resetTimeoutMs) { - console.log('[CircuitBreaker] Reset timeout reached, transitioning to HALF_OPEN'); + logger.info('Reset timeout reached, transitioning to HALF_OPEN', { + service: this.serviceName, + elapsedMs: elapsed + }); this.state = CircuitState.HALF_OPEN; // 상태 메트릭 기록 (HALF_OPEN) @@ -194,7 +200,9 @@ export class CircuitBreaker { this.successCount++; if (this.state === CircuitState.HALF_OPEN) { - console.log('[CircuitBreaker] Half-open test succeeded, closing circuit'); + logger.info('Half-open test succeeded, closing circuit', { + service: this.serviceName + }); this.state = CircuitState.CLOSED; this.failures = []; this.openedAt = null; @@ -218,7 +226,10 @@ export class CircuitBreaker { // If in half-open state, one failure reopens the circuit if (this.state === CircuitState.HALF_OPEN) { - console.log('[CircuitBreaker] Half-open test failed, reopening circuit'); + logger.warn('Half-open test failed, reopening circuit', { + service: this.serviceName, + error: error.message + }); this.state = CircuitState.OPEN; this.openedAt = now; @@ -246,9 +257,11 @@ export class CircuitBreaker { // Check if we should open the circuit if (this.state === CircuitState.CLOSED) { if (this.failures.length >= this.failureThreshold) { - console.log( - `[CircuitBreaker] Failure threshold (${this.failureThreshold}) exceeded, opening circuit` - ); + logger.warn('Failure threshold exceeded, opening circuit', { + service: this.serviceName, + failureThreshold: this.failureThreshold, + currentFailures: this.failures.length + }); this.state = CircuitState.OPEN; this.openedAt = now; @@ -291,7 +304,9 @@ export class CircuitBreaker { 'Circuit breaker is open - service unavailable', this.state ); - console.log('[CircuitBreaker] Request blocked - circuit is OPEN'); + logger.warn('Request blocked - circuit is OPEN', { + service: this.serviceName + }); throw error; } @@ -315,10 +330,11 @@ export class CircuitBreaker { this.onFailure(err); // Log failure - console.error( - `[CircuitBreaker] Operation failed (${this.failures.length}/${this.failureThreshold} failures):`, - err.message - ); + logger.error('Operation failed', err, { + service: this.serviceName, + failures: this.failures.length, + threshold: this.failureThreshold + }); // Re-throw the original error throw err; diff --git a/src/utils/retry.ts b/src/utils/retry.ts index 998b91f..3de4a01 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -12,6 +12,9 @@ import { metrics } from './metrics'; import { notifyAdmin, NotificationOptions } from '../services/notification'; +import { createLogger } from './logger'; + +const logger = createLogger('retry'); /** * Configuration options for retry behavior @@ -123,7 +126,11 @@ export async function retryWithBackoff( // Log success if this was a retry if (attempt > 0) { - console.log(`[Retry] Success on attempt ${attempt + 1}/${maxRetries + 1}`); + logger.info('Success on retry', { + service: serviceName, + attempt: attempt + 1, + totalAttempts: maxRetries + 1 + }); } return result; @@ -132,10 +139,10 @@ export async function retryWithBackoff( // If this was the last attempt, throw RetryError if (attempt === maxRetries) { - console.error( - `[Retry] All ${maxRetries + 1} attempts failed. Last error:`, - lastError.message - ); + logger.error('All attempts failed', lastError, { + service: serviceName, + totalAttempts: maxRetries + 1 + }); // Send admin notification if configured if (notification) { @@ -176,10 +183,13 @@ export async function retryWithBackoff( jitter ); - console.log( - `[Retry] Attempt ${attempt + 1}/${maxRetries + 1} failed. Retrying in ${delay}ms...`, - lastError.message - ); + logger.warn('Attempt failed, retrying', { + service: serviceName, + attempt: attempt + 1, + totalAttempts: maxRetries + 1, + delayMs: delay, + error: lastError.message + }); // Wait before next retry await sleep(delay);