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

13 KiB
Raw Permalink Blame History

Logger 사용 가이드

구조화된 로깅 유틸리티 (src/utils/logger.ts) 사용 방법 안내

목차

  1. 개요
  2. 기본 사용법
  3. 로그 레벨
  4. 성능 측정
  5. 사용자 컨텍스트
  6. 출력 형식
  7. 마이그레이션 가이드
  8. Best Practices

개요

주요 특징

  • JSON 기반 구조화 로그: 프로덕션 환경에서 로그 수집 시스템 연동
  • 환경별 자동 전환: 개발(읽기 쉬운 포맷) ↔ 프로덕션(JSON)
  • 타입 안전성: TypeScript strict mode 지원
  • 성능 측정: 내장 타이머로 실행 시간 자동 계산
  • 에러 안전성: 로깅 실패가 메인 로직 중단시키지 않음
  • 사용자 컨텍스트: 사용자 ID 자동 포함

로그 레벨

레벨 우선순위 용도 예시
DEBUG 0 디버깅 정보 (개발 전용) 함수 호출, 파라미터 값
INFO 1 일반 정보 요청 처리, 성공 응답
WARN 2 경고 (주의 필요) API 지연, 폴백 사용
ERROR 3 에러 (복구 가능) API 실패, 재시도
FATAL 4 치명적 에러 시스템 장애, DB 연결 실패

기본 사용법

1. 로거 생성

import { createLogger } from './utils/logger';

// 서비스별 로거 생성
const logger = createLogger('openai');

2. 로그 출력

// 일반 정보
logger.info('AI 응답 생성 시작');

// 컨텍스트 포함
logger.info('API 호출 완료', {
  model: 'gpt-4o-mini',
  tokensUsed: 1000,
});

// 경고
logger.warn('API 응답 지연', {
  endpoint: '/chat/completions',
  responseTime: 3500,
});

3. 에러 처리

try {
  await callOpenAI();
} catch (error) {
  logger.error('OpenAI API 호출 실패', error as Error, {
    model: 'gpt-4o-mini',
    retryCount: 3,
  });
}

로그 레벨

DEBUG (개발 전용)

import { createDebugLogger } from './utils/logger';

const logger = createDebugLogger('debug-service');

logger.debug('함수 호출', {
  functionName: 'processData',
  arguments: [1, 2, 3],
});

INFO (기본)

logger.info('메시지 수신', {
  userId: '123456789',
  messageType: 'text',
  length: 42,
});

WARN (주의 필요)

logger.warn('Circuit breaker 경고', {
  failureCount: 2,
  threshold: 3,
});

ERROR (복구 가능)

try {
  await apiCall();
} catch (error) {
  logger.error('API 호출 실패', error as Error, {
    endpoint: '/api/data',
    retryAttempt: 2,
  });
}

FATAL (치명적)

logger.fatal('데이터베이스 연결 실패', error as Error, {
  database: 'telegram-conversations',
  connectionString: 'D1',
});

성능 측정

기본 사용

const endTimer = logger.startTimer('데이터 처리 완료', {
  recordCount: 10000,
});

await processData();

endTimer(); // duration이 자동으로 로그에 포함됨

OpenAI API 호출 예시

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 자동 포함

const logger = createLogger('telegram');

// 사용자 로거 생성 (userId 자동 포함)
const userLogger = logger.withUser('821596605');

userLogger.info('메시지 수신', {
  messageType: 'text',
  length: 42,
});

userLogger.info('AI 응답 전송', {
  responseLength: 150,
});

출력 예시

{
  "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)

{
  "timestamp": "2026-01-19T16:45:23.123Z",
  "level": "INFO",
  "service": "openai",
  "message": "AI 응답 생성 시작",
  "context": {
    "userId": "123",
    "model": "gpt-4"
  }
}
{
  "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)"
  }
}

마이그레이션 가이드

기존 코드

// ❌ 기존: console.log
console.log('[OpenAI] AI 응답 생성 시작');
console.log('[OpenAI] tool_calls:', JSON.stringify(toolCalls));
console.error('[OpenAI] API 호출 실패:', error);

새 코드

// ✅ 신규: 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

// 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

// 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

// 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. 서비스별 로거 생성

// ✅ Good: 서비스별 로거
const openaiLogger = createLogger('openai');
const telegramLogger = createLogger('telegram');
const depositLogger = createLogger('deposit');

// ❌ Bad: 로거 재사용
const logger = createLogger('app');

2. 컨텍스트 정보 포함

// ✅ Good: 컨텍스트 포함
logger.info('메시지 수신', {
  userId: '123',
  messageType: 'text',
  length: 42,
});

// ❌ Bad: 메시지에 직접 포함
logger.info(`메시지 수신: userId=123, type=text, length=42`);

3. 에러 객체 전달

// ✅ 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. 성능 측정 활용

// ✅ Good: 타이머 사용
const end = logger.startTimer('데이터 처리 완료');
await processData();
end();

// ❌ Bad: 수동 계산
const start = Date.now();
await processData();
logger.info(`데이터 처리 완료 (${Date.now() - start}ms)`);

5. 민감 정보 제외

// ✅ 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. 로그 레벨 적절히 사용

// ✅ 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 또는 환경 변수로 설정:

[vars]
ENVIRONMENT = "production"  # 또는 "development"

로그 레벨 제어

// 프로덕션: INFO 레벨 이상만 출력
const logger = createLogger('service', env);

// 개발: DEBUG 레벨부터 모든 로그 출력
const logger = createDebugLogger('service');

실제 사용 예시

OpenAI 서비스

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

예치금 서비스

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: 로그 레벨 확인:

// DEBUG 레벨은 개발 환경에서만 출력됨
const logger = createDebugLogger('service'); // DEBUG 포함
const logger = createLogger('service');      // INFO 이상만

Q: 프로덕션에서 JSON이 아닌 텍스트 로그가 출력됩니다

A: 환경 변수 확인:

// wrangler.toml
[vars]
ENVIRONMENT = "production"

Q: 에러 스택이 출력되지 않습니다

A: 에러 객체를 전달하세요:

// ✅ 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 섹션)