288 lines
7.2 KiB
Markdown
288 lines
7.2 KiB
Markdown
# 메트릭 시스템 사용 예시
|
|
|
|
## 개요
|
|
|
|
`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);
|
|
});
|
|
});
|
|
```
|