[도커 30강] 22강. 로그 수집 전략과 표준 출력 설계

[도커 30강] 22강. 로그 수집 전략과 표준 출력 설계

컨테이너 운영이 어려워지는 가장 큰 이유 중 하나는, 장애는 분명히 발생했는데 “어디를 봐야 하는지”를 팀이 합의하지 못한 상태로 시간이 흘러버리기 때문입니다. 로컬에서는 터미널에 보이던 로그가 운영에서는 파일로 흩어지고, 일부 서비스는 JSON 로그를 남기고 일부는 사람이 읽기 좋은 문자열만 남겨서 검색이 어려워집니다. 결국 문제를 고치는 시간보다 “로그를 찾는 시간”이 더 길어집니다. 오늘 강의의 주제는 바로 이 병목을 줄이는 실전 설계입니다.

핵심은 단순합니다. 컨테이너의 기본 로그 경로는 파일이 아니라 표준 출력(stdout/stderr)으로 통일하고, 수집은 플랫폼이 담당하게 만든다는 원칙입니다. 이 원칙을 지키면 도커 단일 호스트, Compose, Kubernetes 어느 환경으로 가도 운영 방식이 크게 흔들리지 않습니다. 또한 로그 포맷을 초기에 표준화하면, 나중에 ELK/Opensearch/Loki/Cloud Logging 같은 도구로 확장할 때도 비용이 작아집니다.


핵심 개념

  • 컨테이너 관점에서 가장 안전한 기본값은 애플리케이션 로그를 stdout/stderr로 출력하는 것입니다. 도커 엔진이 이를 수집하고 docker logs로 조회할 수 있으며, 오케스트레이터도 같은 가정을 사용합니다.
  • 로그는 “많이 남기는 것”보다 “구조적으로 남기는 것”이 중요합니다. 최소 필드는 timestamp, level, service, message, trace_id를 권장합니다.
  • 표준 출력 설계를 잘하면 애플리케이션 내부 파일 경로(/var/log/app.log)에 덜 의존하게 되어, 컨테이너 교체/스케일아웃 상황에서도 관측성이 유지됩니다.
  • 로그 레벨 정책(DEBUG/INFO/WARN/ERROR)을 환경별로 구분해야 합니다. 개발과 운영에서 같은 verbosity를 쓰면 비용과 노이즈가 급격히 증가합니다.
  • PII(개인정보), 토큰, 비밀번호는 로그에 남기지 않는 게 원칙입니다. “일단 남기고 나중에 마스킹”은 운영에서 거의 실패합니다.

기본 사용

예제 1) 표준 출력 중심 애플리케이션 로그 확인

mkdir -p ~/docker100/lesson22
cd ~/docker100/lesson22

cat > app.py <<'PY'
import json, os, sys, time, datetime, uuid

service = os.getenv("SERVICE_NAME", "payments-api")

for i in range(5):
    log = {
        "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
        "level": "INFO",
        "service": service,
        "trace_id": str(uuid.uuid4()),
        "message": f"request handled count={i}"
    }
    print(json.dumps(log), flush=True)
    time.sleep(1)

print(json.dumps({
    "timestamp": datetime.datetime.utcnow().isoformat() + "Z",
    "level": "ERROR",
    "service": service,
    "trace_id": str(uuid.uuid4()),
    "message": "db timeout"
}), file=sys.stderr, flush=True)
PY

cat > Dockerfile <<'DOCKER'
FROM python:3.12-slim
WORKDIR /app
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
DOCKER

docker build -t devlab/logging-stdout:demo .
docker run --name lesson22-log-demo --rm devlab/logging-stdout:demo

설명:

  • 애플리케이션이 파일 대신 stdout/stderr로 JSON 로그를 출력합니다.
  • 도커는 별도 에이전트 없이도 기본 로그 드라이버로 해당 출력을 수집합니다.
  • print(..., flush=True)로 버퍼링 지연을 줄여 실시간성을 확보했습니다.

예제 2) docker logs로 구조화 로그 필터링/추적

docker run -d --name lesson22-log-demo devlab/logging-stdout:demo
sleep 2

# 최근 로그 확인
docker logs lesson22-log-demo --tail 20

# 시간 기준으로 조회
docker logs lesson22-log-demo --since 10m

# stderr만 따로 보기
docker logs lesson22-log-demo --tail 50 2>&1 | grep '"level": "ERROR"'

# trace_id 추적으로 특정 요청 흐름 찾기
docker logs lesson22-log-demo 2>&1 | grep 'trace_id'

