-
이코에코(Eco²) Observability #2: 로깅 정책 수립이코에코(Eco²)/Observability 2025. 12. 19. 02:20
개요
로깅 시스템을 구축했다면, 이제 무엇을, 어떻게, 얼마나 로깅할지 정책을 수립해야 합니다.
이 글에서는 빅테크 기업들의 로깅 베스트 프랙티스를 분석하고, 우리 프로젝트에 맞는 정책을 도출하는 과정을 공유합니다.
빅테크 로깅 베스트 프랙티스 분석
Google SRE
핵심 원칙:
- 로그는 이벤트 기록이 아닌 질문에 대한 답이어야 함
- 구조화된 로그로 쿼리 가능성 확보
- Trace ID로 분산 시스템 추적
실천 사항:
✅ 요청 ID, trace ID 필수 포함 ✅ 에러 발생 시 컨텍스트 (입력값, 상태) 기록 ✅ 비즈니스 메트릭과 로그 연계 ❌ 민감 정보 로깅 금지 ❌ 과도한 DEBUG 로그 금지Uber
핵심 원칙:
- High Cardinality 지원: 사용자 ID, 트랜잭션 ID로 검색
- Log Aggregation: 중앙 집중화된 로그 분석
- Cost-aware Logging: 로그 볼륨 = 비용
로그 레벨 가이드:
Level 사용 시점 예시 ERROR 즉각 대응 필요 DB 연결 실패, 외부 API 500 WARN 잠재적 문제 재시도 발생, Rate limit 근접 INFO 정상 비즈니스 이벤트 로그인 성공, 주문 완료 DEBUG 개발/디버깅 함수 진입, 변수 값 로그 볼륨 관리:
Development: DEBUG 허용 Staging: INFO + 일부 DEBUG Production: INFO 이상만
📐 CNCF 표준 분석
OpenTelemetry

로그 데이터 모델:
{ "timestamp": "2025-12-17T10:00:00.000Z", "severity_text": "INFO", "body": "User login successful", "resource": { "service.name": "auth-api", "service.version": "1.0.7" }, "attributes": { "user.id": "usr-123", "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736", "span_id": "00f067aa0ba902b7" } }Elastic Common Schema (ECS)
표준 필드 구조:
{ "@timestamp": "2025-12-17T10:00:00.000Z", "message": "User login successful", "log.level": "info", "log.logger": "auth.service", "service.name": "auth-api", "service.version": "1.0.7", "trace.id": "4bf92f3577b34da6a3ce929d0e0e4736", "span.id": "00f067aa0ba902b7", "error.type": "AuthenticationError", "error.message": "Invalid token", "error.stack_trace": "..." }장점:
- Elasticsearch/Kibana 최적화
- 400+ 표준 필드 정의
- 다양한 에코시스템 호환
Eco² 로깅 정책 v1.1
1. 로그 포맷 표준
ECS 기반 JSON 포맷 (현재 구현):
{ "@timestamp": "2025-12-17T09:15:30.123Z", "message": "Authorization allowed", "log.level": "info", "log.logger": "domains.auth.services.auth", "ecs.version": "8.11.0", "service.name": "auth-api", "service.version": "1.0.7", "service.environment": "dev", "trace.id": "4bf92f3577b34da6a3ce929d0e0e4736", "span.id": "00f067aa0ba902b7", "event.action": "authorization", "event.outcome": "success", "event.duration_ms": 12.5 }2. 필수 필드 정의 (현재 구현)

필드 타입 필수 구현 상태 @timestampISO 8601 ✅ ✅ Python/Go messagestring ✅ ✅ Python/Go log.levelstring ✅ ✅ Python/Go service.namestring ✅ ✅ Python/Go service.versionstring ✅ ✅ Python/Go trace.idstring ✅ ✅ Python (OTEL), Go (B3) span.idstring ✅ ✅ Python (OTEL), Go (B3) event.actionstring △ ✅ Go (ext-authz) event.outcomestring △ ✅ Go (ext-authz) 3. 로그 레벨 가이드라인
Level 사용 시점 프로덕션 활성화 현재 설정 DEBUG 상세 디버깅 ❌ dev만 INFO 정상 비즈니스 이벤트 ✅ 기본값 WARNING 잠재적 문제, 재시도 ✅ 활성화 ERROR 오류 발생 ✅ 활성화 CRITICAL 서비스 중단 수준 ✅ 활성화 4. 서비스별 로깅 구현
Python API (auth, character, chat, etc.)

