← 모든 글

Logs부터, 그 다음은 나중에 — 관찰 가능성 우선순위 결정기

logs·metrics·traces 셋 다 못 깔 때, 실제 장애 경험으로 정한 투자 순서

셋 다 갖추면 좋다. 하지만 현실은 런웨이 6주짜리 스쿼드가 Datadog 청구서 보고 기절하는 순간 온다.

그때 뭘 먼저 살려야 하는지, 두 번 태워 보고 나서야 답이 정해졌다.

장애 때 가장 먼저 찾는 게 뭔지 기억하라

올해 2월, 결제 콜백이 간헐적으로 씹히는 사건이 있었다. 에러율은 0.3%였는데 금액 기준으로는 하루 약 170만 원이 증발하는 수준. metrics는 있었다 — payment_callback_error_total 카운터, 예쁘게 올라가는 그래프. 문제는 올라가는지를 알 수 없었다는 거다.

카운터가 올라간다는 사실은 알아도, 어떤 merchant_id에서, 어떤 응답 코드로, 어떤 payload가 들어왔을 때 터지는지는 metrics로 못 잡는다. 결국 CloudWatch Insights에서 로그 5만 줄을 JSON 필터로 뒤져서 원인을 찾았다. amount 필드가 정수 대신 문자열로 오는 특정 PG사 케이스였다.

그날 이후 내 우선순위 1번은 구조화 로그다. 이유 없이 정해진 게 아니다.

Logs: 디버깅의 원자재

로그는 가장 원시적인 관찰 수단이고, 그래서 가장 범용적이다. metrics와 traces가 없어도 로그만 있으면 사건의 맥락을 재구성할 수 있다. 반대는 성립하지 않는다.

  • 구조화 필드를 처음부터 강제한다{"level":"error","trace_id":"...","user_id":"...","latency_ms":320} 형태로. 나중에 grep으로 뒤지는 비구조화 로그는 팀원이 5명 넘어가면 무기가 아니라 고문이 된다. 결제 사건 때도 JSON 로그가 아니었으면 Insights 필터 자체를 못 썼다.
  • retention은 최소 30일 — 이상 징후는 보통 7~14일 패턴으로 온다. 7일 보관이면 바로 전 주기를 못 보는 경우가 생긴다. 실제로 배치 잡 오류가 격주 월요일마다 터지는데 7일 보관 탓에 첫 발생 로그를 날린 적 있다. 그 이후로 30일이 기본값이다.
  • 샘플링은 에러에 적용하지 않는다 — 비용 절감으로 10% 샘플링 켰다가 0.3% 에러율 케이스에서 에러 로그가 통계적으로 거의 안 잡히는 상황을 겪었다. 에러는 100%, 정상 트래픽만 샘플링하는 게 맞다.

Metrics: 이상 신호를 먼저 알아채는 용도

로그가 ‘무슨 일이 있었나’를 설명한다면, metrics는 ‘뭔가 이상해지기 시작한 시점’을 가장 싸게 포착한다. 알람 임계값 하나로 새벽 3시에 안 일어나도 되는 날이 생긴다.

  • SLI 3개부터 시작한다 — 에러율, 레이턴시 p99, 포화도(큐 깊이 또는 CPU). 이 세 개가 없으면 서비스가 죽어가는 걸 사용자 CS 인입으로 먼저 알게 된다. 실제로 큐 포화 알람이 없던 시절, 외부 파트너가 “API 응답이 이상하다”고 먼저 연락을 줬다. 부끄러웠다.
  • 카디널리티 폭탄 주의user_id를 레이블로 넣었다가 Prometheus가 OOM 나서 내려간 적 있다. 레이블은 10개 미만 고정값 기준으로. 가변 식별자는 무조건 로그에 넣는다.
  • 알람은 만들고 끝이 아니다 — 처음 3개월은 알람 10개 중 7개가 노이즈였다. 매주 수요일 오전에 알람 FP 리뷰를 30분씩 잡고 임계값을 조정했더니 3개월 뒤 노이즈 비율이 2/10으로 줄었다. 알람은 한 번 세팅으로 끝나는 게 아니다.

Traces: 세 번째가 맞는 이유

분산 추적은 강력하다. 근데 로그와 metrics가 없는 상태에서 traces부터 붙이면 설치 비용 대비 얻는 게 생각보다 적다.

트레이싱이 진가를 발휘하는 건 “어느 서비스에서 느린지는 아는데 왜 느린지 서비스 경계 너머로 추적이 안 될 때”다. 마이크로서비스 5개 이하 규모에서는 로그에 trace_id 찍어두고 Kibana나 CloudWatch에서 필터링하는 게 OpenTelemetry 풀 스택 올리는 것보다 훨씬 싸다.

  • trace_id는 공짜다 — 분산 추적 인프라 없이도, 요청 진입점에서 UUID 하나 생성해서 모든 로그에 심으면 된다. X-Request-ID 헤더로 전파하고, 로그에 찍는 것만으로 콜 체인 재구성이 된다. 실제로 이 방식으로 6개월 버텼다.
  • Jaeger/Tempo는 팀이 traces를 소비하는 문화가 생겼을 때 — 도구를 먼저 깔고 문화를 기대하면 대개 아무도 안 본다. 우리 팀은 PR 리뷰에 trace_id 링크를 첨부하는 관행을 먼저 만들고, 그다음에 Tempo를 붙였다. 순서가 중요했다.

예산·인력이 쪼들릴 때 실제 투자 순서

이건 이론이 아니라 지금 운영하면서 실제로 밟은 순서다.

1단계: 구조화 로그 + 30일 보관. 비용은 S3 + CloudWatch Logs 기준 월 3~5만 원 수준부터 시작된다. 이것만 있어도 대부분의 장애 원인을 찾을 수 있다.

2단계: SLI 3개 알람. Prometheus + Alertmanager 셀프호스팅이면 EC2 t3.small 하나로 돌아간다. 관리 비용이 아깝다면 CloudWatch Metrics 알람 3개만 켜도 된다. 완벽하지 않아도 없는 것보다 압도적으로 낫다.

3단계: 구조화 로그에 trace_id 심기. 추가 인프라 없이 코드 한 줄이다. 이 세 단계까지가 ‘최소 생존 관찰 가능성’이고, 이후 Tempo나 Datadog APM은 그다음 문제다.

다음 한 가지

이번 분기 안에, 아직 구조화 로그가 없는 배치 파이프라인 2개에 {"job":"...","run_id":"...","status":"...","elapsed_ms":N} 포맷을 강제하는 PR을 올린다 — 격주 배치 오류를 또 로그 없이 추적하는 일이 반복되고 있으니까.


🛒 이 글과 어울리는 추천 상품

위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.