로그 설계와 관측성 — 장애 대응 시간을 절반으로 줄인 운영 방식

게시일: 2025년 4월 25일 · 14분 읽기

새벽 3시에 장애 콜 받고 로그 뒤지는데 grep으로 30분 쓴 적 있다.

문제: 로그의 무질서

처음 로깅:

console.log('User login');
console.log('DB error');
console.log('API response sent');

문제:

Solution 1: Structured Logging

JSON 형태로 로그:

// ✅ 좋음
logger.info({
    event: 'user_login',
    userId: '12345',
    email: 'user@example.com',
    timestamp: new Date().toISOString(),
    duration: 234,  // ms
});

// 또는
logger.info('User login', {
    userId: '12345',
    email: 'user@example.com',
});

로그 포맷:

{"timestamp":"2024-11-10T03:15:45Z","level":"info","event":"user_login","userId":"12345","email":"user@example.com","duration":234}

이제 파싱 가능하고, 검색 가능하다.

로그 레벨 규칙

logger.debug('Entering function processAudio');  // 개발 중에만
logger.info('Call started, callId=123');         // 일반 이벤트
logger.warn('Slow query detected, 2.5s');       // 문제 신호
logger.error('Failed to save user', error);     // 에러 (복구 가능)
logger.fatal('Out of memory');                  // 중대 에러 (복구 불가)

Correlation ID

한 요청의 모든 로그를 추적:

// Express middleware
app.use((req, res, next) => {
    req.correlationId = req.headers['x-correlation-id'] || crypto.randomUUID();
    res.setHeader('X-Correlation-ID', req.correlationId);
    next();
});

// 로그할 때 항상 correlationId 포함
logger.info('Processing request', {
    correlationId: req.correlationId,
    userId: req.user.id,
    path: req.path,
});

이제 correlationId로 한 요청의 모든 로그를 한 번에 볼 수 있다.

Winston + Loki 설정

import winston from 'winston';
import WinstonLoki from 'winston-loki';

const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    format: winston.format.json(),
    transports: [
        // Console (개발 중)
        new winston.transports.Console({
            format: winston.format.combine(
                winston.format.colorize(),
                winston.format.simple()
            ),
        }),

        // Loki (프로덕션)
        new WinstonLoki({
            host: 'http://loki:3100',
            labels: { app: 'audio-chat' },
            json: true,
        }),
    ],
});

export default logger;

Loki 쿼리 예시

특정 사용자의 모든 로그:

{app="audio-chat"} | json | userId="12345"

에러만 보기:

{app="audio-chat"} | json | level="error"

느린 요청:

{app="audio-chat"} | json | duration > 1000

Grafana Alerting

조건:

에러 로그가 1분 동안 5개 이상이면 alert

// Loki query
sum(count_over_time({app="audio-chat"} | json | level="error" [1m])) > 5

장애 대응 프로세스

Before (30분):

1. SSH 접속 (2분)
2. 로그 파일 찾기 (3분)
3. grep으로 검색 (10분)
4. 원인 파악 (15분)

After (5분):

1. Grafana alert 보기 (30초)
2. Loki에서 correlationId 검색 (1분)
3. 원인 명확 (3분 30초)

핵심 로그 이벤트

음성 채팅 앱이라면:

logger.info('call_started', {
    callId: call.id,
    participantIds: [user1, user2],
    duration: 0,
});

logger.info('call_message_sent', {
    callId: call.id,
    userId: user.id,
    messageLength: message.length,
});

logger.error('call_connection_failed', {
    callId: call.id,
    userId: user.id,
    error: error.message,
    errorCode: error.code,
});

결론

로그는 애프리케이션의 눈이다. 구조화된 로그 없으면 장애 대응이 무질서하다.

투자:

효과: 장애 대응 시간 50% 단축 (영구적)

가장 좋은 투자다.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.