Home Logging Overview
Post
Cancel

Logging Overview

Logging

목차


Logging의 역할

로그는 시스템에서 발생한 사건을 시간 순서로 남긴 기록이다. 장애 분석, 감사, 디버깅, 성능 모니터링에 필수적이지만 너무 많으면 비용과 소음이 증가한다.

1
2
3
4
5
6
7
8
9
10
11
Application
    ↓ (이벤트 발생)
Logging
    ↓ (시간 순서로 기록)
Log File (Local)
    ↓ (수집)
Collector (Fluentd, Logstash)
    ↓ (저장)
Storage (Elasticsearch, S3)
    ↓ (검색/분석)
Logging Platform (Kibana, Splunk)

로그가 필요한 이유

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
장애 분석:
- "왜 주문 처리가 실패했는가?"
- 로그에서 ERROR부터 역추적

성능 모니터링:
- "어느 함수가 느린가?"
- execution time 로그로 추적

감사(Audit):
- "누가 언제 이 데이터를 수정했는가?"
- 모든 주요 작업 기록

보안:
- "비정상 접근이 있었는가?"
- 로그인 실패, 권한 위반 기록

Log Level

“DEBUG, INFO, WARN, ERROR는 어떻게 구분하는가?”

Log Level은 로그의 심각도를 나타내며, 어떤 수준 이상의 로그만 출력할지 결정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DEBUG
│ 개발자 디버깅용 상세 정보
│ 예: 함수 진입/종료, 변수값, SQL 쿼리
│ 프로덕션에서는 꺼짐 (성능 영향)
│
INFO
│ 애플리케이션 정상 동작 정보
│ 예: 요청 수신, 처리 완료, 서버 시작
│ 프로덕션에서 기본 레벨
│
WARN
│ 문제가 될 가능성 있음
│ 예: deprecated API 사용, 권장되지 않는 설정, 높은 메모리 사용
│ 주의 필요 (곧 에러가 될 수 있음)
│
ERROR
│ 에러 발생, 작업 실패
│ 예: 데이터베이스 연결 실패, 유효성 검증 실패
│ 즉시 대응 필요
│
FATAL/CRITICAL
  애플리케이션 자체가 동작 불가
  예: 시스템 시작 실패, 메모리 부족
  긴급 대응 필수

Level별 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import logging

logger = logging.getLogger(__name__)

# DEBUG: 상세한 흐름 정보
logger.debug(f"Processing order {order_id} from user {user_id}")

# INFO: 정상 동작 정보
logger.info(f"Order {order_id} created successfully")

# WARN: 주의 필요
logger.warning(f"Slow query detected: {query_time}ms > 1000ms threshold")

# ERROR: 에러 (작업 실패하지만 앱은 계속)
logger.error(f"Failed to send email to {email}: {error}")

# CRITICAL: 심각한 에러 (앱 중단)
logger.critical(f"Database connection lost, cannot continue")

Log Level 설정

1
2
3
4
5
6
7
8
9
10
11
# production
log_level: INFO
# DEBUG, INFO, WARN, ERROR, CRITICAL만 기록

# staging
log_level: DEBUG
# 모든 레벨 기록

# local development
log_level: DEBUG
# 디버깅 정보 포함

Level별 저장 비용

1
2
3
4
5
DEBUG 포함: 로그 용량 10배 증가, 비용 10배
INFO 기본: 1GB/일 (기준)
WARN/ERROR만: 100MB/일 (10% 비용)

→ 환경별로 다르게 설정 필수

Structured Logging

“structured logging은 왜 검색에 유리한가?”

Structured Logging은 로그를 JSON 같은 정형화된 형식으로 저장해서 필드별로 검색/필터링 가능하게 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Text Logging (비정형):
"2026-05-23T10:15:30 ERROR Failed to create order for user 12345"
→ 문자열 검색만 가능 (느림, 부정확)

Structured Logging (정형):
{
  "timestamp": "2026-05-23T10:15:30",
  "level": "ERROR",
  "action": "create_order",
  "user_id": 12345,
  "error_code": "INVALID_PAYMENT",
  "message": "Failed to create order"
}
→ 필드별 검색 가능 (빠름, 정확)

검색 예시

1
2
3
4
5
6
7
-- Text 검색 (느리고 부정확)
grep "ERROR.*order" logs.txt

-- Structured 검색 (빠르고 정확)
SELECT * FROM logs WHERE level='ERROR' AND action='create_order'
SELECT * FROM logs WHERE user_id=12345 AND timestamp > '2026-05-23'
SELECT COUNT(*) FROM logs WHERE error_code='INVALID_PAYMENT'

Structured Logging 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import logging
import json
from datetime import datetime

