feat(phase-5-3): 모니터링 강화
logger.ts, metrics.ts, /api/metrics 추가 Version: e3bcb4ae
This commit is contained in:
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 섹션)
|
||||
Reference in New Issue
Block a user