📊 에러 로깅과 모니터링
📖 정의
**에러 로깅(Error Logging)**은 애플리케이션에서 발생하는 에러와 중요한 이벤트를 기록하는 것이고, **모니터링(Monitoring)**은 실시간으로 애플리케이션 상태를 추적하는 것입니다. console.log는 개발 환경에서만 유용하며, 프로덕션에서는 구조화된 로깅, 에러 추적 도구(Sentry), 로그 레벨, 알림 시스템이 필수입니다.
🎯 비유로 이해하기
병원의 환자 모니터링
에러 로깅을 병원 시스템에 비유하면:
console.log = 간호사의 메모
├─ 개인 수첩에 기록
├─ 체계적이지 않음
├─ 퇴근하면 못 봄
└─ 응급 상황 대응 어려움
로깅 시스템 = 전자 의료 기록
├─ 모든 정보 중앙 저장
├─ 시간순 정렬
├─ 검색 가능
└─ 여러 의사가 동시 접근
에러 모니터링 = 환자 모니터
├─ 실시간 생체 신호
├─ 이상 시 즉시 알림
├─ 대시보드로 한눈에
└─ 24시간 감시
로그 레벨 = 응급도 분류
├─ ERROR = 응급 (즉시 대응)
├─ WARN = 주의 (관찰 필요)
├─ INFO = 정상 (기록용)
└─ DEBUG = 상세 (진단용)
비행기 블랙박스
개발 환경 = 지상 테스트
└─ console.log로 충분
프로덕션 환경 = 실제 비행
├─ 블랙박스 (로깅 시스템)
│ └─ 모든 이벤트 기록
├─ 계기판 (모니터링 대시보드)
│ └─ 실시간 상태 확인
└─ 관제탑 (알림 시스템)
└─ 문제 발생 시 즉시 통보
사고 발생 시:
1. 블랙박스 확인 (로그 분석)
2. 원인 파악 (스택 트레이스)
3. 재발 방지 (모니터링 강화)
⚙️ 작동 원리
1. 로그 레벨 (Log Levels)
ERROR (높음) - 즉시 대응 필요
├─ 서버 크래시
├─ 데이터베이스 연결 실패
├─ 결제 시스템 오류
└─ 예: "PaymentService: Credit card charge failed"
WARN - 주의 필요
├─ 성능 저하
├─ 디스크 공간 부족
├─ API 응답 느림
└─ 예: "Database: Connection pool near capacity (90%)"
INFO - 정상 작동 기록
├─ 서버 시작/종료
├─ 사용자 로그인
├─ 주요 기능 실행
└─ 예: "Server started on port 3000"
DEBUG - 개발/디버깅용
├─ 함수 호출 추적
├─ 변수 값 확인
├─ 상세한 실행 흐름
└─ 예: "UserService.findById called with id=123"
TRACE (낮음) - 매우 상세
└─ 모든 세부 정보
2. 에러 추적 흐름
애플리케이션에서 에러 발생
↓
에러 캐치 (try-catch)
↓
에러 정보 수집
├─ 에러 메시지
├─ 스택 트레이스
├─ 사용자 정보
├─ 요청 정보 (URL, params)
├─ 환경 정보 (브라우저, OS)
└─ 시간
↓
로깅 시스템으로 전송
├─ Sentry
├─ LogRocket
└─ CloudWatch
↓
분석 및 알림
├─ 에러 그룹화
├─ 빈도 계산
├─ 심각도 판단
└─ Slack/이메일 알림
↓
개발자 대응
└─ 원인 파악 및 수정
3. Sentry 작동 원리
클라이언트 (브라우저/서버)
├─ Sentry SDK 초기화
├─ 자동 에러 캐치
└─ 에러 발생 시 전송
↓
Sentry 서버
├─ 에러 수신
├─ 소스맵 적용 (압축 해제)
├─ 스택 트레이스 정리
├─ 중복 제거 (같은 에러 그룹화)
├─ 영향받은 사용자 수 계산
└─ 알림 규칙 확인
↓
대시보드
├─ 에러 목록
├─ 상세 정보
├─ 타임라인
├─ 영향 범위
└─ 해결 상태
↓
알림 (조건 충족 시)
├─ Slack
├─ 이메일
├─ Discord
└─ PagerDuty
💡 실제 예시
Step 1: 기본 로깅 시스템 구축
# 프로젝트 생성
mkdir logging-demo
cd logging-demo
npm init -y
# Winston 로깅 라이브러리 설치
npm install winston
# Express 및 기타 패키지
npm install express dotenv
// logger.js
// Winston으로 구조화된 로깅 시스템 구축
const winston = require('winston');
const path = require('path');
// 로그 포맷 정의
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
// 개발 환경용 포맷 (사람이 읽기 쉽게)
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0) {
msg += ` ${JSON.stringify(meta)}`;
}
return msg;
})
);
// Logger 생성
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
transports: [
// 에러 로그 파일
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// 전체 로그 파일
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5
})
]
});
// 개발 환경에서는 콘솔에도 출력
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: consoleFormat
}));
}
// 헬퍼 함수
logger.logRequest = (req, res, duration) => {
logger.info('HTTP Request', {
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
});
};
logger.logError = (error, req = null) => {
const errorInfo = {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
};
if (req) {
errorInfo.request = {
method: req.method,
url: req.originalUrl,
headers: req.headers,
body: req.body,
query: req.query,
params: req.params,
ip: req.ip
};
}
logger.error('Application Error', errorInfo);
};
module.exports = logger;
// server.js
require('dotenv').config();
const express = require('express');
const logger = require('./logger');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
// 요청 로깅 미들웨어
app.use((req, res, next) => {
const start = Date.now();
// 응답이 끝나면 로그 기록
res.on('finish', () => {
const duration = Date.now() - start;
logger.logRequest(req, res, duration);
});
next();
});
// 정상 라우트
app.get('/', (req, res) => {
logger.info('Home page accessed');
res.json({ message: 'Welcome to Logging Demo' });
});
// 의도적으로 에러 발생
app.get('/error', (req, res) => {
logger.warn('Error endpoint accessed - this will throw an error');
// 에러 발생!
throw new Error('This is a test error');
});
// 데이터베이스 시뮬레이션
app.get('/users/:id', (req, res) => {
const userId = req.params.id;
logger.debug('Fetching user', { userId });
// 사용자 조회 (시뮬레이션)
if (userId === '1') {
logger.info('User found', { userId, username: 'john_doe' });
res.json({ id: 1, username: 'john_doe' });
} else {
logger.warn('User not found', { userId });
res.status(404).json({ error: 'User not found' });
}
});
// 느린 API 시뮬레이션
app.get('/slow', async (req, res) => {
const delay = 3000;
logger.warn('Slow API called', { delay });
await new Promise(resolve => setTimeout(resolve, delay));
res.json({ message: 'This was slow' });
});
// 404 핸들러
app.use((req, res) => {
logger.warn('404 Not Found', {
method: req.method,
url: req.originalUrl
});
res.status(404).json({ error: 'Not Found' });
});
// 에러 핸들러
app.use((err, req, res, next) => {
// 에러 로깅
logger.logError(err, req);
// 클라이언트 응답
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
app.listen(PORT, () => {
logger.info('Server started', {
port: PORT,
environment: process.env.NODE_ENV || 'development'
});
});
module.exports = app;
# .env
PORT=3000
NODE_ENV=development
LOG_LEVEL=debug
# logs 디렉토리 생성
mkdir logs
# .gitignore에 추가
echo "logs/" >> .gitignore
# 서버 실행
node server.js
# 출력 예시:
# 2024-01-15 10:00:00 [info]: Server started {"port":"3000","environment":"development"}
# 테스트
curl http://localhost:3000
# 2024-01-15 10:00:05 [info]: Home page accessed
# 2024-01-15 10:00:05 [info]: HTTP Request {"method":"GET","url":"/","status":200,"duration":"5ms",...}
curl http://localhost:3000/error
# 2024-01-15 10:00:10 [warn]: Error endpoint accessed - this will throw an error
# 2024-01-15 10:00:10 [error]: Application Error {"message":"This is a test error","stack":"Error: This is a test error\n at..."}
# 로그 파일 확인
tail -f logs/combined.log
tail -f logs/error.log
Step 2: Sentry 통합 (프로덕션 에러 추적)
# Sentry 패키지 설치
npm install @sentry/node @sentry/tracing
// sentry.js
const Sentry = require('@sentry/node');
const Tracing = require('@sentry/tracing');
function initSentry(app) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV || 'development',
tracesSampleRate: 1.0, // 성능 모니터링: 100% 요청 추적
// Release 버전 (git commit hash 사용 권장)
release: process.env.APP_VERSION || '1.0.0',
// Express 통합
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
new Tracing.Integrations.Express({ app })
]
});
return Sentry;
}
module.exports = { initSentry };
// server.js (Sentry 추가)
require('dotenv').config();
const express = require('express');
const logger = require('./logger');
const { initSentry } = require('./sentry');
const app = express();
const Sentry = initSentry(app);
// Sentry 미들웨어 (최상단에 배치)
app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());
app.use(express.json());
// ... 기존 라우트 ...
// 에러 핸들러 (Sentry 에러 핸들러 먼저)
app.use(Sentry.Handlers.errorHandler());
// 커스텀 에러 핸들러
app.use((err, req, res, next) => {
// Winston으로도 로깅
logger.logError(err, req);
// Sentry에 추가 컨텍스트 전송
Sentry.withScope((scope) => {
scope.setUser({
id: req.user?.id,
username: req.user?.username,
email: req.user?.email
});
scope.setExtra('requestBody', req.body);
scope.setExtra('requestQuery', req.query);
Sentry.captureException(err);
});
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
logger.info('Server started with Sentry monitoring', {
port: PORT,
environment: process.env.NODE_ENV
});
});
# .env에 Sentry DSN 추가
cat >> .env << 'EOF'
SENTRY_DSN=https://abc123xyz456@o123456.ingest.sentry.io/7890123
APP_VERSION=1.0.0
EOF
# Sentry DSN 얻기:
# 1. https://sentry.io 가입
# 2. 새 프로젝트 생성 (Node.js)
# 3. DSN 복사
// 에러 발생 시 Sentry 대시보드에서 확인 가능:
// - 에러 메시지와 스택 트레이스
// - 발생 시간과 빈도
// - 영향받은 사용자 수
// - 요청 정보 (URL, headers, body)
// - 사용자 정보 (로그인한 경우)
// - 환경 정보 (Node 버전, OS 등)