class StructuredFormatter(logging.Formatter):
    def format(self, record):
        log_dict = {
            "timestamp": datetime.utcnow().isoformat(),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
            "module": record.module,
            "function": record.funcName,
            "line": record.lineno,
        }
        
        # 추가 필드
        if hasattr(record, 'user_id'):
            log_dict['user_id'] = record.user_id
        if hasattr(record, 'request_id'):
            log_dict['request_id'] = record.request_id
        
        return json.dumps(log_dict)

# 사용
logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = StructuredFormatter()
handler.setFormatter(formatter)
logger.addHandler(handler)

# 로깅 시 추가 정보
logger = logging.LoggerAdapter(logger, {'user_id': 12345, 'request_id': 'abc-123'})
logger.info("Order created")

Structured Logging의 이점

1
2
3
4
5
6
7
8
9
10
11
검색 속도:
- Text: 초 단위
- Structured: 밀리초 단위

정확성:
- Text: "order" 검색 → 모든 "order" 포함 단어 매칭
- Structured: action='order_created' 필드만 매칭

분석:
- Text: 수동 파싱 필요
- Structured: 바로 집계 가능 (user별 에러율, 시간대별 성능 등)

Correlation ID

“correlation id는 분산 시스템에서 왜 필요한가?”

Correlation ID는 하나의 사용자 요청이 여러 서비스를 거쳐가며 남기는 로그들을 연결하는 식별자다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
사용자 요청
    ↓ API Gateway (correlation_id: req-abc123)
    
    ├→ User Service (로그: correlation_id=req-abc123)
    │   ├→ Database (로그: correlation_id=req-abc123)
    │   └→ Cache (로그: correlation_id=req-abc123)
    │
    ├→ Order Service (로그: correlation_id=req-abc123)
    │   ├→ Database (로그: correlation_id=req-abc123)
    │   └→ Kafka (로그: correlation_id=req-abc123)
    │
    └→ Payment Service (로그: correlation_id=req-abc123)
        ├→ Payment Gateway (로그: correlation_id=req-abc123)
        └→ Database (로그: correlation_id=req-abc123)

→ correlation_id=req-abc123로 모든 로그 추적 가능

없으면 어떻게 되는가?

1
2
3
4
5
6
7
8
9
10
문제: "주문이 실패했다고 하는데 왜?"

Correlation ID 없이:
- 로그 시간으로 찾기 (부정확, 동시 요청 구분 불가)
- 여러 서비스의 로그를 연결할 수 없음
- 전체 흐름을 따라가기 어려움

Correlation ID 사용:
grep "correlation_id=req-abc123" all-logs.txt
→ 전체 흐름이 명확하게 보임

구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from uuid import uuid4
import logging

class RequestContextFilter(logging.Filter):
    def filter(self, record):
        record.correlation_id = getattr(request_context, 'correlation_id', 'none')
        return True

# HTTP 요청 처리 시
from flask import Flask, request
import uuid

app = Flask(__name__)

@app.before_request
def before_request():
    # 기존 header에 있으면 사용, 없으면 생성
    correlation_id = request.headers.get('X-Correlation-ID', str(uuid4()))
    request.correlation_id = correlation_id

@app.after_request
def after_request(response):
    # Response에도 correlation ID 포함 (클라이언트가 참조 가능)
    response.headers['X-Correlation-ID'] = request.correlation_id
    return response

Correlation ID 전파

1
2
3
4
5
6
7
8
9
10
# Service A에서 Service B를 호출할 때
import requests

correlation_id = request.correlation_id

# Service B로 correlation_id 전달
response = requests.get(
    'http://service-b/endpoint',
    headers={'X-Correlation-ID': correlation_id}
)

Log Aggregation

Log Aggregation이란?

여러 서버/서비스의 로그를 한 곳에 모아서 중앙에서 검색하고 분석한다.

1
2
3
4
5
6
7
8
9
Server 1 (로그 파일: /var/log/app.log)
Server 2 (로그 파일: /var/log/app.log)
Server 3 (로그 파일: /var/log/app.log)
    ↓ (수집)
Log Collector (Fluentd, Logstash, Beats)
    ↓ (전송)
Central Storage (Elasticsearch, Splunk, CloudWatch)
    ↓ (검색)
UI (Kibana, Splunk, Datadog)

필요한 이유

1
2
3
4
5
6
7
8
9
10
11
로그 aggregation 없이:
- 각 서버에 SSH 접속
- tail -f로 실시간 확인
- 여러 서버 동시 모니터링 불가
- 과거 로그 검색 불가

로그 aggregation 사용:
- 중앙 UI에서 모든 로그 검색
- 실시간 대시보드
- 여러 서버의 로그 관계 분석
- 자동 알림 설정