설명:

  • 운영 초기 단계에서는 docker logs만으로도 기본 진단이 가능합니다.
  • JSON 포맷을 사용하면 grep/jq 기반 임시 분석이 쉬워집니다.
  • 단, 장기 보관/검색/알림은 중앙 수집기로 넘겨야 안정적입니다.

예제 3) Compose에서 로그 정책과 회전(log rotation) 적용

cat > compose.yaml <<'YAML'
services:
  api:
    image: devlab/logging-stdout:demo
    environment:
      - SERVICE_NAME=payments-api
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"
YAML

docker compose up -d
docker compose logs -f --tail=50 api

docker inspect api | grep -A8 '"LogConfig"'

설명:

  • json-file 드라이버의 max-size, max-file은 디스크 폭주를 막는 최소 안전장치입니다.
  • 단일 서버 운영에서 로그 회전 정책이 없으면 /var/lib/docker가 빠르게 가득 차 장애로 이어질 수 있습니다.
  • Compose 단계에서 정책을 코드로 남겨두면 팀 간 편차가 줄어듭니다.

예제 4) 실무용 로그 스키마 검증 루틴 만들기

# 컨테이너 로그를 파일로 받아 스키마 검증
docker logs lesson22-log-demo 2>&1 > sample.log

# 필수 필드가 모두 존재하는지 빠르게 검사
cat sample.log | jq -c 'select(.timestamp and .level and .service and .message and .trace_id)' | wc -l

# level 분포 확인
cat sample.log | jq -r '.level' | sort | uniq -c

# 민감정보 탐지
grep -Ei 'password|token|secret|authorization' sample.log || echo 'no obvious secrets found'

설명:

  • 팀 표준 스키마가 실제 런타임에서도 지켜지는지 자동 검증하는 습관이 중요합니다.
  • 장애 후 포렌식에서 “필드 누락 로그”가 가장 큰 재해 요인 중 하나입니다.
  • CI에 동일 검사를 붙이면 로그 품질 회귀를 조기에 잡을 수 있습니다.

자주 하는 실수

실수 1) 컨테이너 내부 파일 로그만 남기기

  • 원인: VM 시대 습관(예: /var/log/app.log)을 그대로 가져옴.
  • 문제: 컨테이너 재생성 시 파일 유실, 다중 인스턴스에서 수집 누락, 운영자가 컨테이너마다 접속해야 하는 비효율.
  • 해결: 애플리케이션 기본 출력은 stdout/stderr로 통일하고, 파일 기록이 꼭 필요하면 부가 용도로만 제한합니다.

실수 2) 사람이 읽기 좋은 문장만 남기고 구조화 필드 생략

  • 원인: 로그는 눈으로만 본다는 가정.
  • 문제: 검색, 집계, 알림 규칙 작성이 어려워지고 장애 원인 분석 시간이 길어짐.
  • 해결: JSON 로그 표준을 도입하고 필수 키(timestamp, level, service, message, trace_id)를 강제합니다.

실수 3) DEBUG 로그를 운영에서 상시 활성화

  • 원인: “혹시 모르니 많이 찍자” 문화.
  • 문제: 저장 비용 증가, 중요한 ERROR가 노이즈에 묻힘, 성능 저하 가능성.
  • 해결: 운영 기본은 INFO 이상, 이슈 발생 시 제한 시간/대상에 한해 DEBUG를 동적 활성화합니다.

실수 4) 민감정보를 로그에 그대로 출력

  • 원인: 요청 전체 객체를 편하게 덤프.
  • 문제: 개인정보/보안 사고, 규제 위반 위험, 로그 저장소 자체가 리스크가 됨.
  • 해결: 애플리케이션 레벨에서 마스킹/필드 제외를 기본 정책으로 두고, 코드리뷰 체크리스트에 포함합니다.

실무 패턴

