LabHub
배우기 러닝패스 코스

OTCA — 오픈텔레메트리 인증 어소시에이트 · SDK 증거 추적: 사라진 스팬 찾기 · 이론

같은 trace ID인데 스팬은 왜 없을까

LabHub 에서 이어서 보기

한 줄 요약

스팬의 식별자가 있다는 것, 내용을 기록한다는 것, 익스포터로 전달했다는 것은
서로 다른 사실입니다. 세 사실을 나누어 확인하면 “계측 코드를 넣었는데 보이지
않는다”는 문제를 추측 대신 관측으로 좁힐 수 있습니다.

왜 이게 필요했나

주문 API에 트레이싱을 넣었습니다. 개발자는 로그에서 trace ID를 찾았고, 코드에는
start_span도 있습니다. 그런데 관측 화면에는 아무것도 없습니다. 이때 컬렉터를
재시작하는 것은 여러 원인 중 하나를 증거 없이 고르는 행동입니다. 실제로는
provider에 processor를 연결하지 않았거나, 샘플러가 스팬을 버렸거나, 스팬을 끝내지
않았을 수 있습니다. 익스포터까지 전달된 뒤 수신 서버가 거절한 경우도 있습니다.

이 모듈은 그 경계 중 SDK 안쪽을 다룹니다. 앞의 실습처럼 OTLP JSON을 손으로
만드는 것이 아니라, 공식 Python SDK가 생성한 스팬을 실제 처리기와 익스포터로
보냅니다. 익스포터 앞에서 보이는 정보와 실제 HTTP 수신 결과를 함께 살펴봅니다.
프로그램이 오류 없이 끝났다는 사실만으로 데이터가 전달됐다고 판정하지 않습니다.

어떻게 동작하나

API, SDK, provider, processor, exporter

애플리케이션의 계측 코드는 API를 통해 스팬을 만듭니다. SDK의 TracerProvider는
샘플러·자원·처리기 같은 실행 설정을 소유합니다. tracer를 얻었다고 해서 전송 경로가
자동으로 완성되지는 않습니다. 어떤 처리기가 끝난 스팬을 어느 익스포터로 넘길지도
정해야 합니다. 첫 실습에서는 이 연결 한 곳을 일부러 비워 두었습니다.

from opentelemetry.sdk.trace.export import SimpleSpanProcessorprovider.add_span_processor(SimpleSpanProcessor(exporter))tracer = provider.get_tracer("orders.instrumentation")

여기서 tracer의 이름은 계측 라이브러리의 범위를 구별하는 이름입니다. 서비스의
이름을 정하는 service.name과 같은 자리가 아닙니다. 두 값에 우연히 같은 문자열을
넣을 수는 있지만 역할까지 같아지지는 않습니다. 처음부터 값을 구분하면 여러
라이브러리가 한 서비스를 계측할 때도 출처를 읽을 수 있습니다.

기록과 샘플링의 세 가지 조합

다음은 이 실습의 공식 SDK와 기본 익스포트 처리기에서 관측할 결과입니다.
processor 관측은 스팬이 끝난 뒤의 호출을 말합니다.

| 결정 | 기록 중인가 | sampled 비트 | processor에 도착 | exporter에 도착 |
| --- | --- | --- | --- | --- |
| DROP | 아니오 | 꺼짐 | 아니오 | 아니오 |
| RECORD_ONLY | 예 | 꺼짐 | 예 | 아니오 |
| RECORD_AND_SAMPLE | 예 | 켜짐 | 예 | 예 |

RECORD_ONLY가 특히 중요합니다. “기록한다”는 이름 때문에 전송까지 된다고 읽기
쉽지만 실제 실험에서는 처리기의 종료 관측만 있고 익스포터의 스팬 목록은 비었습니다.
메모리 안에서 정보를 관찰하려는 목적과 원격으로 보낼 데이터의 양을 정하는 목적은
분리할 수 있습니다. 이 표를 외우는 데서 멈추지 말고, 학생 코드의 결정 하나를 바꿔
어느 목록이 늘고 줄어드는지 실행해 보세요.

샘플링으로 내용이 버려져도 유효한 스팬 컨텍스트가 있을 수 있습니다. 따라서 로그의
trace ID는 검색의 실마리이지 저장된 스팬이 반드시 있다는 보증서가 아닙니다.
반대로 스팬을 끝낸 뒤 is_recording이 false가 됐다는 사실도 처음부터 DROP이었다는
뜻은 아닙니다. 상태를 관측한 시점까지 적어야 두 경우를 구별할 수 있습니다.

