refactor: centralize auth middleware and standardize logging
1. API Key Middleware (api.ts) - Create apiKeyAuth Hono middleware with timing-safe comparison - Apply to /deposit/balance and /deposit/deduct routes - Remove duplicate requireApiKey() calls from handlers - Reduce ~15 lines of duplicated code 2. Logger Standardization (6 files, 27 replacements) - webhook.ts: 2 console.error → logger - message-handler.ts: 7 console → logger - deposit-matcher.ts: 4 console → logger - n8n-service.ts: 3 console.error → logger - circuit-breaker.ts: 8 console → logger - retry.ts: 3 console → logger Benefits: - Single point of auth change - Structured logging with context - Better observability in production Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,7 @@ JSON:`;
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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) {
|
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})` };
|
return { error: `n8n 호출 실패 (${response.status})` };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +133,7 @@ export async function callN8n(
|
|||||||
|
|
||||||
return parseResult.data;
|
return parseResult.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('n8n fetch error:', error);
|
logger.error('n8n fetch error', error as Error, { userId, type });
|
||||||
return { error: 'n8n 연결 실패' };
|
return { error: 'n8n 연결 실패' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
import { cors } from 'hono/cors';
|
import { cors } from 'hono/cors';
|
||||||
|
import { createMiddleware } from 'hono/factory';
|
||||||
import { Env } from '../types';
|
import { Env } from '../types';
|
||||||
import { sendMessage } from '../telegram';
|
import { sendMessage } from '../telegram';
|
||||||
import {
|
import {
|
||||||
@@ -44,17 +45,20 @@ const ChatApiBodySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API Key 인증 검증 (Timing-safe comparison으로 타이밍 공격 방지)
|
* API Key 인증 미들웨어 (Timing-safe comparison으로 타이밍 공격 방지)
|
||||||
* @returns 인증 실패 시 Response, 성공 시 null
|
* X-API-Key 헤더로 DEPOSIT_API_SECRET 검증
|
||||||
*/
|
*/
|
||||||
function requireApiKey(request: Request, env: Env): Response | null {
|
const apiKeyAuth = createMiddleware<{ Bindings: Env }>(async (c, next) => {
|
||||||
const apiSecret = env.DEPOSIT_API_SECRET;
|
const apiSecret = c.env.DEPOSIT_API_SECRET;
|
||||||
const authHeader = request.headers.get('X-API-Key');
|
const authHeader = c.req.header('X-API-Key');
|
||||||
|
|
||||||
if (!apiSecret || !timingSafeEqual(authHeader, apiSecret)) {
|
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 헤더 생성
|
* CORS 헤더 생성
|
||||||
@@ -101,17 +105,12 @@ async function getOrCreateUser(
|
|||||||
/**
|
/**
|
||||||
* GET /api/deposit/balance - 잔액 조회 (namecheap-api 전용)
|
* GET /api/deposit/balance - 잔액 조회 (namecheap-api 전용)
|
||||||
*
|
*
|
||||||
* @param request - HTTP Request
|
|
||||||
* @param env - Environment bindings
|
* @param env - Environment bindings
|
||||||
* @param url - Parsed URL
|
* @param url - Parsed URL
|
||||||
* @returns JSON response with balance
|
* @returns JSON response with balance
|
||||||
*/
|
*/
|
||||||
async function handleDepositBalance(request: Request, env: Env, url: URL): Promise<Response> {
|
async function handleDepositBalance(env: Env, url: URL): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
// API Key 인증
|
|
||||||
const authError = requireApiKey(request, env);
|
|
||||||
if (authError) return authError;
|
|
||||||
|
|
||||||
const telegramId = url.searchParams.get('telegram_id');
|
const telegramId = url.searchParams.get('telegram_id');
|
||||||
if (!telegramId) {
|
if (!telegramId) {
|
||||||
return Response.json({ error: 'telegram_id required' }, { status: 400 });
|
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<Response> {
|
async function handleDepositDeduct(request: Request, env: Env): Promise<Response> {
|
||||||
try {
|
try {
|
||||||
// API Key 인증
|
|
||||||
const authError = requireApiKey(request, env);
|
|
||||||
if (authError) return authError;
|
|
||||||
|
|
||||||
// JSON 파싱 (별도 에러 핸들링)
|
// JSON 파싱 (별도 에러 핸들링)
|
||||||
let jsonData: unknown;
|
let jsonData: unknown;
|
||||||
try {
|
try {
|
||||||
@@ -869,14 +864,14 @@ api.use('/contact', cors({
|
|||||||
allowHeaders: ['Content-Type'],
|
allowHeaders: ['Content-Type'],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// GET /deposit/balance - 잔액 조회 (namecheap-api 전용)
|
// GET /deposit/balance - 잔액 조회 (namecheap-api 전용, API Key 필요)
|
||||||
api.get('/deposit/balance', async (c) => {
|
api.get('/deposit/balance', apiKeyAuth, async (c) => {
|
||||||
const url = new URL(c.req.url);
|
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 전용)
|
// POST /deposit/deduct - 잔액 차감 (namecheap-api 전용, API Key 필요)
|
||||||
api.post('/deposit/deduct', async (c) => {
|
api.post('/deposit/deduct', apiKeyAuth, async (c) => {
|
||||||
return await handleDepositDeduct(c.req.raw, c.env);
|
return await handleDepositDeduct(c.req.raw, c.env);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ import { handleCommand } from '../../commands';
|
|||||||
import { UserService } from '../../services/user-service';
|
import { UserService } from '../../services/user-service';
|
||||||
import { ConversationService } from '../../services/conversation-service';
|
import { ConversationService } from '../../services/conversation-service';
|
||||||
import { ERROR_MESSAGES } from '../../constants/messages';
|
import { ERROR_MESSAGES } from '../../constants/messages';
|
||||||
|
import { createLogger } from '../../utils/logger';
|
||||||
import type { Env, TelegramUpdate } from '../../types';
|
import type { Env, TelegramUpdate } from '../../types';
|
||||||
|
|
||||||
|
const logger = createLogger('message-handler');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 메시지 처리 핸들러
|
* 메시지 처리 핸들러
|
||||||
*/
|
*/
|
||||||
@@ -44,7 +47,7 @@ export async function handleMessage(
|
|||||||
message.from.username
|
message.from.username
|
||||||
);
|
);
|
||||||
} catch (dbError) {
|
} catch (dbError) {
|
||||||
console.error('[handleMessage] 사용자 DB 오류:', dbError);
|
logger.error('사용자 DB 오류', dbError as Error, { telegramUserId });
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
env.BOT_TOKEN,
|
env.BOT_TOKEN,
|
||||||
chatId,
|
chatId,
|
||||||
@@ -80,7 +83,7 @@ export async function handleMessage(
|
|||||||
await sendMessage(env.BOT_TOKEN, chatId, result.message);
|
await sendMessage(env.BOT_TOKEN, chatId, result.message);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[handleMessage] 서버 삭제 처리 오류:', error);
|
logger.error('서버 삭제 처리 오류', error as Error, { orderId: JSON.parse(deleteSessionData).orderId });
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
env.BOT_TOKEN,
|
env.BOT_TOKEN,
|
||||||
chatId,
|
chatId,
|
||||||
@@ -107,7 +110,7 @@ export async function handleMessage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[handleMessage] 삭제 세션 취소 오류:', error);
|
logger.error('삭제 세션 취소 오류', error as Error, { telegramUserId });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,7 +205,7 @@ export async function handleMessage(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[handleMessage] 서버 신청 처리 오류:', error);
|
logger.error('서버 신청 처리 오류', error as Error, { telegramUserId });
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
env.BOT_TOKEN,
|
env.BOT_TOKEN,
|
||||||
chatId,
|
chatId,
|
||||||
@@ -248,7 +251,7 @@ export async function handleMessage(
|
|||||||
|
|
||||||
// 10. 응답 전송 (키보드 포함 여부 확인)
|
// 10. 응답 전송 (키보드 포함 여부 확인)
|
||||||
if (result.keyboardData) {
|
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') {
|
if (result.keyboardData.type === 'domain_register') {
|
||||||
const { domain, price } = result.keyboardData;
|
const { domain, price } = result.keyboardData;
|
||||||
const callbackData = `domain_reg:${domain}:${price}`;
|
const callbackData = `domain_reg:${domain}:${price}`;
|
||||||
@@ -272,7 +275,7 @@ export async function handleMessage(
|
|||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
// TypeScript exhaustiveness check - should never reach here
|
// 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);
|
await sendMessage(env.BOT_TOKEN, chatId, finalResponse);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -280,7 +283,7 @@ export async function handleMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[handleMessage] 처리 오류:', error);
|
logger.error('처리 오류', error as Error, { chatId, telegramUserId });
|
||||||
await sendMessage(
|
await sendMessage(
|
||||||
env.BOT_TOKEN,
|
env.BOT_TOKEN,
|
||||||
chatId,
|
chatId,
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import type { Env } from '../types';
|
|||||||
import { validateWebhookRequest } from '../security';
|
import { validateWebhookRequest } from '../security';
|
||||||
import { handleCallbackQuery } from './handlers/callback-handler';
|
import { handleCallbackQuery } from './handlers/callback-handler';
|
||||||
import { handleMessage } from './handlers/message-handler';
|
import { handleMessage } from './handlers/message-handler';
|
||||||
|
import { createLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
const logger = createLogger('webhook');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Telegram Webhook 요청 처리
|
* Telegram Webhook 요청 처리
|
||||||
@@ -10,7 +13,7 @@ export async function handleWebhook(request: Request, env: Env): Promise<Respons
|
|||||||
const validation = await validateWebhookRequest(request, env);
|
const validation = await validateWebhookRequest(request, env);
|
||||||
|
|
||||||
if (!validation.valid) {
|
if (!validation.valid) {
|
||||||
console.error('[Webhook] 검증 실패:', validation.error);
|
logger.error('검증 실패', new Error(validation.error || 'Unknown validation error'));
|
||||||
return new Response(validation.error, { status: 401 });
|
return new Response(validation.error, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +28,7 @@ export async function handleWebhook(request: Request, env: Env): Promise<Respons
|
|||||||
await handleMessage(env, update);
|
await handleMessage(env, update);
|
||||||
return new Response('OK');
|
return new Response('OK');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Webhook] 메시지 처리 오류:', error);
|
logger.error('메시지 처리 오류', error as Error);
|
||||||
return new Response('Error', { status: 500 });
|
return new Response('Error', { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,11 +55,18 @@ export async function matchPendingDeposit(
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
if (!pendingTx) {
|
if (!pendingTx) {
|
||||||
console.log('[matchPendingDeposit] 매칭되는 pending 거래 없음');
|
logger.info('매칭되는 pending 거래 없음', {
|
||||||
|
depositorName: notification.depositorName.slice(0, 7),
|
||||||
|
amount: notification.amount
|
||||||
|
});
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[matchPendingDeposit] 매칭 발견:', pendingTx);
|
logger.info('매칭 발견', {
|
||||||
|
transactionId: pendingTx.id,
|
||||||
|
userId: pendingTx.user_id,
|
||||||
|
amount: pendingTx.amount
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistic Locking으로 동시성 안전한 매칭 처리
|
// Optimistic Locking으로 동시성 안전한 매칭 처리
|
||||||
@@ -108,7 +115,7 @@ export async function matchPendingDeposit(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[matchPendingDeposit] 매칭 완료:', {
|
logger.info('매칭 완료', {
|
||||||
transactionId: pendingTx.id,
|
transactionId: pendingTx.id,
|
||||||
userId: pendingTx.user_id,
|
userId: pendingTx.user_id,
|
||||||
amount: pendingTx.amount,
|
amount: pendingTx.amount,
|
||||||
@@ -125,8 +132,12 @@ export async function matchPendingDeposit(
|
|||||||
transactionId: pendingTx.id,
|
transactionId: pendingTx.id,
|
||||||
userId: pendingTx.user_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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { metrics } from './metrics';
|
import { metrics } from './metrics';
|
||||||
import { notifyAdmin, NotificationOptions } from '../services/notification';
|
import { notifyAdmin, NotificationOptions } from '../services/notification';
|
||||||
|
import { createLogger } from './logger';
|
||||||
|
|
||||||
|
const logger = createLogger('circuit-breaker');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Circuit Breaker pattern implementation
|
* Circuit Breaker pattern implementation
|
||||||
@@ -99,7 +102,7 @@ export class CircuitBreaker {
|
|||||||
this.serviceName = options?.serviceName ?? 'unknown';
|
this.serviceName = options?.serviceName ?? 'unknown';
|
||||||
this.notification = options?.notification;
|
this.notification = options?.notification;
|
||||||
|
|
||||||
console.log('[CircuitBreaker] Initialized', {
|
logger.info('Initialized', {
|
||||||
serviceName: this.serviceName,
|
serviceName: this.serviceName,
|
||||||
failureThreshold: this.failureThreshold,
|
failureThreshold: this.failureThreshold,
|
||||||
resetTimeoutMs: this.resetTimeoutMs,
|
resetTimeoutMs: this.resetTimeoutMs,
|
||||||
@@ -146,7 +149,7 @@ export class CircuitBreaker {
|
|||||||
* Manually reset the circuit to closed state
|
* Manually reset the circuit to closed state
|
||||||
*/
|
*/
|
||||||
reset(): void {
|
reset(): void {
|
||||||
console.log('[CircuitBreaker] Manual reset');
|
logger.info('Manual reset', { service: this.serviceName });
|
||||||
this.state = CircuitState.CLOSED;
|
this.state = CircuitState.CLOSED;
|
||||||
this.failures = [];
|
this.failures = [];
|
||||||
this.openedAt = null;
|
this.openedAt = null;
|
||||||
@@ -178,7 +181,10 @@ export class CircuitBreaker {
|
|||||||
const elapsed = now - this.openedAt;
|
const elapsed = now - this.openedAt;
|
||||||
|
|
||||||
if (elapsed >= this.resetTimeoutMs) {
|
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;
|
this.state = CircuitState.HALF_OPEN;
|
||||||
|
|
||||||
// 상태 메트릭 기록 (HALF_OPEN)
|
// 상태 메트릭 기록 (HALF_OPEN)
|
||||||
@@ -194,7 +200,9 @@ export class CircuitBreaker {
|
|||||||
this.successCount++;
|
this.successCount++;
|
||||||
|
|
||||||
if (this.state === CircuitState.HALF_OPEN) {
|
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.state = CircuitState.CLOSED;
|
||||||
this.failures = [];
|
this.failures = [];
|
||||||
this.openedAt = null;
|
this.openedAt = null;
|
||||||
@@ -218,7 +226,10 @@ export class CircuitBreaker {
|
|||||||
|
|
||||||
// If in half-open state, one failure reopens the circuit
|
// If in half-open state, one failure reopens the circuit
|
||||||
if (this.state === CircuitState.HALF_OPEN) {
|
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.state = CircuitState.OPEN;
|
||||||
this.openedAt = now;
|
this.openedAt = now;
|
||||||
|
|
||||||
@@ -246,9 +257,11 @@ export class CircuitBreaker {
|
|||||||
// Check if we should open the circuit
|
// Check if we should open the circuit
|
||||||
if (this.state === CircuitState.CLOSED) {
|
if (this.state === CircuitState.CLOSED) {
|
||||||
if (this.failures.length >= this.failureThreshold) {
|
if (this.failures.length >= this.failureThreshold) {
|
||||||
console.log(
|
logger.warn('Failure threshold exceeded, opening circuit', {
|
||||||
`[CircuitBreaker] Failure threshold (${this.failureThreshold}) exceeded, opening circuit`
|
service: this.serviceName,
|
||||||
);
|
failureThreshold: this.failureThreshold,
|
||||||
|
currentFailures: this.failures.length
|
||||||
|
});
|
||||||
this.state = CircuitState.OPEN;
|
this.state = CircuitState.OPEN;
|
||||||
this.openedAt = now;
|
this.openedAt = now;
|
||||||
|
|
||||||
@@ -291,7 +304,9 @@ export class CircuitBreaker {
|
|||||||
'Circuit breaker is open - service unavailable',
|
'Circuit breaker is open - service unavailable',
|
||||||
this.state
|
this.state
|
||||||
);
|
);
|
||||||
console.log('[CircuitBreaker] Request blocked - circuit is OPEN');
|
logger.warn('Request blocked - circuit is OPEN', {
|
||||||
|
service: this.serviceName
|
||||||
|
});
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,10 +330,11 @@ export class CircuitBreaker {
|
|||||||
this.onFailure(err);
|
this.onFailure(err);
|
||||||
|
|
||||||
// Log failure
|
// Log failure
|
||||||
console.error(
|
logger.error('Operation failed', err, {
|
||||||
`[CircuitBreaker] Operation failed (${this.failures.length}/${this.failureThreshold} failures):`,
|
service: this.serviceName,
|
||||||
err.message
|
failures: this.failures.length,
|
||||||
);
|
threshold: this.failureThreshold
|
||||||
|
});
|
||||||
|
|
||||||
// Re-throw the original error
|
// Re-throw the original error
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
|
|
||||||
import { metrics } from './metrics';
|
import { metrics } from './metrics';
|
||||||
import { notifyAdmin, NotificationOptions } from '../services/notification';
|
import { notifyAdmin, NotificationOptions } from '../services/notification';
|
||||||
|
import { createLogger } from './logger';
|
||||||
|
|
||||||
|
const logger = createLogger('retry');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration options for retry behavior
|
* Configuration options for retry behavior
|
||||||
@@ -123,7 +126,11 @@ export async function retryWithBackoff<T>(
|
|||||||
|
|
||||||
// Log success if this was a retry
|
// Log success if this was a retry
|
||||||
if (attempt > 0) {
|
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;
|
return result;
|
||||||
@@ -132,10 +139,10 @@ export async function retryWithBackoff<T>(
|
|||||||
|
|
||||||
// If this was the last attempt, throw RetryError
|
// If this was the last attempt, throw RetryError
|
||||||
if (attempt === maxRetries) {
|
if (attempt === maxRetries) {
|
||||||
console.error(
|
logger.error('All attempts failed', lastError, {
|
||||||
`[Retry] All ${maxRetries + 1} attempts failed. Last error:`,
|
service: serviceName,
|
||||||
lastError.message
|
totalAttempts: maxRetries + 1
|
||||||
);
|
});
|
||||||
|
|
||||||
// Send admin notification if configured
|
// Send admin notification if configured
|
||||||
if (notification) {
|
if (notification) {
|
||||||
@@ -176,10 +183,13 @@ export async function retryWithBackoff<T>(
|
|||||||
jitter
|
jitter
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(
|
logger.warn('Attempt failed, retrying', {
|
||||||
`[Retry] Attempt ${attempt + 1}/${maxRetries + 1} failed. Retrying in ${delay}ms...`,
|
service: serviceName,
|
||||||
lastError.message
|
attempt: attempt + 1,
|
||||||
);
|
totalAttempts: maxRetries + 1,
|
||||||
|
delayMs: delay,
|
||||||
|
error: lastError.message
|
||||||
|
});
|
||||||
|
|
||||||
// Wait before next retry
|
// Wait before next retry
|
||||||
await sleep(delay);
|
await sleep(delay);
|
||||||
|
|||||||
Reference in New Issue
Block a user