구현 위치:
domains/{service}/core/logging.py# ECSJsonFormatter - OpenTelemetry trace.id 자동 주입 if HAS_OPENTELEMETRY: span = trace.get_current_span() ctx = span.get_span_context() if ctx.is_valid: log_obj["trace.id"] = format(ctx.trace_id, "032x") log_obj["span.id"] = format(ctx.span_id, "016x")Go ext-authz

구현 위치:
domains/ext-authz/internal/logging/logger.go// WithTrace - B3 trace context 주입 func (l *Logger) WithTrace(traceID, spanID string) *Logger { attrs := []any{slog.String("trace.id", traceID)} if spanID != "" { attrs = append(attrs, slog.String("span.id", spanID)) } return &Logger{Logger: l.With(attrs...)} }5. 민감 정보 처리 (구현 완료)
절대 로깅 금지:
- 비밀번호, 인증 토큰
- 주민번호, 전화번호
- 신용카드, 계좌번호
- 암호화 키
현재 마스킹 대상 (패턴 기반):
# domains/auth/core/constants.py SENSITIVE_FIELD_PATTERNS = frozenset({ "password", # 사용자 비밀번호 "secret", # jwt_secret_key, client_secret "token", # JWT, OAuth tokens "api_key", # External API keys "authorization", # HTTP Authorization header })마스킹 구현:
# Python 구현 MASK_PRESERVE_PREFIX = 4 # 앞 4자리 표시 MASK_PRESERVE_SUFFIX = 4 # 뒤 4자리 표시 def _mask_value(value: str) -> str: if len(value) <= 10: return "***REDACTED***" return f"{value[:4]}...{value[-4:]}" # eyJh...4fQk// Go 구현 (ext-authz) func MaskUserID(userID string) string { if len(userID) <= 4 { return "****" } return userID[:4] + "****" }6. 도메인별 로깅 스코프
auth-api:
이벤트 레벨 필수 extra 필드 구현 OAuth 로그인 시작 INFO provider,state✅ OAuth 콜백 성공 INFO provider,user_id✅ OAuth 콜백 실패 ERROR provider,error_type✅ 토큰 발급 INFO user_id,token_type✅ 토큰 검증 실패 WARNING reason✅ ext-authz:
이벤트 레벨 필수 extra 필드 구현 Authorization allowed INFO user.id,event.action,event.outcome✅ Authorization denied WARN event.reason,error.message✅ Public path allowed INFO url.path✅ 7. 성능 고려사항
로그 레벨 게이팅:
# ✅ 좋은 예: 레벨 체크 후 로깅 if logger.isEnabledFor(logging.DEBUG): logger.debug("Expensive data", extra={"result": expensive_fn()}) # ❌ 나쁜 예: 항상 연산 수행 logger.debug("Expensive data", extra={"result": expensive_fn()})환경별 로그 볼륨 (현재 측정):
환경 기본 레벨 실제 볼륨 (2025-12-17 기준) Development DEBUG ~420MB/day (1.1M docs) Production INFO 예상 ~200MB/day
Trace ID 전파 흐름

핵심 결정 사항과 근거
왜 ECS인가?

선택 이유:
- ECK Operator 생태계와 일치
- 우리는 ECK(Elastic Cloud on Kubernetes) Operator로 ES/Kibana를 관리 (ADR-001)
- ECS는 Elastic 생태계의 표준 스키마로, Kibana가 자동으로 필드를 인식
service.name,trace.id등이 사이드바에 바로 표시됨
- Phase 2 (EDA) 전환 대비
- 현재: Fluent Bit → ES 직접 전송
- EDA 도입 시: Fluent Bit → Kafka → Logstash → ES
- ECS 표준 필드를 사용하면 Logstash 파이프라인에서 추가 변환 불필요
- OpenTelemetry 호환성
- OTEL
trace_id→ ECStrace.id매핑 표준화 - Jaeger 트레이스와 Kibana 로그 간 상관관계 조회 가능
- OTEL
왜 JSON인가?

