~10 min · production, observability, correlation-id, tracing
Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"사용자가 오류를 신고했을 때 상관관계 ID가 없으면 여러 서비스의 로그 시각을 일일이 맞춰야 해. ID가 있으면 한 번의 검색으로 요청의 전체 경로를 따라갈 수 있지. 운영에서는 이 차이가 아주 커."
상관관계 ID가 해결하는 문제
현대 애플리케이션에서는 사용자 요청 하나가 게이트웨이, 애플리케이션 서버, 인증 서비스, 데이터베이스, 캐시, 외부 서비스 제공자(Stripe, OpenAI), 메시지 큐와 워커를 차례로 거칠 수 있어. 각 구성 요소는 저마다 로그를 남겨. 공유 식별자가 없으면 서비스 B의 기록이 서비스 A의 어느 요청에서 이어졌는지 알아내려고 시각과 정황을 대조해야 하지. 느리고 실수하기 쉬우며, 서비스 수가 늘수록 더 어려워져.
해법은 요청 경계에서 고유 ID를 정하고, 허용된 모든 하위 호출과 비동기 작업에 전파하며, 구조화 로그에 같은 필드로 기록하는 거야. 그러면 ID 하나로 요청의 시작부터 끝까지 관련 기록을 모을 수 있어. 단, ID는 연결 고리일 뿐이므로 정확한 시각, 서비스 이름, 상태, 지연 시간 같은 맥락도 함께 남겨야 해.
패턴의 세 층
생성. 요청을 처음 받는 신뢰 경계, 보통 게이트웨이나 가장 바깥쪽 미들웨어에서 UUID 같은 고유 값을 만들어 상관관계 ID로 사용해. 클라이언트가 X-Request-ID를 보냈다면 길이·문자 집합·형식을 검증하고, 신뢰 정책에 따라 재사용하거나 새 내부 ID를 발급해. 검증하지 않은 값을 그대로 로그에 넣으면 로그 위조나 과도한 저장 공간 사용으로 이어질 수 있어.
전파. 하위 서비스로 보내는 HTTP 요청에는 합의한 헤더로 ID를 전달해. 애플리케이션 전용 상관관계에는 X-Request-ID나 X-Correlation-Id를 쓸 수 있고, 분산 추적에는 W3C 표준 traceparent를 사용해. 백그라운드 작업은 메시지 메타데이터나 페이로드에 필요한 맥락을 담아 워커까지 이어 줘.
로그 기록. 모든 관련 로그에 ID를 동일한 구조화 필드로 넣고, 응답에도 공개 가능한 요청 ID를 돌려줘. 그러면 지원 담당자가 사용자가 전달한 ID로 기록을 찾을 수 있어. 상관관계 ID와 trace ID를 따로 쓴다면 둘을 같은 로그에 기록해 서로 오갈 수 있게 만들어야 해.
W3C 트레이스 맥락 — 분산 추적의 공통 언어
W3C Trace Context 권고안은 분산 추적 맥락을 전달하는 표준 헤더 두 개를 정의해:
traceparent — 00-{trace_id}-{span_id}-{flags} 형식이야. trace_id는 전체 트레이스를 묶고, span_id는 현재 호출의 부모 스팬을 가리키며, flags에는 추적 데이터의 샘플링 여부 같은 정보가 들어가.
tracestate — 여러 추적 시스템이 공급자별 상태를 상호 운용 가능한 방식으로 전달하는 헤더야.
OpenTelemetry, Honeycomb, Datadog, Jaeger, AWS X-Ray, Google Cloud Trace 같은 도구가 이 표준과 연동돼. trace ID를 로그 상관관계 키로 활용할 수 있지만, 애플리케이션의 request ID와 항상 같은 값인 것은 아니야. 아래 동결 예제는 형식을 설명하기 위한 최소 코드라서 입력 검증과 헤더 길이 제한이 없고, 새 요청 분기에서 반환하는 trace_id와 새로 만든 traceparent 안의 trace ID가 서로 달라질 수 있어. 운영 코드에서는 검증된 SDK를 사용하거나 하나의 trace ID로 두 값을 함께 만들어야 해.
여러 서비스에 걸친 요청을 운영한다면 상관관계 맥락은 사실상 필수야. 신뢰 경계에서 안전하게 생성하거나 검증하고, 필요한 경로에 빠짐없이 전파하며, 구조화 로그와 트레이스에 함께 남겨. 구현 비용은 작지만 장애 조사 시간을 크게 줄일 수 있어.
ID를 넘어 — 관측 가능성의 세 축
상관관계 ID는 더 넓은 관측 가능성 체계로 들어가는 출발점이야:
로그 — 개별 사건의 상세 기록이야. 요청 ID나 trace ID를 구조화 필드로 남겨.
메트릭 — 요청률, 지연 시간 P50/P99, 엔드포인트별 오류율처럼 집계된 수치야.
트레이스 — 한 요청이 여러 서비스를 통과한 경로와 각 작업의 관계를 스팬으로 표현해.
세 축을 연결하면 메트릭의 이상 징후에서 관련 트레이스로, 다시 특정 요청의 로그로 좁혀 갈 수 있어. 막연한 장애 신호를 재현 가능한 조사 경로로 바꾸는 연결 고리인 셈이지. 다만 메트릭에는 요청별 ID처럼 카디널리티가 높은 값을 일반 레이블로 넣지 말고, exemplars나 트레이스 링크처럼 용도에 맞는 방식을 써야 해.
cwkPippa의 관측 가능성 구성
cwkPippa는 들어오는 HTTP 요청마다 미들웨어에서 request_id(UUID)를 만들고 request.state.request_id에 저장한 뒤, 응답의 X-Request-ID 헤더와 관련 로그에 기록해. JSONL 세션 로그는 별도의 conversation_id를 기준으로 대화를 묶어. 대화 하나에는 여러 request_id가 생길 수 있으므로, 장애가 발생한 대화를 먼저 찾고 그 시점의 request_id를 추려 각 어댑터의 로그를 연결해 조사할 수 있어. 전용 분산 추적 인프라가 없어도 일관된 식별자와 구조화 로그만으로 상당한 진단 능력을 확보하는 방식이야.
Code
FastAPI 미들웨어: ID 생성·재사용, 로그 기록, 응답 헤더 반환·python
# FastAPI — correlation ID middleware
import uuid, logging
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
log = logging.getLogger(__name__)
class CorrelationIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# 있으면 client-공급 ID 재사용 (일부 client 가 client-driven tracing 함)
request_id = request.headers.get('x-request-id') or str(uuid.uuid4())
request.state.request_id = request_id
# 모든 log 줄에 첨부 (production 엔 structlog 나 contextvars)
log.info('request.start', extra={'request_id': request_id, 'path': request.url.path})
response = await call_next(request)
# Response 에 echo, client 가 봄
response.headers['X-Request-ID'] = request_id
log.info('request.end', extra={'request_id': request_id, 'status': response.status_code})
return response
app = FastAPI()
app.add_middleware(CorrelationIDMiddleware)
@app.get('/api/anything')
async def anything(request: Request):
rid = request.state.request_id
log.info('handler.work', extra={'request_id': rid})
return {'rid': rid}
전파: 모든 하위 서비스 호출에 ID 전달·python
# httpx 통해 downstream service 에 전파
import httpx
from fastapi import Request
async def call_upstream(request: Request, payload: dict):
rid = request.state.request_id
async with httpx.AsyncClient() as client:
resp = await client.post(
'https://upstream.example.com/process',
json=payload,
headers={'X-Request-ID': rid}, # downstream 전파
)
return resp.json()
# Upstream service 가 같은 X-Request-ID header 봄,
# 같은 ID 와 log, 자기 response 에 돌려줌 — loop 닫음.
W3C traceparent — OpenTelemetry 호환 추적 맥락·python
# W3C Trace Context — 표준 형식 (OpenTelemetry-호환)
import uuid
from fastapi import Request
def make_traceparent(trace_id: str | None = None, span_id: str | None = None) -> str:
trace_id = trace_id or uuid.uuid4().hex # 32 hex 글자
span_id = span_id or uuid.uuid4().hex[:16] # 16 hex 글자
return f'00-{trace_id}-{span_id}-01' # 00=version, 01=sampled
# 들어오는 traceparent 읽기 (있으면); 아니면 하나 생성
def get_trace_context(request: Request) -> dict:
incoming = request.headers.get('traceparent')
if incoming:
# parse: version-trace_id-span_id-flags
_, trace_id, _, _ = incoming.split('-')
return {'trace_id': trace_id, 'traceparent': make_traceparent(trace_id)}
return {'trace_id': uuid.uuid4().hex, 'traceparent': make_traceparent()}
FastAPI 애플리케이션에 상관관계 ID 미들웨어를 추가해 봐. 요청마다 UUID를 만들고 request.state에 저장한 뒤, 응답의 X-Request-ID 헤더와 모든 관련 로그에 기록해. 요청을 열 번 보내 각 요청의 ID는 서로 다르고, 한 요청 안에서는 로그와 응답 헤더의 ID가 일치하는지 확인해. 보너스로 httpx를 사용해 두 번째 FastAPI 서비스를 호출하고 X-Request-ID를 전달해 봐. 두 서비스의 로그에서 같은 사용자 요청이 같은 ID로 연결되는지 확인하면 돼.
Hint
BaseHTTPMiddleware로 개념을 간단히 구현할 수 있어. 운영 환경에서는 모든 로그 호출에 extra=를 반복하기보다 structlog나 contextvars를 이용해 요청 맥락을 주입하는 방식을 검토해. 두 서비스 예제에서는 같은 request_id가 양쪽 로그에 나타나는지뿐 아니라, 외부에서 들어온 ID를 검증하고 하위 호출에서 빠짐없이 전달하는지도 확인해.
Progress
Progress is local-only — sign in to sync across devices.