Files
telegram-bot-workers/docs/metrics-usage-example.md
kappa c0e47482c4 feat(phase-5-3): 모니터링 강화
logger.ts, metrics.ts, /api/metrics 추가
Version: e3bcb4ae
2026-01-19 16:43:36 +09:00

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