feat(phase-5-3): 모니터링 강화
logger.ts, metrics.ts, /api/metrics 추가 Version: e3bcb4ae
This commit is contained in:
@@ -276,6 +276,7 @@ wrangler d1 execute telegram-conversations --command "SELECT * FROM users LIMIT
|
||||
| `/api/deposit/*` | X-API-Key 헤더 | namecheap-api 전용 |
|
||||
| `/api/test` | WEBHOOK_SECRET | 테스트 전용 |
|
||||
| `/api/contact` | CORS | hosting.anvil.it.com만 |
|
||||
| `/api/metrics` | Bearer Token (WEBHOOK_SECRET) | 관리자 전용 (Circuit Breaker 상태) |
|
||||
|
||||
**CORS 정책:**
|
||||
```typescript
|
||||
|
||||
591
docs/logger-guide.md
Normal file
591
docs/logger-guide.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# Logger 사용 가이드
|
||||
|
||||
구조화된 로깅 유틸리티 (`src/utils/logger.ts`) 사용 방법 안내
|
||||
|
||||
## 목차
|
||||
|
||||
1. [개요](#개요)
|
||||
2. [기본 사용법](#기본-사용법)
|
||||
3. [로그 레벨](#로그-레벨)
|
||||
4. [성능 측정](#성능-측정)
|
||||
5. [사용자 컨텍스트](#사용자-컨텍스트)
|
||||
6. [출력 형식](#출력-형식)
|
||||
7. [마이그레이션 가이드](#마이그레이션-가이드)
|
||||
8. [Best Practices](#best-practices)
|
||||
|
||||
---
|
||||
|
||||
## 개요
|
||||
|
||||
### 주요 특징
|
||||
|
||||
- ✅ **JSON 기반 구조화 로그**: 프로덕션 환경에서 로그 수집 시스템 연동
|
||||
- ✅ **환경별 자동 전환**: 개발(읽기 쉬운 포맷) ↔ 프로덕션(JSON)
|
||||
- ✅ **타입 안전성**: TypeScript strict mode 지원
|
||||
- ✅ **성능 측정**: 내장 타이머로 실행 시간 자동 계산
|
||||
- ✅ **에러 안전성**: 로깅 실패가 메인 로직 중단시키지 않음
|
||||
- ✅ **사용자 컨텍스트**: 사용자 ID 자동 포함
|
||||
|
||||
### 로그 레벨
|
||||
|
||||
| 레벨 | 우선순위 | 용도 | 예시 |
|
||||
|------|---------|------|------|
|
||||
| `DEBUG` | 0 | 디버깅 정보 (개발 전용) | 함수 호출, 파라미터 값 |
|
||||
| `INFO` | 1 | 일반 정보 | 요청 처리, 성공 응답 |
|
||||
| `WARN` | 2 | 경고 (주의 필요) | API 지연, 폴백 사용 |
|
||||
| `ERROR` | 3 | 에러 (복구 가능) | API 실패, 재시도 |
|
||||
| `FATAL` | 4 | 치명적 에러 | 시스템 장애, DB 연결 실패 |
|
||||
|
||||
---
|
||||
|
||||
## 기본 사용법
|
||||
|
||||
### 1. 로거 생성
|
||||
|
||||
```typescript
|
||||
import { createLogger } from './utils/logger';
|
||||
|
||||
// 서비스별 로거 생성
|
||||
const logger = createLogger('openai');
|
||||
```
|
||||
|
||||
### 2. 로그 출력
|
||||
|
||||
```typescript
|
||||
// 일반 정보
|
||||
logger.info('AI 응답 생성 시작');
|
||||
|
||||
// 컨텍스트 포함
|
||||
logger.info('API 호출 완료', {
|
||||
model: 'gpt-4o-mini',
|
||||
tokensUsed: 1000,
|
||||
});
|
||||
|
||||
// 경고
|
||||
logger.warn('API 응답 지연', {
|
||||
endpoint: '/chat/completions',
|
||||
responseTime: 3500,
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 에러 처리
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await callOpenAI();
|
||||
} catch (error) {
|
||||
logger.error('OpenAI API 호출 실패', error as Error, {
|
||||
model: 'gpt-4o-mini',
|
||||
retryCount: 3,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 로그 레벨
|
||||
|
||||
### DEBUG (개발 전용)
|
||||
|
||||
```typescript
|
||||
import { createDebugLogger } from './utils/logger';
|
||||
|
||||
const logger = createDebugLogger('debug-service');
|
||||
|
||||
logger.debug('함수 호출', {
|
||||
functionName: 'processData',
|
||||
arguments: [1, 2, 3],
|
||||
});
|
||||
```
|
||||
|
||||
### INFO (기본)
|
||||
|
||||
```typescript
|
||||
logger.info('메시지 수신', {
|
||||
userId: '123456789',
|
||||
messageType: 'text',
|
||||
length: 42,
|
||||
});
|
||||
```
|
||||
|
||||
### WARN (주의 필요)
|
||||
|
||||
```typescript
|
||||
logger.warn('Circuit breaker 경고', {
|
||||
failureCount: 2,
|
||||
threshold: 3,
|
||||
});
|
||||
```
|
||||
|
||||
### ERROR (복구 가능)
|
||||
|
||||
```typescript
|
||||
try {
|
||||
await apiCall();
|
||||
} catch (error) {
|
||||
logger.error('API 호출 실패', error as Error, {
|
||||
endpoint: '/api/data',
|
||||
retryAttempt: 2,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### FATAL (치명적)
|
||||
|
||||
```typescript
|
||||
logger.fatal('데이터베이스 연결 실패', error as Error, {
|
||||
database: 'telegram-conversations',
|
||||
connectionString: 'D1',
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 성능 측정
|
||||
|
||||
### 기본 사용
|
||||
|
||||
```typescript
|
||||
const endTimer = logger.startTimer('데이터 처리 완료', {
|
||||
recordCount: 10000,
|
||||
});
|
||||
|
||||
await processData();
|
||||
|
||||
endTimer(); // duration이 자동으로 로그에 포함됨
|
||||
```
|
||||
|
||||
### OpenAI API 호출 예시
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('openai');
|
||||
|
||||
async function generateResponse() {
|
||||
const end = logger.startTimer('AI 응답 생성 완료');
|
||||
|
||||
try {
|
||||
const response = await callOpenAI();
|
||||
end(); // 성공 시 duration 로그
|
||||
return response;
|
||||
} catch (error) {
|
||||
logger.error('AI 응답 생성 실패', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 출력 예시
|
||||
|
||||
```
|
||||
ℹ️ [2026-01-19T16:45:23.123Z] INFO [openai] AI 응답 생성 완료 {"model":"gpt-4o-mini"} (1234ms)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 사용자 컨텍스트
|
||||
|
||||
### 사용자 ID 자동 포함
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('telegram');
|
||||
|
||||
// 사용자 로거 생성 (userId 자동 포함)
|
||||
const userLogger = logger.withUser('821596605');
|
||||
|
||||
userLogger.info('메시지 수신', {
|
||||
messageType: 'text',
|
||||
length: 42,
|
||||
});
|
||||
|
||||
userLogger.info('AI 응답 전송', {
|
||||
responseLength: 150,
|
||||
});
|
||||
```
|
||||
|
||||
### 출력 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T16:45:23.123Z",
|
||||
"level": "INFO",
|
||||
"service": "telegram",
|
||||
"message": "메시지 수신",
|
||||
"userId": "821596605",
|
||||
"context": {
|
||||
"messageType": "text",
|
||||
"length": 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 출력 형식
|
||||
|
||||
### 개발 환경 (human-readable)
|
||||
|
||||
```
|
||||
ℹ️ [2026-01-19T16:45:23.123Z] INFO [openai] AI 응답 생성 시작 {"userId":"123","model":"gpt-4"}
|
||||
⚠️ [2026-01-19T16:45:25.456Z] WARN [openai] API 응답 지연 {"responseTime":3500}
|
||||
❌ [2026-01-19T16:45:27.789Z] ERROR [openai] API 호출 실패 {"retryCount":3}
|
||||
Error: Error: Connection timeout
|
||||
Stack: Error: Connection timeout
|
||||
at callOpenAI (openai-service.ts:45:11)
|
||||
```
|
||||
|
||||
### 프로덕션 (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T16:45:23.123Z",
|
||||
"level": "INFO",
|
||||
"service": "openai",
|
||||
"message": "AI 응답 생성 시작",
|
||||
"context": {
|
||||
"userId": "123",
|
||||
"model": "gpt-4"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T16:45:27.789Z",
|
||||
"level": "ERROR",
|
||||
"service": "openai",
|
||||
"message": "API 호출 실패",
|
||||
"context": {
|
||||
"retryCount": 3
|
||||
},
|
||||
"error": {
|
||||
"name": "Error",
|
||||
"message": "Connection timeout",
|
||||
"stack": "Error: Connection timeout\n at callOpenAI (openai-service.ts:45:11)"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 가이드
|
||||
|
||||
### 기존 코드
|
||||
|
||||
```typescript
|
||||
// ❌ 기존: console.log
|
||||
console.log('[OpenAI] AI 응답 생성 시작');
|
||||
console.log('[OpenAI] tool_calls:', JSON.stringify(toolCalls));
|
||||
console.error('[OpenAI] API 호출 실패:', error);
|
||||
```
|
||||
|
||||
### 새 코드
|
||||
|
||||
```typescript
|
||||
// ✅ 신규: createLogger
|
||||
const logger = createLogger('openai');
|
||||
|
||||
logger.info('AI 응답 생성 시작');
|
||||
logger.debug('tool_calls 수신', { toolCalls });
|
||||
logger.error('API 호출 실패', error as Error, { model: 'gpt-4o-mini' });
|
||||
```
|
||||
|
||||
### 서비스별 변환 예시
|
||||
|
||||
#### openai-service.ts
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
console.log('[OpenAI] tool_calls:', assistantMessage.tool_calls);
|
||||
console.error('[OpenAI] Circuit breaker open:', error.message);
|
||||
|
||||
// After
|
||||
const logger = createLogger('openai');
|
||||
logger.debug('tool_calls 수신', { toolCalls: assistantMessage.tool_calls });
|
||||
logger.error('Circuit breaker 열림', error as Error);
|
||||
```
|
||||
|
||||
#### deposit-agent.ts
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
console.log(`[DepositAgent] Function call: ${funcName}`, funcArgs);
|
||||
console.error('[DepositAgent] Error:', error);
|
||||
|
||||
// After
|
||||
const logger = createLogger('deposit');
|
||||
logger.info('Function call 실행', { functionName: funcName, arguments: funcArgs });
|
||||
logger.error('예치금 처리 실패', error as Error);
|
||||
```
|
||||
|
||||
#### services/notification.ts
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
console.log('[Notification] 관리자 ID가 설정되지 않아 알림을 건너뜁니다.');
|
||||
console.error('[Notification] 알림 전송 중 오류 발생:', error);
|
||||
|
||||
// After
|
||||
const logger = createLogger('notification');
|
||||
logger.warn('관리자 ID 미설정', { notificationType: type });
|
||||
logger.error('알림 전송 실패', error as Error, { type, service: details.service });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. 서비스별 로거 생성
|
||||
|
||||
```typescript
|
||||
// ✅ Good: 서비스별 로거
|
||||
const openaiLogger = createLogger('openai');
|
||||
const telegramLogger = createLogger('telegram');
|
||||
const depositLogger = createLogger('deposit');
|
||||
|
||||
// ❌ Bad: 로거 재사용
|
||||
const logger = createLogger('app');
|
||||
```
|
||||
|
||||
### 2. 컨텍스트 정보 포함
|
||||
|
||||
```typescript
|
||||
// ✅ Good: 컨텍스트 포함
|
||||
logger.info('메시지 수신', {
|
||||
userId: '123',
|
||||
messageType: 'text',
|
||||
length: 42,
|
||||
});
|
||||
|
||||
// ❌ Bad: 메시지에 직접 포함
|
||||
logger.info(`메시지 수신: userId=123, type=text, length=42`);
|
||||
```
|
||||
|
||||
### 3. 에러 객체 전달
|
||||
|
||||
```typescript
|
||||
// ✅ Good: 에러 객체 전달
|
||||
try {
|
||||
await apiCall();
|
||||
} catch (error) {
|
||||
logger.error('API 호출 실패', error as Error, { endpoint: '/api/data' });
|
||||
}
|
||||
|
||||
// ❌ Bad: 에러를 문자열로 변환
|
||||
try {
|
||||
await apiCall();
|
||||
} catch (error) {
|
||||
logger.error(`API 호출 실패: ${error}`);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 성능 측정 활용
|
||||
|
||||
```typescript
|
||||
// ✅ Good: 타이머 사용
|
||||
const end = logger.startTimer('데이터 처리 완료');
|
||||
await processData();
|
||||
end();
|
||||
|
||||
// ❌ Bad: 수동 계산
|
||||
const start = Date.now();
|
||||
await processData();
|
||||
logger.info(`데이터 처리 완료 (${Date.now() - start}ms)`);
|
||||
```
|
||||
|
||||
### 5. 민감 정보 제외
|
||||
|
||||
```typescript
|
||||
// ✅ Good: 민감 정보 제외
|
||||
logger.info('API 호출', {
|
||||
endpoint: '/api/data',
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
// ❌ Bad: API 키, 비밀번호 등 포함
|
||||
logger.info('API 호출', {
|
||||
endpoint: '/api/data',
|
||||
apiKey: env.OPENAI_API_KEY, // ❌ 절대 금지
|
||||
password: userPassword, // ❌ 절대 금지
|
||||
});
|
||||
```
|
||||
|
||||
### 6. 로그 레벨 적절히 사용
|
||||
|
||||
```typescript
|
||||
// ✅ Good: 레벨에 맞는 로그
|
||||
logger.debug('디버그 정보', { details }); // 개발 전용
|
||||
logger.info('일반 정보', { status: 'ok' }); // 정상 동작
|
||||
logger.warn('경고', { delayMs: 3500 }); // 주의 필요
|
||||
logger.error('에러', error, { context }); // 복구 가능 에러
|
||||
logger.fatal('치명적 에러', error); // 시스템 장애
|
||||
|
||||
// ❌ Bad: 모든 로그를 info로
|
||||
logger.info('디버그 정보'); // ❌ debug 사용
|
||||
logger.info('경고'); // ❌ warn 사용
|
||||
logger.info('에러 발생'); // ❌ error 사용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 환경 설정
|
||||
|
||||
### 환경 변수
|
||||
|
||||
`wrangler.toml` 또는 환경 변수로 설정:
|
||||
|
||||
```toml
|
||||
[vars]
|
||||
ENVIRONMENT = "production" # 또는 "development"
|
||||
```
|
||||
|
||||
### 로그 레벨 제어
|
||||
|
||||
```typescript
|
||||
// 프로덕션: INFO 레벨 이상만 출력
|
||||
const logger = createLogger('service', env);
|
||||
|
||||
// 개발: DEBUG 레벨부터 모든 로그 출력
|
||||
const logger = createDebugLogger('service');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 실제 사용 예시
|
||||
|
||||
### OpenAI 서비스
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('openai', env);
|
||||
|
||||
export async function generateOpenAIResponse(
|
||||
env: Env,
|
||||
userMessage: string,
|
||||
systemPrompt: string
|
||||
): Promise<string> {
|
||||
const userLogger = logger.withUser(telegramUserId || 'unknown');
|
||||
const end = logger.startTimer('AI 응답 생성 완료');
|
||||
|
||||
try {
|
||||
userLogger.info('AI 응답 생성 시작', {
|
||||
messageLength: userMessage.length,
|
||||
model: 'gpt-4o-mini',
|
||||
});
|
||||
|
||||
const response = await callOpenAI(apiKey, messages, selectedTools);
|
||||
|
||||
userLogger.info('Function Calling 실행', {
|
||||
toolName: toolCall.function.name,
|
||||
arguments: toolCall.function.arguments,
|
||||
});
|
||||
|
||||
end();
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof CircuitBreakerError) {
|
||||
userLogger.error('Circuit breaker 열림', error, {
|
||||
service: 'openai',
|
||||
});
|
||||
} else if (error instanceof RetryError) {
|
||||
userLogger.error('재시도 실패', error, {
|
||||
maxRetries: 3,
|
||||
});
|
||||
} else {
|
||||
userLogger.error('AI 응답 생성 실패', error as Error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 예치금 서비스
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('deposit', env);
|
||||
|
||||
export async function matchPendingDeposit(
|
||||
db: D1Database,
|
||||
notification: BankNotification,
|
||||
env: Env
|
||||
): Promise<boolean> {
|
||||
logger.info('입금 매칭 시작', {
|
||||
depositorName: notification.depositorName,
|
||||
amount: notification.amount,
|
||||
bank: notification.bankName,
|
||||
});
|
||||
|
||||
const end = logger.startTimer('입금 매칭 완료');
|
||||
|
||||
try {
|
||||
const result = await db.prepare(/* ... */).first();
|
||||
|
||||
if (!result) {
|
||||
logger.info('매칭되는 pending 거래 없음');
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('매칭 발견', {
|
||||
transactionId: pendingTx.id,
|
||||
userId: pendingTx.user_id,
|
||||
});
|
||||
|
||||
await db.batch([/* ... */]);
|
||||
|
||||
logger.info('매칭 완료', {
|
||||
transactionId: pendingTx.id,
|
||||
newBalance: currentBalance + notification.amount,
|
||||
});
|
||||
|
||||
end();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('DB 업데이트 실패', error as Error, {
|
||||
transactionId: pendingTx?.id,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 문제 해결
|
||||
|
||||
### Q: 로그가 출력되지 않습니다
|
||||
|
||||
**A:** 로그 레벨 확인:
|
||||
|
||||
```typescript
|
||||
// DEBUG 레벨은 개발 환경에서만 출력됨
|
||||
const logger = createDebugLogger('service'); // DEBUG 포함
|
||||
const logger = createLogger('service'); // INFO 이상만
|
||||
```
|
||||
|
||||
### Q: 프로덕션에서 JSON이 아닌 텍스트 로그가 출력됩니다
|
||||
|
||||
**A:** 환경 변수 확인:
|
||||
|
||||
```typescript
|
||||
// wrangler.toml
|
||||
[vars]
|
||||
ENVIRONMENT = "production"
|
||||
```
|
||||
|
||||
### Q: 에러 스택이 출력되지 않습니다
|
||||
|
||||
**A:** 에러 객체를 전달하세요:
|
||||
|
||||
```typescript
|
||||
// ✅ Good
|
||||
logger.error('실패', error as Error);
|
||||
|
||||
// ❌ Bad
|
||||
logger.error('실패', undefined, { error: error.message });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 추가 자료
|
||||
|
||||
- **구현 파일**: `/src/utils/logger.ts`
|
||||
- **테스트 예시**: `/src/utils/__test__/logger.test.ts`
|
||||
- **CLAUDE.md**: 개발자 가이드 (Code Style & Conventions 섹션)
|
||||
170
docs/metrics-api-example.md
Normal file
170
docs/metrics-api-example.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Metrics API Example Response
|
||||
|
||||
## Endpoint: GET /api/metrics
|
||||
|
||||
**Authentication:** Bearer Token (WEBHOOK_SECRET)
|
||||
|
||||
**Example Request:**
|
||||
```bash
|
||||
curl -X GET https://telegram-summary-bot.kappa-d8e.workers.dev/api/metrics \
|
||||
-H "Authorization: Bearer your-webhook-secret"
|
||||
```
|
||||
|
||||
## Response Format
|
||||
|
||||
### Successful Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T07:35:42.123Z",
|
||||
"circuitBreakers": {
|
||||
"openai": {
|
||||
"state": "CLOSED",
|
||||
"failures": 0,
|
||||
"lastFailureTime": null,
|
||||
"stats": {
|
||||
"totalRequests": 1250,
|
||||
"totalFailures": 5,
|
||||
"totalSuccesses": 1245
|
||||
},
|
||||
"config": {
|
||||
"failureThreshold": 3,
|
||||
"resetTimeoutMs": 30000,
|
||||
"monitoringWindowMs": 60000
|
||||
}
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"api_calls": {
|
||||
"openai": {
|
||||
"count": 1250,
|
||||
"avg_duration": 0
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"retry_exhausted": 0,
|
||||
"circuit_breaker_open": 0
|
||||
},
|
||||
"cache": {
|
||||
"hit_rate": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Circuit States
|
||||
|
||||
| State | Description | Behavior |
|
||||
|-------|-------------|----------|
|
||||
| `CLOSED` | Normal operation | All requests pass through |
|
||||
| `HALF_OPEN` | Testing recovery | Single test request allowed |
|
||||
| `OPEN` | Service unavailable | All requests blocked immediately |
|
||||
|
||||
### When Circuit is OPEN
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T07:40:15.456Z",
|
||||
"circuitBreakers": {
|
||||
"openai": {
|
||||
"state": "OPEN",
|
||||
"failures": 3,
|
||||
"lastFailureTime": "2026-01-19T07:40:10.123Z",
|
||||
"stats": {
|
||||
"totalRequests": 1253,
|
||||
"totalFailures": 8,
|
||||
"totalSuccesses": 1245
|
||||
},
|
||||
"config": {
|
||||
"failureThreshold": 3,
|
||||
"resetTimeoutMs": 30000,
|
||||
"monitoringWindowMs": 60000
|
||||
}
|
||||
}
|
||||
},
|
||||
"metrics": {
|
||||
"api_calls": {
|
||||
"openai": {
|
||||
"count": 1253,
|
||||
"avg_duration": 0
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"retry_exhausted": 0,
|
||||
"circuit_breaker_open": 1
|
||||
},
|
||||
"cache": {
|
||||
"hit_rate": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Responses
|
||||
|
||||
**401 Unauthorized:**
|
||||
```json
|
||||
{
|
||||
"error": "Unauthorized"
|
||||
}
|
||||
```
|
||||
|
||||
**500 Internal Server Error:**
|
||||
```json
|
||||
{
|
||||
"error": "Error message here"
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Monitoring Dashboard
|
||||
- Poll this endpoint periodically to track Circuit Breaker health
|
||||
- Alert when state changes to OPEN
|
||||
- Track failure rate trends
|
||||
|
||||
### Debugging
|
||||
- Check Circuit Breaker state during production issues
|
||||
- Verify if service degradation is due to circuit being open
|
||||
- Review recent failure counts
|
||||
|
||||
### Performance Analysis
|
||||
- Monitor total requests and success rate
|
||||
- Identify patterns in failure occurrences
|
||||
- Validate circuit breaker thresholds
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
The `metrics` section is designed to be extensible:
|
||||
|
||||
```typescript
|
||||
// Planned additions:
|
||||
metrics: {
|
||||
api_calls: {
|
||||
openai: { count: number; avg_duration: number },
|
||||
namecheap: { count: number; avg_duration: number },
|
||||
brave: { count: number; avg_duration: number }
|
||||
},
|
||||
errors: {
|
||||
retry_exhausted: number,
|
||||
circuit_breaker_open: number,
|
||||
timeout: number
|
||||
},
|
||||
cache: {
|
||||
hit_rate: number,
|
||||
total_hits: number,
|
||||
total_misses: number
|
||||
},
|
||||
database: {
|
||||
query_count: number,
|
||||
avg_duration: number
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Metrics reset on Worker restart (no persistence)
|
||||
- Circuit Breaker state is per-instance (not shared across Workers)
|
||||
- `lastFailureTime` is only populated when failures exist
|
||||
- All timestamps are in ISO 8601 format (UTC)
|
||||
287
docs/metrics-usage-example.md
Normal file
287
docs/metrics-usage-example.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# 메트릭 시스템 사용 예시
|
||||
|
||||
## 개요
|
||||
|
||||
`src/utils/metrics.ts`는 API 호출 성능, Circuit Breaker 상태, 에러율 등을 추적하는 메트릭 수집 시스템입니다.
|
||||
|
||||
## 주요 특징
|
||||
|
||||
- **메모리 기반**: 최근 1000개 메트릭만 FIFO 방식으로 유지
|
||||
- **Thread-safe**: 동시 요청 안전
|
||||
- **저비용**: 메모리 연산만 사용하여 성능 오버헤드 최소화
|
||||
- **Worker 재시작 시 초기화**: 장기 저장이 필요하면 향후 KV 확장 가능
|
||||
|
||||
## 기본 사용법
|
||||
|
||||
### 1. Import
|
||||
|
||||
```typescript
|
||||
import { metrics } from './utils/metrics';
|
||||
```
|
||||
|
||||
### 2. 카운터 증가
|
||||
|
||||
API 호출 횟수, 에러 횟수 등을 카운트합니다.
|
||||
|
||||
```typescript
|
||||
// API 호출 시작
|
||||
metrics.increment('api_call_count', { service: 'openai', endpoint: '/chat' });
|
||||
|
||||
// 에러 발생
|
||||
metrics.increment('api_error_count', { service: 'openai', error: 'timeout' });
|
||||
|
||||
// 재시도
|
||||
metrics.increment('retry_count', { service: 'openai' });
|
||||
```
|
||||
|
||||
### 3. 값 기록
|
||||
|
||||
Circuit Breaker 상태, 캐시 히트율 등 특정 값을 기록합니다.
|
||||
|
||||
```typescript
|
||||
// Circuit Breaker 상태 (0=CLOSED, 1=OPEN, 2=HALF_OPEN)
|
||||
metrics.record('circuit_breaker_state', 1, { service: 'openai' });
|
||||
|
||||
// 캐시 히트율 (0.0 ~ 1.0)
|
||||
metrics.record('cache_hit_rate', 0.85, { cache: 'tld_prices' });
|
||||
|
||||
// API 응답 시간 (ms)
|
||||
metrics.record('api_call_duration', 245, { service: 'openai' });
|
||||
```
|
||||
|
||||
### 4. 타이머 사용
|
||||
|
||||
API 호출 시간을 자동으로 측정합니다.
|
||||
|
||||
```typescript
|
||||
const timer = metrics.startTimer('api_call_duration', { service: 'openai' });
|
||||
|
||||
try {
|
||||
const response = await openaiClient.chat.completions.create({...});
|
||||
return response;
|
||||
} finally {
|
||||
timer(); // 자동으로 duration 기록
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 통계 조회
|
||||
|
||||
```typescript
|
||||
// 전체 API 호출 시간 통계
|
||||
const stats = metrics.getStats('api_call_duration');
|
||||
console.log(`평균: ${stats.avg}ms, 최대: ${stats.max}ms`);
|
||||
|
||||
// 특정 서비스만 필터링
|
||||
const openaiStats = metrics.getStats('api_call_duration', { service: 'openai' });
|
||||
console.log(`OpenAI 평균: ${openaiStats.avg}ms`);
|
||||
```
|
||||
|
||||
## Circuit Breaker와 통합
|
||||
|
||||
`src/utils/circuit-breaker.ts`에서 상태 변경 시 메트릭을 기록합니다.
|
||||
|
||||
```typescript
|
||||
// circuit-breaker.ts
|
||||
import { metrics } from './metrics';
|
||||
|
||||
class CircuitBreaker {
|
||||
private setState(newState: CircuitBreakerState): void {
|
||||
this.state = newState;
|
||||
|
||||
// 메트릭 기록 (0=CLOSED, 1=OPEN, 2=HALF_OPEN)
|
||||
const stateValue = newState === 'CLOSED' ? 0 : newState === 'OPEN' ? 1 : 2;
|
||||
metrics.record('circuit_breaker_state', stateValue, { service: this.name });
|
||||
|
||||
console.log(`[CircuitBreaker:${this.name}] State: ${newState}`);
|
||||
}
|
||||
|
||||
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
||||
const timer = metrics.startTimer('api_call_duration', { service: this.name });
|
||||
|
||||
try {
|
||||
metrics.increment('api_call_count', { service: this.name });
|
||||
|
||||
const result = await fn();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
metrics.increment('api_error_count', { service: this.name });
|
||||
|
||||
// 재시도 로직...
|
||||
throw error;
|
||||
} finally {
|
||||
timer();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Retry 메커니즘과 통합
|
||||
|
||||
`src/utils/retry.ts`에서 재시도 횟수를 기록합니다.
|
||||
|
||||
```typescript
|
||||
// retry.ts
|
||||
import { metrics } from './metrics';
|
||||
|
||||
async function retryWithExponentialBackoff<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: RetryOptions
|
||||
): Promise<T> {
|
||||
let attempt = 0;
|
||||
|
||||
while (attempt < options.maxRetries) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
attempt++;
|
||||
metrics.increment('retry_count', { attempt: attempt.toString() });
|
||||
|
||||
if (attempt >= options.maxRetries) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
await delay(options.initialDelay * Math.pow(2, attempt - 1));
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Max retries exceeded');
|
||||
}
|
||||
```
|
||||
|
||||
## Cache와 통합
|
||||
|
||||
`src/utils/cache.ts`에서 캐시 히트율을 기록합니다.
|
||||
|
||||
```typescript
|
||||
// cache.ts
|
||||
import { metrics } from './metrics';
|
||||
|
||||
class Cache {
|
||||
async get<T>(key: string): Promise<T | null> {
|
||||
const value = await this.kv.get<T>(key, 'json');
|
||||
|
||||
if (value !== null) {
|
||||
metrics.record('cache_hit_rate', 1, { cache: this.name });
|
||||
} else {
|
||||
metrics.record('cache_hit_rate', 0, { cache: this.name });
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 모니터링 대시보드 예시
|
||||
|
||||
Worker에서 메트릭 엔드포인트를 추가하여 실시간 모니터링:
|
||||
|
||||
```typescript
|
||||
// index.ts
|
||||
if (url.pathname === '/metrics') {
|
||||
const stats = {
|
||||
api_calls: {
|
||||
openai: metrics.getStats('api_call_count', { service: 'openai' }),
|
||||
context7: metrics.getStats('api_call_count', { service: 'context7' }),
|
||||
},
|
||||
response_times: {
|
||||
openai: metrics.getStats('api_call_duration', { service: 'openai' }),
|
||||
context7: metrics.getStats('api_call_duration', { service: 'context7' }),
|
||||
},
|
||||
errors: {
|
||||
total: metrics.getStats('api_error_count'),
|
||||
},
|
||||
circuit_breaker: {
|
||||
openai: metrics.getStats('circuit_breaker_state', { service: 'openai' }),
|
||||
},
|
||||
cache: {
|
||||
tld_prices: metrics.getStats('cache_hit_rate', { cache: 'tld_prices' }),
|
||||
},
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(stats, null, 2), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 메트릭 타입
|
||||
|
||||
| 타입 | 설명 | 값 범위 |
|
||||
|------|------|---------|
|
||||
| `api_call_duration` | API 호출 시간 | ms (0+) |
|
||||
| `api_call_count` | API 호출 횟수 | 카운터 (1) |
|
||||
| `api_error_count` | API 에러 횟수 | 카운터 (1) |
|
||||
| `circuit_breaker_state` | Circuit Breaker 상태 | 0=CLOSED, 1=OPEN, 2=HALF_OPEN |
|
||||
| `retry_count` | 재시도 횟수 | 카운터 (1) |
|
||||
| `cache_hit_rate` | 캐시 히트율 | 0 (miss) ~ 1 (hit) |
|
||||
|
||||
## 메모리 관리
|
||||
|
||||
- 최대 1000개 메트릭 유지 (FIFO)
|
||||
- 각 메트릭: ~100 bytes (name + value + timestamp + tags)
|
||||
- 총 메모리: ~100KB (매우 저렴)
|
||||
|
||||
## 향후 확장
|
||||
|
||||
### KV 저장 (장기 보관)
|
||||
|
||||
```typescript
|
||||
// 주기적으로 KV에 저장 (cron)
|
||||
const stats = metrics.getStats('api_call_duration');
|
||||
await env.METRICS_KV.put(
|
||||
`stats:${Date.now()}`,
|
||||
JSON.stringify(stats),
|
||||
{ expirationTtl: 86400 * 7 } // 7일 보관
|
||||
);
|
||||
```
|
||||
|
||||
### 외부 모니터링 연동
|
||||
|
||||
```typescript
|
||||
// Cloudflare Analytics Engine
|
||||
import { Analytics } from '@cloudflare/workers-types';
|
||||
|
||||
const timer = metrics.startTimer('api_call_duration', { service: 'openai' });
|
||||
const response = await callOpenAI();
|
||||
timer();
|
||||
|
||||
// Analytics Engine에 전송
|
||||
await env.ANALYTICS.writeDataPoint({
|
||||
blobs: ['openai'],
|
||||
doubles: [response.duration],
|
||||
indexes: ['api_call'],
|
||||
});
|
||||
```
|
||||
|
||||
## 테스트
|
||||
|
||||
```typescript
|
||||
import { MetricsCollector } from './utils/metrics';
|
||||
|
||||
describe('MetricsCollector', () => {
|
||||
const metrics = new MetricsCollector();
|
||||
|
||||
beforeEach(() => {
|
||||
metrics.reset();
|
||||
});
|
||||
|
||||
it('should increment counter', () => {
|
||||
metrics.increment('api_call_count', { service: 'test' });
|
||||
const stats = metrics.getStats('api_call_count', { service: 'test' });
|
||||
expect(stats.sum).toBe(1);
|
||||
});
|
||||
|
||||
it('should calculate stats correctly', () => {
|
||||
metrics.record('api_call_duration', 100);
|
||||
metrics.record('api_call_duration', 200);
|
||||
metrics.record('api_call_duration', 300);
|
||||
|
||||
const stats = metrics.getStats('api_call_duration');
|
||||
expect(stats.count).toBe(3);
|
||||
expect(stats.avg).toBe(200);
|
||||
expect(stats.min).toBe(100);
|
||||
expect(stats.max).toBe(300);
|
||||
});
|
||||
});
|
||||
```
|
||||
@@ -7,7 +7,7 @@ import { CircuitBreaker, CircuitBreakerError } from './utils/circuit-breaker';
|
||||
const OPENAI_API_URL = 'https://gateway.ai.cloudflare.com/v1/d8e5997eb4040f8b489f09095c0f623c/telegram-bot/openai/chat/completions';
|
||||
|
||||
// Circuit Breaker 인스턴스 (전역 공유)
|
||||
const openaiCircuitBreaker = new CircuitBreaker({
|
||||
export const openaiCircuitBreaker = new CircuitBreaker({
|
||||
failureThreshold: 3, // 3회 연속 실패 시 차단
|
||||
resetTimeoutMs: 30000, // 30초 후 복구 시도
|
||||
monitoringWindowMs: 60000 // 1분 윈도우
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
generateAIResponse,
|
||||
} from '../summary-service';
|
||||
import { handleCommand } from '../commands';
|
||||
import { openaiCircuitBreaker } from '../openai-service';
|
||||
|
||||
// 사용자 조회/생성
|
||||
async function getOrCreateUser(
|
||||
@@ -59,6 +60,9 @@ async function getOrCreateUser(
|
||||
* -H "Origin: https://hosting.anvil.it.com" \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -d '{"email":"test@example.com","message":"test message"}'
|
||||
* 6. Test metrics (Circuit Breaker status):
|
||||
* curl http://localhost:8787/api/metrics \
|
||||
* -H "Authorization: Bearer your-webhook-secret"
|
||||
*/
|
||||
export async function handleApiRequest(request: Request, env: Env, url: URL): Promise<Response> {
|
||||
// Deposit API - 잔액 조회 (namecheap-api 전용)
|
||||
@@ -314,5 +318,60 @@ export async function handleApiRequest(request: Request, env: Env, url: URL): Pr
|
||||
});
|
||||
}
|
||||
|
||||
// Metrics API - Circuit Breaker 상태 조회 (관리자 전용)
|
||||
if (url.pathname === '/api/metrics' && request.method === 'GET') {
|
||||
try {
|
||||
// WEBHOOK_SECRET 인증
|
||||
const authHeader = request.headers.get('Authorization');
|
||||
if (!env.WEBHOOK_SECRET || authHeader !== `Bearer ${env.WEBHOOK_SECRET}`) {
|
||||
return Response.json({ error: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Circuit Breaker 상태 수집
|
||||
const openaiStats = openaiCircuitBreaker.getStats();
|
||||
|
||||
// 메트릭 응답 생성
|
||||
const metrics = {
|
||||
timestamp: new Date().toISOString(),
|
||||
circuitBreakers: {
|
||||
openai: {
|
||||
state: openaiStats.state,
|
||||
failures: openaiStats.failures,
|
||||
lastFailureTime: openaiStats.lastFailureTime?.toISOString(),
|
||||
stats: openaiStats.stats,
|
||||
config: openaiStats.config,
|
||||
},
|
||||
},
|
||||
// 추후 확장 가능: API 호출 통계, 캐시 hit rate 등
|
||||
metrics: {
|
||||
api_calls: {
|
||||
// 추후 구현: 실제 API 호출 통계
|
||||
openai: { count: openaiStats.stats.totalRequests, avg_duration: 0 },
|
||||
},
|
||||
errors: {
|
||||
// 추후 구현: 에러 통계
|
||||
retry_exhausted: 0,
|
||||
circuit_breaker_open: openaiStats.state === 'OPEN' ? 1 : 0,
|
||||
},
|
||||
cache: {
|
||||
// 추후 구현: 캐시 hit rate
|
||||
hit_rate: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
console.log('[Metrics API] Circuit breaker stats retrieved:', {
|
||||
state: openaiStats.state,
|
||||
failures: openaiStats.failures,
|
||||
requests: openaiStats.stats.totalRequests,
|
||||
});
|
||||
|
||||
return Response.json(metrics);
|
||||
} catch (error) {
|
||||
console.error('[Metrics API] Error:', error);
|
||||
return Response.json({ error: String(error) }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
return new Response('Not Found', { status: 404 });
|
||||
}
|
||||
|
||||
114
src/utils/__demo__/logger-demo.ts
Normal file
114
src/utils/__demo__/logger-demo.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Logger 데모 스크립트
|
||||
*
|
||||
* 실제 출력을 보여주기 위한 데모 코드
|
||||
*/
|
||||
|
||||
import { createLogger, createDebugLogger } from '../logger';
|
||||
|
||||
console.log('='.repeat(80));
|
||||
console.log('Logger Demo - 구조화된 로깅 유틸리티');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 1. 기본 로깅
|
||||
console.log('\n[1] 기본 로깅 (INFO 레벨)');
|
||||
console.log('-'.repeat(80));
|
||||
const logger = createLogger('demo');
|
||||
logger.info('서비스 시작');
|
||||
logger.info('설정 로드 완료', { configFile: 'app.config.json', version: '1.0.0' });
|
||||
|
||||
// 2. 다양한 로그 레벨
|
||||
console.log('\n[2] 로그 레벨별 출력');
|
||||
console.log('-'.repeat(80));
|
||||
const levelLogger = createLogger('levels');
|
||||
levelLogger.debug('디버그 메시지 (출력 안됨 - INFO 레벨 이상만)');
|
||||
levelLogger.info('일반 정보 메시지');
|
||||
levelLogger.warn('경고 메시지', { threshold: 100, current: 150 });
|
||||
|
||||
try {
|
||||
throw new Error('테스트 에러');
|
||||
} catch (error) {
|
||||
levelLogger.error('에러 발생', error as Error, { context: 'test' });
|
||||
}
|
||||
|
||||
// 3. 디버그 로거
|
||||
console.log('\n[3] 디버그 로거 (DEBUG 레벨 포함)');
|
||||
console.log('-'.repeat(80));
|
||||
const debugLogger = createDebugLogger('debug');
|
||||
debugLogger.debug('디버그 정보 출력됨', { data: [1, 2, 3] });
|
||||
debugLogger.info('일반 정보');
|
||||
|
||||
// 4. 성능 측정
|
||||
console.log('\n[4] 성능 측정 (타이머)');
|
||||
console.log('-'.repeat(80));
|
||||
const perfLogger = createLogger('performance');
|
||||
const endTimer = perfLogger.startTimer('작업 완료', { operation: 'data-processing' });
|
||||
// 시뮬레이션: 1초 대기
|
||||
setTimeout(() => {
|
||||
endTimer(); // duration이 자동으로 로그에 포함됨
|
||||
}, 1000);
|
||||
|
||||
// 5. 사용자 컨텍스트
|
||||
console.log('\n[5] 사용자 컨텍스트 (userId 자동 포함)');
|
||||
console.log('-'.repeat(80));
|
||||
const userLogger = createLogger('telegram').withUser('821596605');
|
||||
userLogger.info('메시지 수신', { messageType: 'text', length: 42 });
|
||||
userLogger.info('AI 응답 전송', { responseLength: 150 });
|
||||
|
||||
// 6. 서비스별 로거
|
||||
console.log('\n[6] 서비스별 로거');
|
||||
console.log('-'.repeat(80));
|
||||
const openaiLogger = createLogger('openai');
|
||||
openaiLogger.info('API 호출', { model: 'gpt-4o-mini', tokens: 1000 });
|
||||
|
||||
const depositLogger = createLogger('deposit');
|
||||
depositLogger.info('입금 확인', { depositorName: '홍길동', amount: 50000 });
|
||||
|
||||
const domainLogger = createLogger('domain');
|
||||
domainLogger.info('도메인 등록', { domain: 'example.com', price: 15000 });
|
||||
|
||||
// 7. 에러 체인
|
||||
console.log('\n[7] 에러 스택 트레이스');
|
||||
console.log('-'.repeat(80));
|
||||
const errorLogger = createLogger('error-handler');
|
||||
try {
|
||||
try {
|
||||
throw new Error('내부 에러: 데이터베이스 연결 실패');
|
||||
} catch (innerError) {
|
||||
throw new Error('외부 에러: 처리 중 예외 발생');
|
||||
}
|
||||
} catch (outerError) {
|
||||
errorLogger.error('최종 에러', outerError as Error, {
|
||||
service: 'database',
|
||||
operation: 'connect',
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
console.log('Demo 완료');
|
||||
console.log('='.repeat(80));
|
||||
|
||||
// 8. JSON 출력 시뮬레이션 (프로덕션 모드)
|
||||
console.log('\n[8] 프로덕션 모드 (JSON 출력) 시뮬레이션');
|
||||
console.log('-'.repeat(80));
|
||||
console.log('// wrangler.toml: ENVIRONMENT = "production"');
|
||||
console.log('// 실제 출력:');
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
level: 'INFO',
|
||||
service: 'openai',
|
||||
message: 'AI 응답 생성 시작',
|
||||
userId: '821596605',
|
||||
context: {
|
||||
model: 'gpt-4o-mini',
|
||||
messageLength: 42,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
console.log('\n' + '='.repeat(80));
|
||||
253
src/utils/__test__/logger.test.ts
Normal file
253
src/utils/__test__/logger.test.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Logger 사용 예시 및 테스트
|
||||
*
|
||||
* 실제 테스트 러너는 없지만, 다양한 사용 패턴을 보여줍니다.
|
||||
*/
|
||||
|
||||
import { createLogger, createDebugLogger } from '../logger';
|
||||
|
||||
/**
|
||||
* 기본 사용법 예시
|
||||
*/
|
||||
function basicUsageExample() {
|
||||
const logger = createLogger('example');
|
||||
|
||||
// 일반 로그
|
||||
logger.info('서비스 시작');
|
||||
logger.info('설정 로드 완료', { configFile: 'app.config.json' });
|
||||
|
||||
// 경고
|
||||
logger.warn('API 응답 지연', { endpoint: '/api/data', responseTime: 3500 });
|
||||
|
||||
// 에러 처리
|
||||
try {
|
||||
throw new Error('연결 실패');
|
||||
} catch (error) {
|
||||
logger.error('데이터베이스 연결 실패', error as Error, {
|
||||
host: 'localhost',
|
||||
port: 5432,
|
||||
});
|
||||
}
|
||||
|
||||
// 치명적 에러
|
||||
logger.fatal('시스템 초기화 실패', new Error('Critical error'), {
|
||||
service: 'database',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 성능 측정 예시
|
||||
*/
|
||||
async function performanceMeasurementExample() {
|
||||
const logger = createLogger('performance');
|
||||
|
||||
// 타이머 시작
|
||||
const endTimer = logger.startTimer('데이터 처리 완료', {
|
||||
recordCount: 10000,
|
||||
batchSize: 100,
|
||||
});
|
||||
|
||||
// 시뮬레이션: 실제 작업
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
// 타이머 종료 (duration이 자동으로 로그에 포함됨)
|
||||
endTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 컨텍스트 예시
|
||||
*/
|
||||
function userContextExample() {
|
||||
const logger = createLogger('telegram');
|
||||
|
||||
// 사용자 ID 포함 로거
|
||||
const userLogger = logger.withUser('123456789');
|
||||
|
||||
userLogger.info('메시지 수신', {
|
||||
messageType: 'text',
|
||||
length: 42,
|
||||
});
|
||||
|
||||
userLogger.info('AI 응답 생성 시작', {
|
||||
model: 'gpt-4o-mini',
|
||||
tokensEstimate: 1000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 서비스별 로거 예시
|
||||
*/
|
||||
function serviceSpecificLoggers() {
|
||||
// OpenAI 서비스
|
||||
const openaiLogger = createLogger('openai');
|
||||
openaiLogger.info('API 호출 시작', {
|
||||
model: 'gpt-4o-mini',
|
||||
maxTokens: 1000,
|
||||
});
|
||||
|
||||
// Telegram 서비스
|
||||
const telegramLogger = createLogger('telegram');
|
||||
telegramLogger.info('메시지 전송', {
|
||||
chatId: 123456789,
|
||||
messageLength: 150,
|
||||
});
|
||||
|
||||
// 예치금 서비스
|
||||
const depositLogger = createLogger('deposit');
|
||||
depositLogger.info('입금 확인', {
|
||||
depositorName: '홍길동',
|
||||
amount: 50000,
|
||||
status: 'confirmed',
|
||||
});
|
||||
|
||||
// 도메인 서비스
|
||||
const domainLogger = createLogger('domain');
|
||||
domainLogger.info('도메인 등록 요청', {
|
||||
domain: 'example.com',
|
||||
userId: '123456789',
|
||||
price: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그 로거 예시
|
||||
*/
|
||||
function debugLoggerExample() {
|
||||
const logger = createDebugLogger('debug-service');
|
||||
|
||||
// DEBUG 레벨 포함 모든 로그 출력
|
||||
logger.debug('상세 디버그 정보', {
|
||||
requestHeaders: { 'Content-Type': 'application/json' },
|
||||
requestBody: { userId: '123', action: 'register' },
|
||||
});
|
||||
|
||||
logger.info('일반 정보');
|
||||
logger.warn('경고');
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI 서비스 실제 사용 예시
|
||||
*/
|
||||
async function openaiServiceExample() {
|
||||
const logger = createLogger('openai');
|
||||
const userLogger = logger.withUser('821596605');
|
||||
|
||||
userLogger.info('AI 응답 생성 시작', {
|
||||
model: 'gpt-4o-mini',
|
||||
messageLength: 42,
|
||||
});
|
||||
|
||||
const endTimer = logger.startTimer('AI 응답 생성 완료');
|
||||
|
||||
try {
|
||||
// 시뮬레이션: OpenAI API 호출
|
||||
await new Promise((resolve) => setTimeout(resolve, 1200));
|
||||
|
||||
userLogger.info('Function Calling 실행', {
|
||||
toolName: 'get_weather',
|
||||
arguments: { location: '서울' },
|
||||
});
|
||||
|
||||
endTimer();
|
||||
} catch (error) {
|
||||
userLogger.error('AI 응답 생성 실패', error as Error, {
|
||||
model: 'gpt-4o-mini',
|
||||
retryCount: 3,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 예치금 서비스 실제 사용 예시
|
||||
*/
|
||||
async function depositServiceExample() {
|
||||
const logger = createLogger('deposit');
|
||||
const userLogger = logger.withUser('821596605');
|
||||
|
||||
userLogger.info('입금 요청 수신', {
|
||||
depositorName: '홍길동',
|
||||
amount: 50000,
|
||||
});
|
||||
|
||||
const endTimer = logger.startTimer('입금 매칭 완료');
|
||||
|
||||
try {
|
||||
// 시뮬레이션: 데이터베이스 조회
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
|
||||
userLogger.info('자동 매칭 성공', {
|
||||
transactionId: 123,
|
||||
depositorName: '홍길동',
|
||||
amount: 50000,
|
||||
newBalance: 50000,
|
||||
});
|
||||
|
||||
endTimer();
|
||||
} catch (error) {
|
||||
userLogger.error('입금 처리 실패', error as Error, {
|
||||
depositorName: '홍길동',
|
||||
amount: 50000,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 에러 체인 로깅 예시
|
||||
*/
|
||||
function errorChainExample() {
|
||||
const logger = createLogger('error-chain');
|
||||
|
||||
try {
|
||||
try {
|
||||
throw new Error('원본 에러: 데이터베이스 연결 실패');
|
||||
} catch (innerError) {
|
||||
logger.error('내부 에러 발생', innerError as Error);
|
||||
throw new Error('처리 중 에러 발생');
|
||||
}
|
||||
} catch (outerError) {
|
||||
logger.error('외부 에러 발생', outerError as Error, {
|
||||
context: 'transaction-processing',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 예시 실행 (실제로는 테스트 러너에서 실행)
|
||||
// Node.js 환경에서만 동작하므로 주석 처리
|
||||
/*
|
||||
if (require.main === module) {
|
||||
console.log('=== 기본 사용법 ===');
|
||||
basicUsageExample();
|
||||
|
||||
console.log('\n=== 성능 측정 ===');
|
||||
performanceMeasurementExample();
|
||||
|
||||
console.log('\n=== 사용자 컨텍스트 ===');
|
||||
userContextExample();
|
||||
|
||||
console.log('\n=== 서비스별 로거 ===');
|
||||
serviceSpecificLoggers();
|
||||
|
||||
console.log('\n=== 디버그 로거 ===');
|
||||
debugLoggerExample();
|
||||
|
||||
console.log('\n=== OpenAI 서비스 예시 ===');
|
||||
openaiServiceExample();
|
||||
|
||||
console.log('\n=== 예치금 서비스 예시 ===');
|
||||
depositServiceExample();
|
||||
|
||||
console.log('\n=== 에러 체인 ===');
|
||||
errorChainExample();
|
||||
}
|
||||
*/
|
||||
|
||||
export {
|
||||
basicUsageExample,
|
||||
performanceMeasurementExample,
|
||||
userContextExample,
|
||||
serviceSpecificLoggers,
|
||||
debugLoggerExample,
|
||||
openaiServiceExample,
|
||||
depositServiceExample,
|
||||
errorChainExample,
|
||||
};
|
||||
@@ -106,12 +106,24 @@ export class CircuitBreaker {
|
||||
* Get circuit statistics
|
||||
*/
|
||||
getStats() {
|
||||
const lastFailure = this.failures.length > 0
|
||||
? this.failures[this.failures.length - 1]
|
||||
: null;
|
||||
|
||||
return {
|
||||
state: this.state,
|
||||
successCount: this.successCount,
|
||||
failureCount: this.failureCount,
|
||||
recentFailures: this.failures.length,
|
||||
openedAt: this.openedAt,
|
||||
failures: this.failures.length,
|
||||
lastFailureTime: lastFailure ? new Date(lastFailure.timestamp) : undefined,
|
||||
stats: {
|
||||
totalRequests: this.successCount + this.failureCount,
|
||||
totalFailures: this.failureCount,
|
||||
totalSuccesses: this.successCount,
|
||||
},
|
||||
config: {
|
||||
failureThreshold: this.failureThreshold,
|
||||
resetTimeoutMs: this.resetTimeoutMs,
|
||||
monitoringWindowMs: this.monitoringWindowMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
290
src/utils/logger.README.md
Normal file
290
src/utils/logger.README.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Logger 유틸리티
|
||||
|
||||
구조화된 JSON 기반 로깅 시스템 (Cloudflare Workers 환경)
|
||||
|
||||
## 주요 기능
|
||||
|
||||
✅ **JSON 기반 구조화 로그** - 프로덕션 환경에서 로그 수집 시스템 연동
|
||||
✅ **환경별 자동 전환** - 개발(읽기 쉬운) ↔ 프로덕션(JSON)
|
||||
✅ **타입 안전성** - TypeScript strict mode 지원
|
||||
✅ **성능 측정** - 내장 타이머로 실행 시간 자동 계산
|
||||
✅ **에러 안전성** - 로깅 실패가 메인 로직 중단시키지 않음
|
||||
✅ **사용자 컨텍스트** - 사용자 ID 자동 포함
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
### 1. 기본 사용
|
||||
|
||||
```typescript
|
||||
import { createLogger } from './utils/logger';
|
||||
|
||||
const logger = createLogger('openai');
|
||||
|
||||
// 일반 로그
|
||||
logger.info('AI 응답 생성 시작');
|
||||
|
||||
// 컨텍스트 포함
|
||||
logger.info('API 호출 완료', {
|
||||
model: 'gpt-4o-mini',
|
||||
tokensUsed: 1000,
|
||||
});
|
||||
|
||||
// 에러 처리
|
||||
try {
|
||||
await callOpenAI();
|
||||
} catch (error) {
|
||||
logger.error('OpenAI API 호출 실패', error as Error, {
|
||||
model: 'gpt-4o-mini',
|
||||
retryCount: 3,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 성능 측정
|
||||
|
||||
```typescript
|
||||
const end = logger.startTimer('AI 응답 생성 완료');
|
||||
await generateResponse();
|
||||
end(); // duration이 자동으로 로그에 포함됨
|
||||
```
|
||||
|
||||
**출력:**
|
||||
```
|
||||
ℹ️ [2026-01-19T16:45:23.123Z] INFO [openai] AI 응답 생성 완료 (1234ms)
|
||||
```
|
||||
|
||||
### 3. 사용자 컨텍스트
|
||||
|
||||
```typescript
|
||||
const userLogger = logger.withUser('821596605');
|
||||
userLogger.info('메시지 수신', { messageType: 'text' });
|
||||
```
|
||||
|
||||
**출력 (JSON):**
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T16:45:23.123Z",
|
||||
"level": "INFO",
|
||||
"service": "openai",
|
||||
"message": "메시지 수신",
|
||||
"userId": "821596605",
|
||||
"context": {
|
||||
"messageType": "text"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 로그 레벨
|
||||
|
||||
| 레벨 | 용도 | 예시 |
|
||||
|------|------|------|
|
||||
| `DEBUG` | 디버깅 (개발 전용) | 함수 호출, 파라미터 값 |
|
||||
| `INFO` | 일반 정보 | 요청 처리, 성공 응답 |
|
||||
| `WARN` | 경고 | API 지연, 폴백 사용 |
|
||||
| `ERROR` | 에러 (복구 가능) | API 실패, 재시도 |
|
||||
| `FATAL` | 치명적 에러 | 시스템 장애 |
|
||||
|
||||
## 출력 형식
|
||||
|
||||
### 개발 환경
|
||||
|
||||
```
|
||||
ℹ️ [2026-01-19T16:45:23.123Z] INFO [openai] AI 응답 생성 시작 {"model":"gpt-4"}
|
||||
⚠️ [2026-01-19T16:45:25.456Z] WARN [openai] API 응답 지연 {"responseTime":3500}
|
||||
❌ [2026-01-19T16:45:27.789Z] ERROR [openai] API 호출 실패 {"retryCount":3}
|
||||
Error: Error: Connection timeout
|
||||
Stack: Error: Connection timeout
|
||||
at callOpenAI (openai-service.ts:45:11)
|
||||
```
|
||||
|
||||
### 프로덕션 (JSON)
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-01-19T16:45:23.123Z",
|
||||
"level": "INFO",
|
||||
"service": "openai",
|
||||
"message": "AI 응답 생성 시작",
|
||||
"context": { "model": "gpt-4" }
|
||||
}
|
||||
```
|
||||
|
||||
## 마이그레이션
|
||||
|
||||
### Before (console.log)
|
||||
|
||||
```typescript
|
||||
console.log('[OpenAI] AI 응답 생성 시작');
|
||||
console.error('[OpenAI] API 호출 실패:', error);
|
||||
```
|
||||
|
||||
### After (Logger)
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('openai');
|
||||
logger.info('AI 응답 생성 시작');
|
||||
logger.error('API 호출 실패', error as Error);
|
||||
```
|
||||
|
||||
## 실제 사용 예시
|
||||
|
||||
### OpenAI 서비스
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('openai', env);
|
||||
|
||||
export async function generateOpenAIResponse(
|
||||
env: Env,
|
||||
userMessage: string
|
||||
): Promise<string> {
|
||||
const userLogger = logger.withUser(telegramUserId);
|
||||
const end = logger.startTimer('AI 응답 생성 완료');
|
||||
|
||||
try {
|
||||
userLogger.info('AI 응답 생성 시작', {
|
||||
messageLength: userMessage.length,
|
||||
model: 'gpt-4o-mini',
|
||||
});
|
||||
|
||||
const response = await callOpenAI(apiKey, messages);
|
||||
end();
|
||||
return response;
|
||||
} catch (error) {
|
||||
userLogger.error('AI 응답 생성 실패', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 예치금 서비스
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('deposit', env);
|
||||
|
||||
export async function matchPendingDeposit(
|
||||
db: D1Database,
|
||||
notification: BankNotification
|
||||
): Promise<boolean> {
|
||||
logger.info('입금 매칭 시작', {
|
||||
depositorName: notification.depositorName,
|
||||
amount: notification.amount,
|
||||
});
|
||||
|
||||
const end = logger.startTimer('입금 매칭 완료');
|
||||
|
||||
try {
|
||||
const result = await findPendingTransaction(db, notification);
|
||||
|
||||
if (!result) {
|
||||
logger.info('매칭되는 pending 거래 없음');
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.info('매칭 완료', {
|
||||
transactionId: result.id,
|
||||
newBalance: result.balance,
|
||||
});
|
||||
|
||||
end();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('DB 업데이트 실패', error as Error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### createLogger(service, env?)
|
||||
|
||||
서비스별 로거 인스턴스 생성
|
||||
|
||||
```typescript
|
||||
const logger = createLogger('openai', env);
|
||||
```
|
||||
|
||||
### createDebugLogger(service)
|
||||
|
||||
DEBUG 레벨 포함 로거 생성 (개발 전용)
|
||||
|
||||
```typescript
|
||||
const logger = createDebugLogger('debug-service');
|
||||
logger.debug('상세 디버그 정보', { data });
|
||||
```
|
||||
|
||||
### logger.info(message, context?)
|
||||
|
||||
일반 정보 로그
|
||||
|
||||
```typescript
|
||||
logger.info('요청 처리', { userId: '123' });
|
||||
```
|
||||
|
||||
### logger.warn(message, context?)
|
||||
|
||||
경고 로그
|
||||
|
||||
```typescript
|
||||
logger.warn('API 응답 지연', { responseTime: 3500 });
|
||||
```
|
||||
|
||||
### logger.error(message, error?, context?)
|
||||
|
||||
에러 로그
|
||||
|
||||
```typescript
|
||||
logger.error('API 호출 실패', error as Error, { retryCount: 3 });
|
||||
```
|
||||
|
||||
### logger.fatal(message, error?, context?)
|
||||
|
||||
치명적 에러 로그
|
||||
|
||||
```typescript
|
||||
logger.fatal('시스템 초기화 실패', error as Error);
|
||||
```
|
||||
|
||||
### logger.startTimer(message?, context?)
|
||||
|
||||
성능 측정 타이머
|
||||
|
||||
```typescript
|
||||
const end = logger.startTimer('작업 완료', { recordCount: 1000 });
|
||||
await doWork();
|
||||
end();
|
||||
```
|
||||
|
||||
### logger.withUser(userId)
|
||||
|
||||
사용자 ID 자동 포함 로거
|
||||
|
||||
```typescript
|
||||
const userLogger = logger.withUser('821596605');
|
||||
userLogger.info('메시지 수신');
|
||||
```
|
||||
|
||||
## 환경 설정
|
||||
|
||||
### wrangler.toml
|
||||
|
||||
```toml
|
||||
[vars]
|
||||
ENVIRONMENT = "production" # 프로덕션 모드
|
||||
```
|
||||
|
||||
- `production`: JSON 형식 출력
|
||||
- `development` (기본): 읽기 쉬운 형식 출력
|
||||
|
||||
## 성능 영향
|
||||
|
||||
- **최소 오버헤드**: <1ms per log entry
|
||||
- **메모리 효율**: 로그 엔트리는 즉시 출력 후 폐기
|
||||
- **에러 안전**: 로깅 실패가 메인 로직 차단하지 않음
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- **상세 가이드**: `/docs/logger-guide.md`
|
||||
- **테스트 예시**: `/src/utils/__test__/logger.test.ts`
|
||||
- **데모 스크립트**: `/src/utils/__demo__/logger-demo.ts`
|
||||
- **CLAUDE.md**: 개발자 가이드 (Code Style 섹션)
|
||||
434
src/utils/logger.ts
Normal file
434
src/utils/logger.ts
Normal file
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 구조화된 로깅 유틸리티
|
||||
*
|
||||
* Cloudflare Workers 환경에서 JSON 기반 로깅을 지원하며,
|
||||
* 프로덕션 환경에서는 구조화된 JSON 로그를,
|
||||
* 개발 환경에서는 읽기 쉬운 포맷을 제공합니다.
|
||||
*
|
||||
* @module logger
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { createLogger } from './utils/logger';
|
||||
*
|
||||
* const logger = createLogger('openai');
|
||||
* logger.info('AI 응답 생성 시작', { userId: '123', model: 'gpt-4' });
|
||||
* ```
|
||||
*/
|
||||
|
||||
import type { Env } from '../types';
|
||||
|
||||
/**
|
||||
* 로그 레벨 열거형
|
||||
*
|
||||
* 우선순위: DEBUG < INFO < WARN < ERROR < FATAL
|
||||
*/
|
||||
export enum LogLevel {
|
||||
/** 디버그 정보 (개발 환경 전용) */
|
||||
DEBUG = 'DEBUG',
|
||||
/** 일반 정보 메시지 */
|
||||
INFO = 'INFO',
|
||||
/** 경고 메시지 (주의 필요하나 정상 동작) */
|
||||
WARN = 'WARN',
|
||||
/** 에러 메시지 (기능 실패, 복구 가능) */
|
||||
ERROR = 'ERROR',
|
||||
/** 치명적 에러 (시스템 장애) */
|
||||
FATAL = 'FATAL',
|
||||
}
|
||||
|
||||
/**
|
||||
* 구조화된 로그 엔트리 인터페이스
|
||||
*/
|
||||
export interface LogEntry {
|
||||
/** ISO 8601 타임스탬프 */
|
||||
timestamp: string;
|
||||
/** 로그 레벨 */
|
||||
level: LogLevel;
|
||||
/** 로그 메시지 */
|
||||
message: string;
|
||||
/** 서비스명 (예: 'openai', 'telegram', 'deposit') */
|
||||
service?: string;
|
||||
/** 추가 컨텍스트 정보 */
|
||||
context?: Record<string, any>;
|
||||
/** 에러 정보 (ERROR/FATAL 레벨용) */
|
||||
error?: {
|
||||
/** 에러 이름 */
|
||||
name: string;
|
||||
/** 에러 메시지 */
|
||||
message: string;
|
||||
/** 스택 트레이스 */
|
||||
stack?: string;
|
||||
};
|
||||
/** 사용자 ID (Telegram ID) */
|
||||
userId?: string;
|
||||
/** 실행 시간 (밀리초) */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 로거 설정 인터페이스
|
||||
*/
|
||||
export interface LoggerOptions {
|
||||
/** 최소 로그 레벨 (이 레벨 이상만 출력) */
|
||||
minLevel?: LogLevel;
|
||||
/** 환경 타입 (자동 감지 가능) */
|
||||
environment?: 'production' | 'development';
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그 레벨 우선순위 맵
|
||||
*/
|
||||
const LOG_LEVEL_PRIORITY: Record<LogLevel, number> = {
|
||||
[LogLevel.DEBUG]: 0,
|
||||
[LogLevel.INFO]: 1,
|
||||
[LogLevel.WARN]: 2,
|
||||
[LogLevel.ERROR]: 3,
|
||||
[LogLevel.FATAL]: 4,
|
||||
};
|
||||
|
||||
/**
|
||||
* 로그 레벨 이모지 (개발 환경용)
|
||||
*/
|
||||
const LOG_LEVEL_EMOJI: Record<LogLevel, string> = {
|
||||
[LogLevel.DEBUG]: '🔍',
|
||||
[LogLevel.INFO]: 'ℹ️',
|
||||
[LogLevel.WARN]: '⚠️',
|
||||
[LogLevel.ERROR]: '❌',
|
||||
[LogLevel.FATAL]: '💀',
|
||||
};
|
||||
|
||||
/**
|
||||
* 구조화된 로깅을 제공하는 Logger 클래스
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const logger = new Logger('openai', LogLevel.INFO);
|
||||
*
|
||||
* // 기본 로깅
|
||||
* logger.info('요청 처리 시작');
|
||||
*
|
||||
* // 컨텍스트 포함
|
||||
* logger.debug('API 호출', { endpoint: '/chat', method: 'POST' });
|
||||
*
|
||||
* // 에러 로깅
|
||||
* try {
|
||||
* // ...
|
||||
* } catch (error) {
|
||||
* logger.error('API 호출 실패', error as Error, { endpoint: '/chat' });
|
||||
* }
|
||||
*
|
||||
* // 성능 측정
|
||||
* const end = logger.startTimer();
|
||||
* await someAsyncOperation();
|
||||
* end(); // duration이 자동으로 로그에 포함됨
|
||||
* ```
|
||||
*/
|
||||
export class Logger {
|
||||
private minLevel: LogLevel;
|
||||
private isProduction: boolean;
|
||||
|
||||
/**
|
||||
* Logger 인스턴스 생성
|
||||
*
|
||||
* @param service - 서비스명 (예: 'openai', 'telegram')
|
||||
* @param options - 로거 옵션
|
||||
*/
|
||||
constructor(
|
||||
private service: string,
|
||||
options: LoggerOptions = {}
|
||||
) {
|
||||
this.minLevel = options.minLevel || LogLevel.INFO;
|
||||
this.isProduction = options.environment === 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그를 출력할지 결정
|
||||
*
|
||||
* @param level - 확인할 로그 레벨
|
||||
* @returns 출력 여부
|
||||
*/
|
||||
private shouldLog(level: LogLevel): boolean {
|
||||
return LOG_LEVEL_PRIORITY[level] >= LOG_LEVEL_PRIORITY[this.minLevel];
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그 엔트리를 포맷팅하여 출력
|
||||
*
|
||||
* @param entry - 로그 엔트리
|
||||
*/
|
||||
private write(entry: LogEntry): void {
|
||||
// 로깅 실패가 메인 로직을 중단시키지 않도록 try-catch
|
||||
try {
|
||||
if (this.isProduction) {
|
||||
// 프로덕션: JSON 형식 (구조화된 로그 수집 시스템용)
|
||||
console.log(JSON.stringify(entry));
|
||||
} else {
|
||||
// 개발 환경: 읽기 쉬운 형식
|
||||
const emoji = LOG_LEVEL_EMOJI[entry.level];
|
||||
const timestamp = entry.timestamp;
|
||||
const level = (entry.level as string).padEnd(5);
|
||||
const service = entry.service ? `[${entry.service}]` : '';
|
||||
const message = entry.message;
|
||||
|
||||
let output = `${emoji} [${timestamp}] ${level} ${service} ${message}`;
|
||||
|
||||
// 컨텍스트 추가
|
||||
if (entry.context && Object.keys(entry.context).length > 0) {
|
||||
output += ` ${JSON.stringify(entry.context)}`;
|
||||
}
|
||||
|
||||
// 에러 정보 추가
|
||||
if (entry.error) {
|
||||
output += `\n Error: ${entry.error.name}: ${entry.error.message}`;
|
||||
if (entry.error.stack) {
|
||||
output += `\n Stack: ${entry.error.stack}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 실행 시간 추가
|
||||
if (entry.duration !== undefined) {
|
||||
output += ` (${entry.duration}ms)`;
|
||||
}
|
||||
|
||||
console.log(output);
|
||||
}
|
||||
} catch (error) {
|
||||
// 로깅 실패 시 기본 console.error로 폴백
|
||||
console.error('[Logger] Failed to write log:', error);
|
||||
console.error('[Logger] Original log entry:', entry);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 로그 엔트리 생성 헬퍼
|
||||
*
|
||||
* @param level - 로그 레벨
|
||||
* @param message - 메시지
|
||||
* @param context - 컨텍스트
|
||||
* @param error - 에러 객체
|
||||
*/
|
||||
private createEntry(
|
||||
level: LogLevel,
|
||||
message: string,
|
||||
context?: Record<string, any>,
|
||||
error?: Error
|
||||
): LogEntry {
|
||||
const entry: LogEntry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
level,
|
||||
message,
|
||||
service: this.service,
|
||||
};
|
||||
|
||||
if (context) {
|
||||
entry.context = context;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
entry.error = {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
};
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* DEBUG 레벨 로그 출력
|
||||
*
|
||||
* @param message - 로그 메시지
|
||||
* @param context - 추가 컨텍스트 (선택)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* logger.debug('함수 호출', { functionName: 'processData', args: [1, 2, 3] });
|
||||
* ```
|
||||
*/
|
||||
debug(message: string, context?: Record<string, any>): void {
|
||||
if (!this.shouldLog(LogLevel.DEBUG)) return;
|
||||
this.write(this.createEntry(LogLevel.DEBUG, message, context));
|
||||
}
|
||||
|
||||
/**
|
||||
* INFO 레벨 로그 출력
|
||||
*
|
||||
* @param message - 로그 메시지
|
||||
* @param context - 추가 컨텍스트 (선택)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* logger.info('요청 처리 시작', { userId: '123', endpoint: '/api/data' });
|
||||
* ```
|
||||
*/
|
||||
info(message: string, context?: Record<string, any>): void {
|
||||
if (!this.shouldLog(LogLevel.INFO)) return;
|
||||
this.write(this.createEntry(LogLevel.INFO, message, context));
|
||||
}
|
||||
|
||||
/**
|
||||
* WARN 레벨 로그 출력
|
||||
*
|
||||
* @param message - 로그 메시지
|
||||
* @param context - 추가 컨텍스트 (선택)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* logger.warn('API 응답 지연', { endpoint: '/api/data', responseTime: 5000 });
|
||||
* ```
|
||||
*/
|
||||
warn(message: string, context?: Record<string, any>): void {
|
||||
if (!this.shouldLog(LogLevel.WARN)) return;
|
||||
this.write(this.createEntry(LogLevel.WARN, message, context));
|
||||
}
|
||||
|
||||
/**
|
||||
* ERROR 레벨 로그 출력
|
||||
*
|
||||
* @param message - 로그 메시지
|
||||
* @param error - 에러 객체 (선택)
|
||||
* @param context - 추가 컨텍스트 (선택)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* try {
|
||||
* await apiCall();
|
||||
* } catch (error) {
|
||||
* logger.error('API 호출 실패', error as Error, { endpoint: '/api/data' });
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
error(message: string, error?: Error, context?: Record<string, any>): void {
|
||||
if (!this.shouldLog(LogLevel.ERROR)) return;
|
||||
this.write(this.createEntry(LogLevel.ERROR, message, context, error));
|
||||
}
|
||||
|
||||
/**
|
||||
* FATAL 레벨 로그 출력 (치명적 에러)
|
||||
*
|
||||
* @param message - 로그 메시지
|
||||
* @param error - 에러 객체 (선택)
|
||||
* @param context - 추가 컨텍스트 (선택)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* logger.fatal('데이터베이스 연결 실패', error, { database: 'main' });
|
||||
* ```
|
||||
*/
|
||||
fatal(message: string, error?: Error, context?: Record<string, any>): void {
|
||||
if (!this.shouldLog(LogLevel.FATAL)) return;
|
||||
this.write(this.createEntry(LogLevel.FATAL, message, context, error));
|
||||
}
|
||||
|
||||
/**
|
||||
* 성능 측정을 위한 타이머 시작
|
||||
*
|
||||
* 반환된 함수를 호출하면 duration이 자동으로 계산되어 로그에 포함됩니다.
|
||||
*
|
||||
* @param message - 로그 메시지
|
||||
* @param context - 추가 컨텍스트 (선택)
|
||||
* @returns 타이머 종료 함수
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const end = logger.startTimer('데이터 처리', { recordCount: 1000 });
|
||||
* await processData();
|
||||
* end(); // duration이 자동으로 로그에 포함됨
|
||||
* ```
|
||||
*/
|
||||
startTimer(message?: string, context?: Record<string, any>): () => void {
|
||||
const startTime = Date.now();
|
||||
const timerMessage = message || 'Operation completed';
|
||||
|
||||
return () => {
|
||||
const duration = Date.now() - startTime;
|
||||
const entry = this.createEntry(LogLevel.INFO, timerMessage, context);
|
||||
entry.duration = duration;
|
||||
this.write(entry);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 사용자 ID를 포함하는 로거 생성
|
||||
*
|
||||
* @param userId - 사용자 ID (Telegram ID)
|
||||
* @returns 사용자 ID가 포함된 새 로거
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const userLogger = logger.withUser('123456789');
|
||||
* userLogger.info('사용자 요청 처리'); // userId가 자동으로 포함됨
|
||||
* ```
|
||||
*/
|
||||
withUser(userId: string): Logger {
|
||||
const userLogger = new Logger(this.service, {
|
||||
minLevel: this.minLevel,
|
||||
environment: this.isProduction ? 'production' : 'development',
|
||||
});
|
||||
|
||||
// 모든 로그에 userId 자동 포함
|
||||
const originalWrite = userLogger['write'].bind(userLogger);
|
||||
userLogger['write'] = (entry: LogEntry) => {
|
||||
entry.userId = userId;
|
||||
originalWrite(entry);
|
||||
};
|
||||
|
||||
return userLogger;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전역 로거 팩토리 함수
|
||||
*
|
||||
* 서비스별 로거 인스턴스를 생성합니다.
|
||||
* 환경 변수 `ENVIRONMENT`를 통해 프로덕션/개발 환경을 자동 감지합니다.
|
||||
*
|
||||
* @param service - 서비스명
|
||||
* @param env - Cloudflare Workers 환경 변수 (선택)
|
||||
* @returns Logger 인스턴스
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 기본 사용
|
||||
* const logger = createLogger('openai');
|
||||
*
|
||||
* // 환경 변수와 함께 사용
|
||||
* const logger = createLogger('telegram', env);
|
||||
*
|
||||
* // 서비스별 로거
|
||||
* const openaiLogger = createLogger('openai');
|
||||
* const telegramLogger = createLogger('telegram');
|
||||
* const depositLogger = createLogger('deposit');
|
||||
* ```
|
||||
*/
|
||||
export function createLogger(service: string, env?: Partial<Env>): Logger {
|
||||
// 환경 감지: 명시적 ENVIRONMENT 변수 또는 프로덕션 감지
|
||||
const environment =
|
||||
(env as any)?.ENVIRONMENT === 'production'
|
||||
? 'production'
|
||||
: 'development';
|
||||
|
||||
return new Logger(service, {
|
||||
minLevel: LogLevel.INFO,
|
||||
environment,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 디버그 레벨 로거 팩토리 (개발 환경 전용)
|
||||
*
|
||||
* @param service - 서비스명
|
||||
* @returns DEBUG 레벨 로거
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const logger = createDebugLogger('openai');
|
||||
* logger.debug('상세 디버그 정보', { data: complexObject });
|
||||
* ```
|
||||
*/
|
||||
export function createDebugLogger(service: string): Logger {
|
||||
return new Logger(service, {
|
||||
minLevel: LogLevel.DEBUG,
|
||||
environment: 'development',
|
||||
});
|
||||
}
|
||||
275
src/utils/metrics.ts
Normal file
275
src/utils/metrics.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/**
|
||||
* 메트릭 수집 시스템
|
||||
*
|
||||
* API 호출 성능, Circuit Breaker 상태, 에러율 등을 추적하는 메트릭 시스템
|
||||
* - 메모리 기반 (최근 1000개 메트릭만 유지)
|
||||
* - Worker 재시작 시 초기화
|
||||
* - Thread-safe 동시 요청 처리
|
||||
*/
|
||||
|
||||
/**
|
||||
* 메트릭 타입
|
||||
*/
|
||||
export type MetricType =
|
||||
| 'api_call_duration' // API 호출 시간 (ms)
|
||||
| 'api_call_count' // API 호출 횟수
|
||||
| 'api_error_count' // API 에러 횟수
|
||||
| 'circuit_breaker_state' // Circuit Breaker 상태 (0=CLOSED, 1=OPEN, 2=HALF_OPEN)
|
||||
| 'retry_count' // 재시도 횟수
|
||||
| 'cache_hit_rate'; // 캐시 히트율
|
||||
|
||||
/**
|
||||
* 메트릭 레코드
|
||||
*/
|
||||
export interface Metric {
|
||||
/** 메트릭 이름 */
|
||||
name: MetricType;
|
||||
/** 메트릭 값 */
|
||||
value: number;
|
||||
/** 타임스탬프 (ms) */
|
||||
timestamp: number;
|
||||
/** 추가 태그 (예: service, endpoint) */
|
||||
tags?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메트릭 통계
|
||||
*/
|
||||
export interface MetricStats {
|
||||
/** 샘플 수 */
|
||||
count: number;
|
||||
/** 합계 */
|
||||
sum: number;
|
||||
/** 평균 */
|
||||
avg: number;
|
||||
/** 최소값 */
|
||||
min: number;
|
||||
/** 최대값 */
|
||||
max: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 메트릭 수집기
|
||||
*
|
||||
* 성능, 에러율, Circuit Breaker 상태 등을 메모리에 수집합니다.
|
||||
* 최근 1000개 메트릭만 FIFO 방식으로 유지하여 메모리 효율성을 보장합니다.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 카운터 증가
|
||||
* metrics.increment('api_call_count', { service: 'openai' });
|
||||
*
|
||||
* // 값 기록
|
||||
* metrics.record('circuit_breaker_state', 1, { service: 'openai' });
|
||||
*
|
||||
* // 타이머 사용
|
||||
* const timer = metrics.startTimer('api_call_duration', { service: 'openai' });
|
||||
* await callAPI();
|
||||
* timer(); // duration 자동 기록
|
||||
*
|
||||
* // 통계 조회
|
||||
* const stats = metrics.getStats('api_call_duration');
|
||||
* console.log(`평균: ${stats.avg}ms`);
|
||||
* ```
|
||||
*/
|
||||
export class MetricsCollector {
|
||||
private metrics: Metric[] = [];
|
||||
private readonly maxMetrics = 1000; // 최대 메트릭 수 (FIFO)
|
||||
|
||||
/**
|
||||
* 카운터 증가
|
||||
*
|
||||
* 지정된 메트릭의 카운터를 1 증가시킵니다.
|
||||
* api_call_count, api_error_count, retry_count 등에 사용됩니다.
|
||||
*
|
||||
* @param metric - 메트릭 타입
|
||||
* @param tags - 추가 태그 (예: { service: 'openai', endpoint: '/chat' })
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* metrics.increment('api_call_count', { service: 'openai' });
|
||||
* metrics.increment('api_error_count', { service: 'context7', error: 'timeout' });
|
||||
* ```
|
||||
*/
|
||||
increment(metric: MetricType, tags?: Record<string, string>): void {
|
||||
this.record(metric, 1, tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* 값 기록
|
||||
*
|
||||
* 지정된 값을 메트릭으로 기록합니다.
|
||||
* circuit_breaker_state, cache_hit_rate, api_call_duration 등에 사용됩니다.
|
||||
*
|
||||
* @param metric - 메트릭 타입
|
||||
* @param value - 기록할 값
|
||||
* @param tags - 추가 태그
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* metrics.record('circuit_breaker_state', 1, { service: 'openai' }); // OPEN
|
||||
* metrics.record('cache_hit_rate', 0.85, { cache: 'tld_prices' });
|
||||
* metrics.record('api_call_duration', 245, { service: 'openai' });
|
||||
* ```
|
||||
*/
|
||||
record(metric: MetricType, value: number, tags?: Record<string, string>): void {
|
||||
this.metrics.push({
|
||||
name: metric,
|
||||
value,
|
||||
timestamp: Date.now(),
|
||||
tags,
|
||||
});
|
||||
|
||||
// FIFO: 최대 개수 초과 시 가장 오래된 메트릭 제거
|
||||
if (this.metrics.length > this.maxMetrics) {
|
||||
this.metrics.shift();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 타이머 시작
|
||||
*
|
||||
* API 호출 시간을 자동으로 측정하는 타이머를 시작합니다.
|
||||
* 반환된 함수를 호출하면 duration이 자동으로 기록됩니다.
|
||||
*
|
||||
* @param metric - 메트릭 타입 (보통 'api_call_duration')
|
||||
* @param tags - 추가 태그
|
||||
* @returns duration 기록 함수
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const timer = metrics.startTimer('api_call_duration', { service: 'openai' });
|
||||
* try {
|
||||
* await openaiClient.chat.completions.create(...);
|
||||
* } finally {
|
||||
* timer(); // duration 자동 기록
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
startTimer(metric: MetricType, tags?: Record<string, string>): () => void {
|
||||
const startTime = Date.now();
|
||||
return () => {
|
||||
const duration = Date.now() - startTime;
|
||||
this.record(metric, duration, tags);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 메트릭 조회
|
||||
*
|
||||
* 지정된 시간 이후의 메트릭을 조회합니다.
|
||||
* since를 생략하면 모든 메트릭을 반환합니다.
|
||||
*
|
||||
* @param since - 조회 시작 시간 (ms timestamp), 생략 시 모든 메트릭
|
||||
* @returns 메트릭 배열
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 최근 5분간 메트릭
|
||||
* const recent = metrics.getMetrics(Date.now() - 5 * 60 * 1000);
|
||||
*
|
||||
* // 모든 메트릭
|
||||
* const all = metrics.getMetrics();
|
||||
* ```
|
||||
*/
|
||||
getMetrics(since?: number): Metric[] {
|
||||
if (since === undefined) {
|
||||
return [...this.metrics];
|
||||
}
|
||||
return this.metrics.filter(m => m.timestamp >= since);
|
||||
}
|
||||
|
||||
/**
|
||||
* 통계 계산
|
||||
*
|
||||
* 지정된 메트릭의 통계를 계산합니다.
|
||||
* count, sum, avg, min, max 값을 포함합니다.
|
||||
*
|
||||
* @param metric - 메트릭 타입
|
||||
* @param tags - 필터링할 태그 (예: { service: 'openai' })
|
||||
* @returns 통계 정보
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 전체 API 호출 시간 통계
|
||||
* const stats = metrics.getStats('api_call_duration');
|
||||
* console.log(`평균: ${stats.avg}ms, 최대: ${stats.max}ms`);
|
||||
*
|
||||
* // OpenAI만 필터링
|
||||
* const openaiStats = metrics.getStats('api_call_duration', { service: 'openai' });
|
||||
* console.log(`OpenAI 평균: ${openaiStats.avg}ms`);
|
||||
* ```
|
||||
*/
|
||||
getStats(metric: MetricType, tags?: Record<string, string>): MetricStats {
|
||||
let filtered = this.metrics.filter(m => m.name === metric);
|
||||
|
||||
// 태그 필터링
|
||||
if (tags) {
|
||||
filtered = filtered.filter(m => {
|
||||
if (!m.tags) return false;
|
||||
// 모든 태그가 일치하는지 확인
|
||||
for (const key in tags) {
|
||||
if (tags[key] !== m.tags[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return { count: 0, sum: 0, avg: 0, min: 0, max: 0 };
|
||||
}
|
||||
|
||||
const values = filtered.map(m => m.value);
|
||||
const sum = values.reduce((a, b) => a + b, 0);
|
||||
const count = values.length;
|
||||
const avg = sum / count;
|
||||
const min = Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
|
||||
return { count, sum, avg, min, max };
|
||||
}
|
||||
|
||||
/**
|
||||
* 메트릭 초기화
|
||||
*
|
||||
* 모든 메트릭을 삭제합니다.
|
||||
* 테스트나 메모리 정리 목적으로 사용됩니다.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* metrics.reset();
|
||||
* ```
|
||||
*/
|
||||
reset(): void {
|
||||
this.metrics = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 메트릭 수
|
||||
*
|
||||
* 현재 수집된 메트릭의 개수를 반환합니다.
|
||||
* 메모리 사용량 모니터링에 사용됩니다.
|
||||
*
|
||||
* @returns 메트릭 수
|
||||
*/
|
||||
size(): number {
|
||||
return this.metrics.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 전역 메트릭 인스턴스
|
||||
*
|
||||
* 프로젝트 전역에서 사용하는 단일 메트릭 수집기입니다.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { metrics } from './utils/metrics';
|
||||
*
|
||||
* metrics.increment('api_call_count', { service: 'openai' });
|
||||
* const stats = metrics.getStats('api_call_duration');
|
||||
* ```
|
||||
*/
|
||||
export const metrics = new MetricsCollector();
|
||||
Reference in New Issue
Block a user