선택 이유:
- Fluent Bit 자동 파싱
Merge_Log: On설정으로 JSON 필드가 root에 자동 병합- 별도 grok 파서 없이 구조화된 로그 처리
- Kibana 쿼리 최적화
service.name: "auth-api"같은 필드 기반 검색- 일반 텍스트 로그 대비 10배 이상 빠른 검색
- EDA 전환 시 Logstash 처리 용이
- JSON → Logstash filter → JSON 파이프라인 단순화
- Saga trace correlation 등 복잡한 변환 지원
왜 도메인별 독립 구현인가?

선택 이유:
- 마이크로서비스 원칙 준수
- 각 서비스는 독립적으로 배포/확장 가능해야 함
- 공통 모듈 의존 시 버전 충돌, 배포 동기화 문제 발생
- 도메인별 커스터마이징
- auth:
provider,token_type,jti필드 - ext-authz:
event.action,event.outcome필드 - character:
character_id,exp_gained필드
- auth:
- 실용적 이유
- 코드 ~200줄 복사 vs 공통 모듈 관리 오버헤드
- 각 도메인별로 독립적으로 로깅 정책 조정 가능
왜 trace.id가 필수인가?

선택 이유:
- EDA 도입 시 로그 폭발 대비
현재: 1 요청 → 1~3개 로그 (일일 ~30,000 로그) EDA 후: 1 요청 → 10~30개 로그 (일일 ~300,000 로그)- Istio가 생성한 trace.id를 전체 흐름에서 공유
- Istio Ingress Gateway가 Source of Truth
- ext-authz, 앱 API, Kafka Consumer 모두 동일 trace.id 사용
- Kibana에서
trace.id: "xxx"검색 → 전체 요청 흐름 조회
- Jaeger ↔ Kibana 상관관계
- Jaeger에서 느린 trace 발견 → trace.id 복사
- Kibana에서 해당 trace.id의 상세 로그 조회
왜 민감 정보 마스킹인가?

선택 이유:
- OWASP 로깅 치트시트 준수
- 인증 정보, 세션 ID, 개인정보는 로그에 포함 금지
- 디버깅 목적이라도 마스킹 필수
- Elasticsearch 특성
- 로그가 검색 가능한 형태로 저장됨
- Kibana에서 누구나 조회 가능
- 잘못된 검색 쿼리로 민감 정보 노출 위험
- 자동 마스킹으로 개발자 실수 방지
- 패턴 기반 (
password,token,secret등) extra필드 전체에 재귀적으로 적용- 개발자가 실수로 토큰을 로깅해도 자동 마스킹
- 패턴 기반 (
왜 Python/Go 각각 구현인가?
구분 Python (FastAPI) Go (ext-authz) Trace 소스 OpenTelemetry SDK gRPC Metadata (B3) 이유 OTEL 자동 계측 ( opentelemetry-instrument)gRPC 서비스라 HTTP 헤더 접근 불가 로깅 stdlib loggingslog(Go 1.21+)이유 Python 표준, 대부분 라이브러리 호환 구조화 로깅 네이티브 지원 마스킹 재귀 dict 순회 개별 함수 이유 extra필드가 중첩 dict일 수 있음필드가 명확하고 단순 
📁 정책 문서 구조
docs/ ├── blogs/observability/ │ ├── 01-efk-stack-setup.md # 인프라 구축 │ ├── 02-logging-policy.md # 정책 수립 (이 문서) │ ├── 03-ecs-structured-logging.md # Python 구현 │ ├── 04-distributed-tracing.md # 분산 트레이싱 │ └── 12-log-trace-correlation.md # 로그-트레이스 상관관계 └── decisions/ └── ADR-001-logging-architecture.md # 아키텍처 결정 기록
Reference
'이코에코(Eco²) > Observability' 카테고리의 다른 글
이코에코(Eco²) Observability #5: 인덱스 전략 및 라이프사이클 관리 (0) 2025.12.19 이코에코(Eco²) Observability #4: 분산 트레이싱 통합 (0) 2025.12.19 이코에코(Eco²) Observability #3: 도메인별 ECS 구조화 로깅 (0) 2025.12.19 이코에코(Eco²) Observability #1: EFK 파이프라인 구축 (0) 2025.12.19 이코에코(Eco²) Observability #0: 로깅 파이프라인 아키텍처 설계 (0) 2025.12.18