분산 트레이싱이 끊기는 자리 · 속성·이벤트·링크 — 재현할 수 있는 스팬 · 실습
스팬은 있는데 재현이 안 된다
목표
실패한 요청의 스팬 한 줄에서 출발해, 그 스팬만 들고 같은 실패를 다시 낼 수 있을 만큼 속성과 이벤트를 채웁니다. 넣으면 안 되는 값을 바꿔 남기는 법, 고유값 예산을 세는 법, 팀 규약을 파일로 적고 검사기로 강제하는 법까지 한 바퀴 돕니다.
왜 중요한가
자동 계측은 HTTP 겉면을 채워 준다. 경로와 상태 코드와 길이는 나오지만, 새벽에 그 스팬을 열어 놓고 '그래서 무엇을 넣으면 이게 다시 나나' 를 물으면 답이 없다. 재현 입력과 그때의 상태와 우리가 한 일을 속성으로 남겨야 조사가 시작된다. 반대로 요청 본문을 통째로 넣으면 연락처와 토큰이 관측 백엔드에 그대로 쌓인다 — 그래서 원문 대신 해시·범주·길이로 바꿔 남긴다. 시점이 있는 사실은 속성이 아니라 이벤트이고, 부모-자식이 아닌 관계는 링크다. 마지막으로 열쇠마다 고유값이 몇 가지인지를 세어 두지 않으면, 예외 메시지 하나가 열쇠를 수천 가지 값으로 부풀려 검색 화면을 죽인다.
단계
1. /opt/app/tracelab/tp_attrs/failed.jsonl 한 줄을 읽어 보세요. 실패한 요청의 스팬인데 재현에 필요한 값이 없습니다. /root/tp-attrs/01-missing.txt 에 다섯 줄을 적으세요 — 각 줄은 <열쇠이름>=<왜 필요한가> 이고, 열쇠는 차례로 shop.cart.item_count · shop.request.body_bytes · shop.cache.hit · shop.queue.depth · shop.payment.retry_count 입니다. 이유는 열쇠마다 25자 이상으로, '이 값이 없으면 무엇을 판단할 수 없는가' 를 자기 말로 적습니다.
2. /root/tp-attrs/02_attrs.py 를 만드세요. tracelab.tp_attrs.orders 로 주문 ord-1010 를 처리하면서 POST /checkout 이라는 SERVER 스팬 하나를 만들고, 1단계의 다섯 열쇠에 주문 번호 shop.order.id 를 더해 여섯 속성을 답니다. 결제는 charge(주문번호, 시도번호) 를 1·2·3 으로 세 번까지 시도하고, shop.payment.retry_count 는 (시도 횟수 − 1) 입니다. 덤프 기본 경로는 /root/tp-attrs/02-attrs.jsonl 이고 환경변수 TRACELAB_OUT 이 있으면 그쪽을 씁니다.
3. /root/tp-attrs/03_redact.py 를 만드세요. 2단계에 세 속성을 더합니다 — shop.customer.email_hash 는 이메일의 SHA-256 16진 문자열 앞 16자, shop.payment.card_brand 는 카드의 brand 값, shop.auth.token_len 은 인증 토큰의 길이(정수)입니다. 이메일·카드 번호(pan)·토큰·우편번호의 원문은 어느 속성에도 남기지 않습니다. 덤프 기본 경로는 /root/tp-attrs/03-redact.jsonl 입니다.
4. /root/tp-attrs/04_events.py 를 만드세요. 3단계에 더해 (1) 캐시가 빗나갔으면 cache.miss 이벤트를 하나, (2) 결제 시도가 실패할 때마다 payment.attempt.failed 이벤트를 하나씩 남깁니다 — 이벤트 속성은 attempt(정수 시도 번호)와 reason(PaymentError 의 kind)입니다. (3) 세 번 다 실패하면 예외 메시지를 shop.error.message 속성에 그대로 넣고 스팬 상태를 ERROR 로 바꿉니다. 덤프 기본 경로는 /root/tp-attrs/04-events.jsonl 입니다. 이 단계의 shop.error.message 는 5단계에서 다시 봅니다.
5. /root/tp-attrs/05_bulk.py 를 만들어 orders.ORDER_IDS 의 200건을 같은 계측으로 한 번씩 처리하세요(요청마다 스팬 하나). 덤프 기본 경로는 /root/tp-attrs/05-bulk.jsonl 입니다. 그다음 /root/tp-attrs/05-cardinality.tsv 에 덤프에 나온 속성 열쇠마다 한 줄씩 <열쇠><탭><고유값수><탭><판정> 을 열쇠 이름 오름차순으로 적으세요. 판정은 shop.order.id 와 shop.customer.email_hash 면 free, 그 밖에 고유값이 20 이하면 low, 20 을 넘으면 leak 입니다.
6. /root/tp-attrs/convention.tsv 를 만드세요. 한 줄이 열쇠 하나이고 탭으로 나눈 네 칸 <열쇠><탭><타입><탭><허용값><탭><고유값성격> 입니다. 타입은 string·int·bool·deny 중 하나, 허용값은 *(제한 없음)이나 | 로 이은 목록, 고유값 성격은 free 또는 low 입니다. 5단계에 나온 열쇠 열 개 중 shop.error.message 는 deny 로 막고 대신 shop.error.kind 를 declined|timeout 으로 새로 넣으세요. 8단계에서 쓸 shop.refund.amount(int·free)도 미리 넣고, 원문 금지 열쇠 shop.customer.email · shop.payment.card_pan · shop.auth.token 도 deny 로 적습니다. deny 줄의 허용값과 성격 칸은 - 로 둡니다.
7. /root/tp-attrs/lint_spans.py 를 만드세요. python3 lint_spans.py <규약파일> <덤프> 로 부르면 규약을 어긴 열쇠마다 VIOLATION <열쇠> <이유> 를 열쇠 이름 오름차순으로 한 번씩만 찍고 종료 코드 1, 어긴 것이 없으면 OK 로 시작하는 한 줄과 종료 코드 0 을 냅니다. 봐야 할 것은 네 가지입니다 — 규약에 없는 열쇠, deny 로 막은 열쇠, 타입이 다른 값, 허용값 목록에 없는 값. 여기에 더해 low 로 선언한 열쇠의 고유값이 덤프 안에서 20 을 넘으면 그것도 위반입니다. 만든 뒤 /root/tp-attrs/05-bulk.jsonl 과 /opt/app/tracelab/tp_attrs/noisy.jsonl 에 각각 돌리고, /root/tp-attrs/07-violations.tsv 에 두 줄 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것> 을 그 순서로 적으세요.
8. /root/tp-attrs/08_refund.py 를 만들어 큐가 시킨 환불 ord-1027 를 처리하는 POST /refund SERVER 스팬 하나를 만드세요. 속성은 shop.order.id · shop.refund.amount · shop.queue.depth · shop.customer.email_hash · shop.payment.card_brand · shop.auth.token_len · shop.payment.retry_count 이고, 실패로 끝나면 shop.error.kind 를 더하고 상태를 ERROR 로 둡니다(원문 메시지는 넣지 않습니다). 실패한 시도마다 payment.attempt.failed 이벤트를 남기고, orders.job_context(주문번호) 가 주는 트레이스 좌표를 링크로 답니다(부모로 붙이지 않습니다). 덤프 기본 경로는 /root/tp-attrs/08-refund.jsonl 이고, 7단계의 검사기를 이 덤프에 돌리면 OK 가 나와야 합니다.
참고
- 작업 디렉터리는
/root/tp-attrs입니다. 없으면 먼저 만드세요. - 계측 프로그램은 반드시
/opt/otel-lab/bin/python로 돌립니다. 덤프를 읽고 세는 스크립트는 시스템python3로 충분합니다. - 공용 배선은
/opt/app/tracelab/dump.py(provider·flush), 재료는/opt/app/tracelab/tp_attrs/orders.py(주문 처리가 아는 사실들)와/opt/app/tracelab/tp_attrs/failed.jsonl(1단계가 보는 실패 스팬),/opt/app/tracelab/tp_attrs/noisy.jsonl(7단계에서 검사기를 돌려 볼 남의 덤프)입니다. 두 덤프는/opt/app/tracelab/tp_attrs/make_fixtures.py가 만듭니다. - 흔한 실수: 덤프 파일을 지우지 않고 프로그램을 다시 돌리는 것. 덤프는 이어 쓰기라 스팬이 쌓입니다.
- 흔한 실수: 관계를 속성 문자열로 적는 것(
parent_trace_id=...). 도구가 이어 주지 못해 사람이 눈으로 찾게 됩니다. 관계는 링크입니다. - 이 파드에는 컬렉터가 없습니다. 운영에서는 컬렉터의 redaction 프로세서가 한 번 더 걸러 내지만, 여기서는 애플리케이션이 스스로 거른 결과만 봅니다. 백엔드의 색인 비용도 잴 수 없어 고유값 수로 대신합니다.
- [OpenTelemetry — Traces](https://opentelemetry.io/docs/concepts/signals/traces/) · [민감한 자료 다루기](https://opentelemetry.io/docs/security/handling-sensitive-data/) · [속성 이름 규약](https://opentelemetry.io/docs/specs/semconv/general/naming/) · [Python API 참고(add_event·Link)](https://opentelemetry-python.readthedocs.io/en/latest/api/trace.html) · [컬렉터 설정 모범 사례(redaction)](https://opentelemetry.io/docs/security/config-best-practices/)
단계 8개
- 실패한 스팬을 놓고 없는 값을 적는다
- 재현에 필요한 값을 속성으로 더한다
- 넣으면 안 되는 값은 해시·범주·길이로 바꾼다
- 시점이 있는 사실은 이벤트로 옮긴다
- 열쇠마다 고유값을 세어 예산표를 만든다
- 팀 규약을 파일로 쓴다
- 규약을 어긴 스팬을 잡는 검사기를 만든다
- 규약대로 두 번째 핸들러를 계측하고 링크로 잇는다