브라우저-백엔드 트레이싱
Sophonz Browser SDK가 시작한 트레이스를 Python 백엔드와 PostgreSQL까지 잇기 — tracePropagationTargets, traceparent·tracestate·baggage를 위한 CORS, 서버 스팬의 session.id, 연결 확인 방법.
사용자의 동작 하나를 트레이스 하나로 따라갈 수 있습니다: 브라우저의 fetch 스팬, 그것이 일으킨 Python 서버 스팬, 데이터베이스 쿼리, 그리고 PostgreSQL이 그 쿼리에 대해 기록한 스팬까지. 네트워크를 건너 이를 전달하는 헤더는 두 개입니다. traceparent는 서버 스팬을 브라우저 스팬에 연결하고, baggage는 브라우저의 session.id를 실어 보내며 SDK가 이를 모든 서버 스팬에 붙입니다. 어느 단계가 빠져도 실패는 드러나지 않습니다. 요청은 성공하고, 서버는 그냥 새 트레이스를 시작합니다.
경로
| 단계 | 위치 | 성립해야 하는 조건 |
|---|---|---|
| 1 | Browser SDK | 요청 URL이 tracePropagationTargets와 일치해 traceparent와 baggage가 추가됩니다. |
| 2 | 브라우저 | CORS 사전 요청(preflight) 응답이 traceparent, tracestate, baggage를 허용합니다. |
| 3 | Python 서버 | tracecontext와 baggage 전파기가 켜져 있어(기본값) 서버 스팬이 브라우저의 트레이스를 이어받습니다. |
| 4 | Python SDK | baggage가 모든 스팬에 복사되고, session.id는 자체 속성으로 승격됩니다. |
| 5 | 데이터베이스 드라이버 | SQLCommenter가 각 SQL 문에 traceparent를 덧붙입니다. |
| 6 | PostgreSQL | pg_tracing이 쿼리 스팬 아래에 서버 측 스팬을 기록합니다. |
5단계와 6단계는 데이터베이스 트레이싱에서 다룹니다. 이 페이지는 나머지를 다룹니다.
1. 브라우저: 헤더를 보낼 곳 정하기
Browser SDK는 URL이 tracePropagationTargets와 일치하는 요청에만 헤더를 추가합니다.
SophonzSDK.init({
collectorUrl: '{{SOPHONZ_TRACES_COLLECTOR_URL}}',
appName: 'shop-web',
appKey: '{{YOUR_APP_KEY}}',
project: 'shop',
tracePropagationTargets: ['api.example.com'],
});- 문자열은 호스트입니다. 스킴, 포트, 경로는 무시하고 URL의 시작부터 호스트를 비교하므로,
api.example.com은https://api.example.com/v1/orders와 일치하지만https://evil.example/?q=api.example.com과는 일치하지 않습니다. - 경로로 제한하려면
/^https:\/\/example\.com\/api\//같은 정규식을 쓰세요. - 빈 목록이거나 옵션이 없으면 모든 오리진으로 헤더를 보냅니다.
자신의 API만 나열하세요. baggage 헤더는 한 사람의 방문을 식별하는 세션 ID를 담고 있으며, traceparent를 받는 오리진에 정확히 똑같이 전달됩니다.
fetch와 XHR 계측은 다음을 보냅니다.
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
baggage: session.id=8f2c1d0e6b7a4c3f9e5d2a1b0c9d8e7f트레이스에 tracestate가 있으면 그것도 보냅니다.
2. 서버: CORS에서 헤더 허용하기
traceparent와 baggage는 CORS 안전 목록에 없는 헤더이므로, 이를 가진 교차 오리진 요청은 사전 요청을 거칩니다. 사전 요청 응답이 이 헤더를 허용하지 않으면 브라우저는 트레이싱뿐 아니라 요청 자체를 막습니다. API에 이미 필요한 헤더와 함께 세 헤더를 모두 허용하세요.
pip install flask-corsfrom flask_cors import CORS
CORS(
app,
origins=["https://shop.example.com"],
supports_credentials=True,
allow_headers=["content-type", "authorization", "traceparent", "tracestate", "baggage"],
)사전 요청인 OPTIONS 요청 자체에는 traceparent가 없습니다. 브라우저는 사전 요청에 사용자 지정 헤더를 보내지 않습니다. 그 서버 스팬은 스팬 하나짜리 별도 트레이스가 되며, 이는 정상입니다.
NOTE — 모든 오리진 허용
Sophonz 샘플 서버는 공개 테스트 페이지가 호출하므로 모든 오리진과 요청된 모든 헤더를 그대로 허용합니다. 운영 API에서는 호출하는 오리진을 나열하세요.
3. 서버: 트레이스 이어받기
설정할 것은 없습니다. OpenTelemetry의 기본 전파기는 tracecontext,baggage이고, Flask, FastAPI, Django의 서버 계측이 요청마다 두 헤더를 읽습니다. 서버 스팬은 브라우저 스팬을 부모로 가지며, 그 컨텍스트에 baggage가 담깁니다.
OTEL_PROPAGATORS를 설정한다면 두 항목을 모두 유지하세요.
export OTEL_PROPAGATORS=tracecontext,baggagetracecontext만 두면 트레이스는 이어지지만 session.id가 더 이상 서버 스팬에 도달하지 않습니다.
기본 샘플러 parentbased_always_on은 traceparent의 sampled 플래그가 켜진 트레이스를 모두 유지하므로, 브라우저가 기록한 트레이스가 서버에서 끊기지 않습니다. 다른 OTEL_TRACES_SAMPLER를 쓰더라도 parentbased_* 샘플러라면 서버에서 시작하는 트레이스에만 적용됩니다.
4. 모든 스팬의 session.id
SDK는 스팬마다 모든 baggage 항목을 baggage.<key>로, promote_baggage_keys(기본값 ["session.id"])의 항목은 원래 이름으로 복사합니다. 위 요청이라면 서버 스팬, 핸들러에서 시작한 스팬, 각 데이터베이스 스팬이 모두 다음을 가집니다.
| 속성 | 값 |
|---|---|
session.id | 8f2c1d0e6b7a4c3f9e5d2a1b0c9d8e7f |
baggage.session.id | 8f2c1d0e6b7a4c3f9e5d2a1b0c9d8e7f |
브라우저 텔레메트리에서 session.id는 리소스 속성이고, 서버 스팬에서는 같은 이름의 스팬 속성입니다. session.id로 조회하면 세션의 양쪽이 모두 나옵니다.
프레임워크 헬퍼는 요청의 모든 스팬에 http.client.ip와 http.user_agent도 붙입니다. 리버스 프록시 뒤에서는 피어 주소가 프록시의 주소이므로, Flask는 ProxyFix, uvicorn은 --proxy-headers, Django는 REMOTE_ADDR을 설정하는 미들웨어로 X-Forwarded-For를 신뢰하세요. 프레임워크 가이드를 참고하세요.
서비스 네임스페이스 맞추기
대시보드는 service.namespace로 범위를 나눕니다. Browser SDK는 project 옵션을 service.namespace로 보냅니다. 백엔드에도 같은 값을 주지 않으면, 트레이스가 제대로 연결되어도 네임스페이스로 필터링한 화면에서 백엔드 스팬이 빠집니다.
export SOPHONZ_SERVICE_NAMESPACE=shop
# 또는 샘플 서버처럼
export OTEL_RESOURCE_ATTRIBUTES=service.namespace=shop확인하기
먼저 임의로 만든 헤더로 서버만 단독으로 테스트하세요.
curl -i https://api.example.com/api/users \
-H "traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" \
-H "baggage: session.id=test-session-1"서버를 SOPHONZ_DEBUG_PAYLOAD=true로 실행합니다. 콘솔 출력의 서버 스팬은 다음과 같아야 합니다.
- 트레이스 ID
4bf92f3577b34da6a3ce929d0e0e4736 - 부모 스팬 ID
00f067aa0ba902b7 session.id가test-session-1이고, 데이터베이스 스팬에도 있음
여기까지 되면 서버 쪽은 정상입니다. 그다음 브라우저를 확인하세요. 개발자 도구의 네트워크 탭에서 API 요청에 traceparent와 baggage가 있어야 하고, 사전 요청은 성공했어야 합니다. curl 요청과 달리 브라우저 요청에서만 서버 스팬이 새 트레이스를 시작한다면 브라우저가 헤더를 보내지 않은 것입니다. tracePropagationTargets와 CORS 응답을 확인하세요.
명령줄에서 사전 요청을 확인하려면 다음을 실행합니다.
curl -i -X OPTIONS https://api.example.com/api/users \
-H "Origin: https://shop.example.com" \
-H "Access-Control-Request-Method: GET" \
-H "Access-Control-Request-Headers: traceparent,tracestate,baggage"응답에는 세 헤더를 모두 나열한 Access-Control-Allow-Headers와, 오리진이 담긴 Access-Control-Allow-Origin이 있어야 합니다.
다음 단계
- 데이터베이스 트레이싱 — 마지막 두 단계, PostgreSQL까지.
- 웹 계측 — 브라우저가 요청마다 기록하는 것.
- 배포 — 위 환경 변수를 컨테이너에서 설정하기.
TIP — 이제 시작입니다
이제 앱을 배포하세요. Sophonz가 실사용자가 겪는 문제를 포착하고, 당신의 앱은 스스로 진화합니다.