구현 예시 (Elasticsearch + Logstash + Kibana)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1. Logstash 설정 (/etc/logstash/conf.d/app.conf)
input {
  file {
    path => "/var/log/app.log"
    start_position => "beginning"
  }
}

filter {
  json {
    source => "message"
  }
  date {
    match => [ "timestamp", "ISO8601" ]
    target => "@timestamp"
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "app-%{+YYYY.MM.dd}"
  }
}

# 2. 실행
logstash -f /etc/logstash/conf.d/app.conf

# 3. Kibana에서 검색
# URL: http://localhost:5601
# Index: app-*
# 검색: level=ERROR AND correlation_id=req-abc123

Retention

Retention이란?

로그를 어떤 기간 동안 보관할지 결정하는 정책이다.

1
2
3
4
5
6
7
Retention 기간별 비용:
7일: 최소 비용 (최근 문제만)
30일: 중간 비용 (1개월 분석)
90일: 높은 비용 (분기별 분석)
1년: 매우 높은 비용 (감사, 규제 준수)

→ 규제 요구사항과 비용을 고려해 결정

환경별 Retention

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Development:
  retention: 7 days  # 최근 이슈만 추적
  cost: 낮음

Staging:
  retention: 30 days  # 배포 전 테스트 결과 보관
  cost: 중간

Production:
  retention: 90 days  # 장애 분석, 성능 추적
  cost: 높음

Compliance (금융/의료):
  retention: 1-7 years  # 규제 요구사항
  cost: 매우 높음

Retention 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Elasticsearch Index Lifecycle Management
PUT _ilm/policy/app-logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0d",
        "actions": {
          "rollover": {
            "max_size": "10gb",
            "max_age": "1d"
          }
        }
      },
      "warm": {
        "min_age": "3d",
        "actions": {
          "set_priority": {
            "priority": 50
          }
        }
      },
      "delete": {
        "min_age": "90d",
        "actions": {
          "delete": {}
        }
      }
    }
  }
}

보안과 민감 정보

“민감 정보는 로그에 왜 남기면 안 되는가?”

로그는 다양한 사람이 접근하고, 보안 위협에 노출될 수 있으므로 민감 정보를 기록하면 데이터 유출 위험이 높아진다.

1
2
3
4
5
6
7
위험한 로그:
logger.info(f"User {user_id} logged in with password {password}")
→ 누가 로그 파일/UI 봤을 때 비밀번호 노출!

안전한 로그:
logger.info(f"User {user_id} logged in")
→ 비밀번호는 기록하지 않음

민감 정보 예시

1
2
3
4
5
6
7
8
9
10
11
12
절대 기록하면 안 됨:
- 비밀번호
- API 키, 토큰
- 신용카드 번호
- SSN, 주민등록번호
- 개인 이메일, 휴대폰
- 의료 정보

기록해도 되지만 마스킹:
- 이메일: user***@example.com
- 카드번호: ****-****-****-1234
- IP: 203.0.113.***

마스킹 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import re
import logging

class MaskingFilter(logging.Filter):
    def filter(self, record):
        # 신용카드 번호 마스킹
        record.msg = re.sub(
            r'\b\d{4}[- ]?\d{4}[- ]?\d{4}[- ]?\d{4}\b',
            '****-****-****-****',
            record.msg
        )
        
        # API 키 마스킹
        record.msg = re.sub(
            r'(api_key|token)=[\w-]+',
            r'\1=***',
            record.msg
        )
        
        # 이메일 마스킹
        record.msg = re.sub(
            r'[\w.-]+@[\w.-]+',
            lambda m: m.group(0).replace(m.group(0).split('@')[0][:-3], '***'),
            record.msg
        )
        
        return True

logger = logging.getLogger(__name__)
logger.addFilter(MaskingFilter())

GDPR/규제 준수

1
2
3
4
5
6
7
8
9
개인정보보호법:
- 개인 식별 가능한 정보는 필요한 기간만 보관
- 로그는 보통 추적 목적이므로 민감 정보 제외
- retention 정책 명확히 (규제 요구사항)

예시:
- 신용카드: 13개월 보관 (규제)
- 거래 기록: 6년 보관 (세무)
- 접근 로그: 90일 보관 (보안)

정리

개념설명
Log LevelDEBUG, INFO, WARN, ERROR, CRITICAL
Structured LoggingJSON 형식의 정형화된 로그
Correlation ID분산 시스템의 요청 추적 ID
Log Aggregation중앙 집중식 로그 수집/검색
Retention로그 보관 기간 정책
Masking민감 정보 숨김 처리

This post is licensed under CC BY 4.0 by the author.