실무에서 로그는 “남기는 기술”이 아니라 “문제를 빨리 찾기 위한 정보 설계”입니다. 아래 패턴을 지키면 작은 팀도 운영 성숙도를 빠르게 높일 수 있습니다.

  • 패턴 1: 로그 계약(Log Contract) 문서화
    서비스별 필수 필드, 레벨 기준, 금지 필드(PII)를 짧은 문서로 합의합니다. 이 문서가 있어야 팀원이 바뀌어도 로그 품질이 유지됩니다.

  • 패턴 2: 요청 단위 추적 키 고정
    API 게이트웨이나 앱 미들웨어에서 trace_id를 생성/전파하고, 모든 로그에 포함합니다. 장애 대응 시 “한 요청의 여정”을 재구성하는 데 필수입니다.

  • 패턴 3: 환경별 로깅 강도 분리
    dev/staging/prod에서 로그 레벨과 샘플링 비율을 다르게 설정합니다. 운영은 안정성과 비용을 최우선으로 두고, 상세 진단은 샘플링+일시적 증가 전략을 씁니다.

  • 패턴 4: 수집기 의존은 플랫폼으로, 애플리케이션 의존은 최소화
    앱 코드에 특정 벤더 SDK를 과하게 묶기보다 stdout 출력 표준을 우선하고, 수집/전송은 사이드카나 노드 에이전트에서 처리합니다.

  • 패턴 5: 알림 기준은 로그 단건이 아니라 패턴 기반
    ERROR 한 줄로 즉시 페이지하는 체계는 피로도를 높입니다. 5분간 ERROR 비율, 특정 메시지 급증, 핵심 API 실패율 등 맥락 기반 임계치를 설계하세요.

  • 패턴 6: 보존 기간과 인덱싱 등급 분리
    모든 로그를 동일하게 오래 보관하면 비용이 폭증합니다. 감사 대상·보안 로그·애플리케이션 일반 로그를 등급화해 보존 기간을 분리합니다.

요약하면, 로그 전략의 목적은 “언젠가 볼 데이터”를 쌓는 것이 아니라 장애 30분 안에 원인 후보를 좁힐 수 있는 체계를 만드는 것입니다. 이 목표를 기준으로 보면 어떤 필드를 추가하고 어떤 로그를 줄일지가 훨씬 명확해집니다.

오늘의 결론

한 줄 요약: 컨테이너 로그는 stdout/stderr 중심으로 구조화(JSON)하고, 레벨·민감정보·회전 정책까지 함께 설계해야 실제 운영에서 빠르게 문제를 찾을 수 있다.

연습문제

  1. 현재 서비스 하나를 선택해 파일 로그를 stdout JSON 로그로 전환하고, 필수 필드 5개를 포함해보세요.
  2. Compose 또는 Docker run 기준으로 로그 회전 정책(max-size, max-file)을 적용한 뒤 디스크 사용량 변화를 비교해보세요.
  3. 샘플 장애 시나리오(예: DB timeout)를 만들고 trace_id로 요청 흐름을 추적해 원인 분석 리포트를 작성해보세요.

이전 강의 정답

21강 연습문제 해설:

  • 리소스 제한은 “많이 제한할수록 좋다”가 아니라 서비스 특성에 맞는 기준점 찾기가 핵심입니다. CPU 바운드 작업은 --cpus 제한이 지연 시간에 미치는 영향을 먼저 측정하고, 메모리 바운드 작업은 OOM 발생 임계값을 관찰하며 점진적으로 조정해야 합니다.
  • --memory만 지정하고 --memory-swap을 무시하면 예상치 못한 스왑 동작으로 응답성이 크게 떨어질 수 있습니다. 운영에서는 메모리/스왑 정책을 함께 정의하고, 노드의 실제 스왑 정책과도 일치시키는 것이 안전합니다.
  • 제한값은 로컬 체감이 아니라 메트릭 근거로 정해야 합니다. 평균이 아니라 P95/P99 지연, 재시작 횟수, OOM 이벤트를 기준으로 잡아야 과도한 낙관 설정을 피할 수 있습니다.

실습 환경/재현 정보

  • OS: macOS 15+ 또는 Ubuntu 22.04+
  • Docker 버전: Docker Engine/Desktop 25.x 이상
  • 도구: jq 1.6+ (로그 JSON 검사 용도)
  • 실행 순서:
    1. 샘플 앱(Dockerfile, app.py) 작성
    2. 이미지 빌드 후 stdout/stderr 로그 확인
    3. Compose 로그 회전 정책 적용
    4. 로그 스키마/민감정보 검사
    5. 필드 누락/레벨 정책 점검
  • 재현 체크:
    • 로그가 컨테이너 내부 파일이 아니라 stdout/stderr로 출력되는가?
    • 필수 JSON 필드가 누락되지 않는가?
    • 운영 로그 레벨 정책(INFO 이상)이 적용되는가?
    • 민감정보 키워드가 로그에 남지 않는가?
    • 로그 회전 설정이 존재하고 디스크 사용량이 통제되는가?