API 오류를 확인할 때 가장 먼저 찾은 건 로그 한 줄이었다. 요청 시각과 사용자에게 보인 오류를 기준으로 파일을 열고, 비슷한 문자열을 검색했다. 서비스가 하나라면 이것만으로도 충분한 경우가 많았다.
하지만 요청이 client API를 지나 gateway와 외부 API까지 이어지면 이야기가 달라졌다. 각 서비스의 시각이 조금씩 다르고, 같은 엔드포인트가 짧은 시간에 여러 번 호출된다. “아마 이 줄일 것”이라는 판단이 늘어날수록 원인을 확정하기 어려웠다.
OpenTelemetry의 분산 추적을 공부하면서 로그를 없애는 기술이 아니라, 흩어진 로그와 작업을 하나의 요청 관계로 묶는 방법이라고 이해하게 됐다.
문자열 하나에서 trace context로
가장 단순한 시도는 요청 ID를 헤더로 전달하는 것이다.
POST /orders HTTP/1.1
Host: api.example.com
X-Request-Id: 7f4c-example
모든 서비스가 이 값을 로그에 남기면 검색이 쉬워진다. 다만 서비스마다 새 하위 작업이 생기거나 병렬 호출이 일어날 때 부모와 자식 관계까지 표현하기는 어렵다.
W3C Trace Context의 traceparent는 trace ID, 현재 span ID, 플래그를 표준 형식으로 전달한다.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
같은 trace 안에서 각 서비스는 자신의 span을 만들고 다음 호출에 새 부모 정보를 전달한다.
trace: 주문 요청
└─ span: client-api POST /orders
├─ span: database INSERT order
└─ span: gateway POST /provider/orders
└─ span: external-api request
코드에서 문맥을 이어 주기
.NET 예제를 단순화하면 ActivitySource로 작업 구간을 만들 수 있다.
private static readonly ActivitySource Source =
new("Example.OrderApi");
public async Task<OrderResult> CreateOrder(CreateOrder command)
{
using var activity = Source.StartActivity("order.create");
activity?.SetTag("order.type", command.Type);
var result = await gateway.CreateOrder(command);
activity?.SetTag("order.result", result.State);
return result;
}
계측된 HTTP 클라이언트를 사용하면 현재 trace context가 다음 요청 헤더로 전달된다. 직접 헤더를 복사하는 코드보다 표준 계측을 쓰는 편이 누락을 줄인다.
로그에도 trace와 span 식별자를 같이 넣으면 상세 문맥을 기존 로그에서 읽고 전체 흐름은 추적 화면에서 볼 수 있다.
{
"level": "warning",
"event": "gateway_timeout",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"elapsed_ms": 3002
}
많이 남기는 것과 잘 관측하는 것
처음에는 span마다 요청 본문과 응답 본문을 모두 붙이면 디버깅이 편할 것 같았다. 하지만 로그와 trace는 오래 저장되고 여러 사람이 볼 수 있다. 토큰, 이름, 계좌 정보 같은 값이 들어가면 관측 도구가 새로운 정보 노출 경로가 된다.
| 남기기 좋은 값 | 그대로 남기지 않을 값 |
|---|---|
| route template | 전체 URL의 민감 query |
| 상태 코드 | 인증 헤더와 쿠키 |
| 처리 시간 | 요청·응답 원문 전체 |
| 내부 상태 enum | 이름·연락처·계좌 값 |
| 일반화한 오류 종류 | 키와 토큰 |
고유한 업무 식별자가 필요하다면 원문 대신 내부 참조값이나 일방향 변환값을 검토해야 한다. 샘플링도 중요하다. 성공 요청을 전부 저장하면 비용이 커지고, 오류만 저장하면 오류 직전의 정상 흐름을 놓칠 수 있다. 트래픽과 위험에 맞는 규칙이 필요하다.
로그를 볼 때도 신선도부터
분산 추적 도입 여부와 별개로, 로그 파일 자체를 확인하는 순서도 정리했다.
호스트와 현재 시각 확인
→ 실제 프로세스와 작업 경로 확인
→ 로그 파일 수정 시각 확인
→ trace ID 또는 정확한 요청 값 검색
→ 앞뒤 span과 오류 문맥 확인
오래된 파일에서 검색 결과가 없다는 사실은 현재 요청이 오지 않았다는 증거가 아니다. 어느 프로세스가 지금 그 파일을 쓰는지 먼저 확인해야 “없음”도 의미가 생긴다.
분산 추적은 예쁜 타임라인을 만드는 일이 아니라 서비스 사이의 인과관계를 보존하는 일에 가까웠다. 로그 한 줄은 여전히 필요하다. 다만 그 한 줄이 어느 요청의 어느 단계인지 연결될 때, 추측이 증거로 바뀐다.
도입 순서도 작게 잡는 편이 현실적이었다. 먼저 입구 API에서 trace context를 받아 로그에 trace ID를 넣고, 다음으로 중요한 HTTP 호출 한두 곳에 전파한다. 그 뒤 DB나 외부 호출 span을 추가하고 sampling과 보존 기간을 조정한다. 처음부터 모든 함수에 span을 만들면 중요한 경로가 잡음에 묻힌다. 사용자 증상에서 출발해 자주 끊기는 경계를 먼저 연결하는 방식이 더 유용했다.
추적의 목적은 수집량이 아니라 답을 찾는 시간이었다.