로그는 많은데 못 찾는다 — 구조화 로그와 trace_id
로그는 초당 수천 줄씩 쌓이는데, 장애 때 특정 사용자의 요청 하나를 못 찾아 몇 시간을 헤맸어요. 남기는 것과 찾을 수 있는 건 달라요. 나중에 찾을 수 있게 로그를 미리 설계하는 법을 정리했어요.
장애가 나서 로그를 뒤졌어요. 분명히 그 시점에 무슨 일이 있었을 텐데, 원하는 줄을 못 찾겠어요.
로그는 충분히 많아요. 초당 수천 줄씩 쌓이고 있고요. 그런데 그 안에서 특정 사용자의 특정 요청을 따라가려니 방법이 없어요. 형식이 제각각이고, 요청을 잇는 식별자도 없고요.
검색해도 관련 없는 줄이 수만 개 나와요. 조건을 좁히려 해도 걸 만한 필드가 없어요. 전부 자유롭게 쓴 문장이거든요.
결국 시각으로 대충 잘라 눈으로 훑었어요. 몇 시간이 걸렸고, 그러고도 확실한 답은 못 얻었어요.
로그를 남기는 것과 찾을 수 있는 것은 다릅니다. 그리고 대부분의 팀이 앞의 것만 하고 뒤의 것은 안 합니다.
왜 그런가
로그가 검색 가능하려면 몇 가지 조건이 필요해요. 그냥 많이 남기는 것으로는 안 돼요.
첫째, 형식이 일정해야 해요. 사람이 읽기 좋게 자유롭게 쓴 문장은 기계가 못 다뤄요. 필드가 구분된 구조화된 형식이어야 특정 값으로 걸러 낼 수 있어요.
둘째, 이어 볼 식별자가 있어야 해요. 요청 하나가 여러 서비스에 걸쳐 수십 줄의 로그를 남기는데, 그걸 묶는 키가 없으면 흩어진 줄로만 남아요. 앞 편에서 본 추적 식별자가 여기서도 쓰여요.
셋째, 맥락이 담겨야 해요. "실패했습니다"만 있으면 아무 도움이 안 돼요. 무엇이, 어떤 값으로, 어떤 상태에서 실패했는지가 있어야 조사가 됩니다.
넷째, 양이 감당 가능해야 해요. 너무 많으면 저장 비용도 문제지만 검색도 느려져요. 그리고 앞 편에서 본 대로 정말 중요한 줄이 소음에 묻혀요.
그리고 흔한 실수가 있어요. 디버깅할 때 필요한 정보를 그때 가서 추가하려는 것. 장애는 이미 지나갔고 로그는 그때 남긴 것만 있어요. 나중에 추가해도 그 장애는 못 봐요.
그래서 로그는 "무슨 일이 일어날지" 모르는 상태에서 미리 설계해야 하는 어려움이 있습니다.
코드로 보면
구조화하고, 이어 붙이고, 맥락을 담는다:
log.info('주문 처리 실패')
무엇이? 누구 것이? 왜?
log.error('order_failed', {
trace_id: ctx.traceId, // 이어 볼 키
order_id: order.id,
user_id: user.id,
reason: 'insufficient_stock',
requested: 3, available: 1, // 판단 근거가 된 값
})
· 요청 단위 공통 필드는 자동으로 붙인다
미들웨어에서 trace_id · user_id · 엔드포인트를 문맥에 넣고
로거가 모든 줄에 자동 부착
· 개인정보는 넣지 않는다
이메일 · 전화번호 · 주소 · 토큰 · 카드번호
식별자만 남기고 값은 필요할 때 DB 에서 본다✕ 문제 지점 · ✓ 해법 · 그대로 돌려 보기 전에 자기 환경에 맞게 고쳐 주세요.
실전에서는
실무에서 효과가 큰 것들을 순서대로 보면 이래요.
구조화가 첫째예요. 문자열이 아니라 필드로 남기면 그때부터 걸러 내기가 됩니다. "이 사용자의 이 엔드포인트에서 5xx 인 것"을 조건으로 찾을 수 있어요.
요청 식별자를 모든 줄에 자동으로 붙이는 게 둘째예요. 미들웨어에서 문맥에 넣고 로거가 자동 부착하면, 개발자가 매번 신경 쓸 필요가 없어요. 그리고 이 식별자가 앞 편의 추적과 같으면 둘을 오갈 수 있어요.
맥락 값을 담는 게 셋째예요. 실패했다면 무엇을 근거로 그렇게 판단했는지 그 값을 남기세요. "재고 부족"보다 "요청 3, 가용 1"이 훨씬 유용해요. 이게 없으면 로그를 봐도 재현이 안 됩니다.
수준을 의미대로 쓰는 게 넷째예요. 앞 편에서 본 것처럼 전부 경고면 구별이 안 돼요. 그리고 정상 동작을 정보 수준으로 남기되, 양을 감당할 수 있는지 봐야 해요.
여기서 균형이 필요해요. 너무 적으면 조사가 안 되고, 너무 많으면 비용과 소음이 문제예요. 실용적인 기준은 "이 로그로 그 장애를 재현하거나 설명할 수 있는가"예요. 안 되면 부족한 거고, 조사에 안 쓰이는 줄이 대부분이면 많은 거예요.
그리고 개인정보를 절대 넣지 마세요. 로그는 여러 시스템에 복제되고 오래 보관되고 여러 사람이 봐요. 식별자만 남기고 실제 값은 필요할 때 원본에서 보는 게 원칙입니다.
마지막으로 보관 기간을 정하세요. 앞 편의 비용 문제와 이어지는데, 최근 것만 상세히 두고 오래된 건 요약하거나 싼 저장소로 옮기면 됩니다.
처방
- 로그를 구조화하세요. 문자열이 아니라 필드로 남겨야 걸러 낼 수 있습니다.
- 요청 식별자를 모든 줄에 자동으로 붙이세요. 미들웨어에서 처리하면 개발자가 신경 쓸 필요가 없습니다.
- 그 식별자를 추적과 공유하세요. 로그와 추적을 오갈 수 있어야 합니다.
- 판단 근거가 된 값을 담으세요. "실패"보다 "요청 3, 가용 1"이 조사에 쓰입니다.
- 수준을 의미대로 쓰세요. 전부 경고면 구별이 안 됩니다.
- 개인정보를 넣지 마세요. 식별자만 남기고 값은 원본에서 보세요.
- 보관 기간을 정하세요. 최근 것만 상세히, 오래된 건 요약하거나 싼 저장소로요.
- 장애 회고에서 "이 로그로 설명이 되는가"를 물으세요. 안 되면 무엇이 빠졌는지가 다음 개선입니다.
- 필드 이름 규칙을 조직 안에서 통일하세요. user_id 와 userId 가 섞여 있으면 걸러 내기가 안 됩니다.
- 로깅을 비동기로 하세요. 동기로 쓰면 그 시간이 요청 응답 시간에 그대로 더해집니다.
지금 확인해 보세요
찾을 수 있는 로그인가:
· 실제로 찾아 본다 — 이게 유일한 검증이다
특정 사용자의 특정 요청을 처음부터 끝까지 따라가 본다
못 하면 식별자가 없거나 형식이 제각각인 것이다
· 필드 이름이 서비스마다 같은가
user_id / userId / uid 가 섞여 있으면 필터가 안 된다
· 로그 유실 지표
수집기가 버린 양을 본다
장애 중에 로그량이 폭증하며 그때 가장 많이 유실된다
· 개인정보가 섞여 있지 않은지
이메일·전화·토큰·카드번호 패턴을 정기적으로 검사
· 로깅이 동기인가 비동기인가
동기면 요청 시간에 그대로 더해진다핵심 한 줄많이 남기는 것과 찾을 수 있는 것은 다르다.