feat(phase-5-3): 모니터링 강화

logger.ts, metrics.ts, /api/metrics 추가
Version: e3bcb4ae
This commit is contained in:
kappa
2026-01-19 16:43:36 +09:00
parent a2194a5d45
commit c0e47482c4
12 changed files with 2491 additions and 5 deletions

591
docs/logger-guide.md Normal file
View 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
View 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)

View 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);
});
});
```