ParentBased의 root는 전체 트레이스 스위치가 아니다

실습에서 만들 정책은 부모가 없는 요청은 기록하지 않고, 부모가 있는 요청은 그
부모의 sampled 결정을 따르는 것입니다. ParentBased(root=ALWAYS_OFF)가 이 정책을
표현합니다. root가 꺼져 있어도 sampled 원격 부모에서 온 자식은 기록됩니다.
root 옵션을 “모든 스팬을 끄는 설정”으로 해석하면 정상적인 자식까지 누락으로
진단하게 됩니다.

반대로 ParentBased(root=ALWAYS_ON)이라고 해서 unsampled 부모의 자식이 자동으로
켜지지는 않습니다. root 샘플러는 부모가 없는 경우의 결정입니다. 원격·로컬 부모,
sampled·unsampled 조합에는 각각의 분기가 있으며 기본값이 부모 결정을 따릅니다.
이번 과제는 원격 두 경우와 로컬 두 경우를 모두 실행해서, 한 헤더에서 우연히
맞은 구현을 정책 전체의 정답으로 인정하지 않습니다.

부모가 꺼져 있으면 자식도 절대로 못 켜는가

아닙니다. 사전 실험에서 ALWAYS_ON과 명시적으로 변경한 원격 부모 정책은
unsampled 부모와 같은 trace ID를 가진 sampled 자식을 만들었습니다. 부모의
결정을 따르는 것은 샘플러 정책이며, 식별자 자체가 자식의 기록을 금지하는 것은
아닙니다. Python 공식 sampling API도 always_on과 parentbased_always_on을 구별합니다.

다만 이것은 이미 버린 부모의 내용이 복구됐다는 뜻이 아닙니다. 부모가 기록하지
않은 속성·이벤트·시간 구간은 그대로 없습니다. 자식 한 구간을 관찰할 수 있게 된
것과 전체 요청의 트레이스가 완전해진 것을 구별해야 합니다. 현장에서 “샘플링을
강제로 켰으니 이제 전체가 보일 것”이라고 기대하면 다른 종류의 오진이 시작됩니다.

현장에서 만나는 모습

평소 root 트래픽 일부만 수집하는 서비스에 외부 파트너의 요청이 들어온다고
생각해 보세요. 파트너가 이미 sampled 컨텍스트를 보낸 경우와 그렇지 않은 경우는
같은 root 비율 설정만 보고 설명할 수 없습니다. 요청에서 받은 컨텍스트가 유효한지,
원격으로 해석됐는지, 선택된 샘플러가 부모 결정을 따르는지 순서대로 확인합니다.

원인이 SDK 샘플링인지 알아보려면 collector 로그만 늘리는 것보다 SDK 처리기 앞뒤의
수를 비교하는 편이 빠를 수 있습니다. 시작·종료 관측이 모두 없으면 생성 경로와
샘플링을 봅니다. 종료 관측은 있지만 내보내기가 없으면 sampled와 처리기 연결을
살펴봅니다. 이미 내보내기가 확인됐다면 그때부터 전송 경계로 이동합니다. 관측할
자리를 한 칸씩 옮기면 무관한 인프라 변경을 줄일 수 있습니다.

실습의 데이터는 실제 고객 정보가 아닌 합성 주문입니다. production에서도 오류
조사를 위해 원문 주문·인증 헤더·개인정보를 무조건 스팬 속성으로 넣어서는 안 됩니다.
필요한 최소 식별자와 상태만으로도 처리 경로를 구별할 수 있는지 먼저 생각하세요.

이어서 확인할 것

다음 이론에서는 예외 이벤트와 스팬 상태, 스팬 종료와 flush, 수신과 저장의 경계를
구별합니다. 이후 실습은 설명에 맞는 Python 함수를 직접 수정하는 방식입니다.
초록불을 만드는 문자열이 아니라 실제 SDK의 관측 결과가 채점 기준입니다.

공식 기준: [Tracing SDK](https://opentelemetry.io/docs/specs/otel/trace/sdk/),
[Python sampling API](https://opentelemetry-python.readthedocs.io/en/latest/sdk/trace.sampling.html).