7.2 KiB
7.2 KiB
메트릭 시스템 사용 예시
개요
src/utils/metrics.ts는 API 호출 성능, Circuit Breaker 상태, 에러율 등을 추적하는 메트릭 수집 시스템입니다.
주요 특징
- 메모리 기반: 최근 1000개 메트릭만 FIFO 방식으로 유지
- Thread-safe: 동시 요청 안전
- 저비용: 메모리 연산만 사용하여 성능 오버헤드 최소화
- Worker 재시작 시 초기화: 장기 저장이 필요하면 향후 KV 확장 가능
기본 사용법
1. Import
import { metrics } from './utils/metrics';
2. 카운터 증가
API 호출 횟수, 에러 횟수 등을 카운트합니다.
// 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 상태, 캐시 히트율 등 특정 값을 기록합니다.
// 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 호출 시간을 자동으로 측정합니다.
const timer = metrics.startTimer('api_call_duration', { service: 'openai' });
try {
const response = await openaiClient.chat.completions.create({...});
return response;
} finally {
timer(); // 자동으로 duration 기록
}
5. 통계 조회
// 전체 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에서 상태 변경 시 메트릭을 기록합니다.
// 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에서 재시도 횟수를 기록합니다.
// 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에서 캐시 히트율을 기록합니다.
// 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에서 메트릭 엔드포인트를 추가하여 실시간 모니터링:
// 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 저장 (장기 보관)
// 주기적으로 KV에 저장 (cron)
const stats = metrics.getStats('api_call_duration');
await env.METRICS_KV.put(
`stats:${Date.now()}`,
JSON.stringify(stats),
{ expirationTtl: 86400 * 7 } // 7일 보관
);
외부 모니터링 연동
// 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'],
});
테스트
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);
});
});