Where Distributed Tracing Breaks
The span is there, but nothing reproduces
한국어 원문으로 표시합니다.
목표
실패한 요청의 스팬 한 줄에서 출발해, 그 스팬만 들고 같은 실패를 다시 낼 수 있을 만큼 속성과 이벤트를 채웁니다. 넣으면 안 되는 값을 바꿔 남기는 법, 고유값 예산을 세는 법, 팀 규약을 파일로 적고 검사기로 강제하는 법까지 한 바퀴 돕니다.
왜 중요한가
자동 계측은 HTTP 겉면을 채워 준다. 경로와 상태 코드와 길이는 나오지만, 새벽에 그 스팬을 열어 놓고 '그래서 무엇을 넣으면 이게 다시 나나' 를 물으면 답이 없다. 재현 입력과 그때의 상태와 우리가 한 일을 속성으로 남겨야 조사가 시작된다. 반대로 요청 본문을 통째로 넣으면 연락처와 토큰이 관측 백엔드에 그대로 쌓인다 — 그래서 원문 대신 해시·범주·길이로 바꿔 남긴다. 시점이 있는 사실은 속성이 아니라 이벤트이고, 부모-자식이 아닌 관계는 링크다. 마지막으로 열쇠마다 고유값이 몇 가지인지를 세어 두지 않으면, 예외 메시지 하나가 열쇠를 수천 가지 값으로 부풀려 검색 화면을 죽인다.
단계
/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자 이상으로, '이 값이 없으면 무엇을 판단할 수 없는가' 를 자기 말로 적습니다./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이 있으면 그쪽을 씁니다./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입니다./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단계에서 다시 봅니다./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입니다./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줄의 허용값과 성격 칸은-로 둡니다./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에 두 줄<덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것>을 그 순서로 적으세요./root/tp-attrs/08_refund.py를 만들어 큐가 시킨 환불ord-1027를 처리하는POST /refundSERVER스팬 하나를 만드세요. 속성은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 · 민감한 자료 다루기 · 속성 이름 규약 · Python API 참고(add_event·Link) · 컬렉터 설정 모범 사례(redaction)
실패한 스팬을 놓고 없는 값을 적는다
/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자 이상으로, '이 값이 없으면 무엇을 판단할 수 없는가' 를 자기 말로 적습니다.
덤프를 보기 좋게 펴려면 python3 -m json.tool /opt/app/tracelab/tp_attrs/failed.jsonl 이 편합니다. 스팬에 지금 있는 것은 HTTP 겉면뿐입니다 — 경로, 상태 코드, 길이. 그것만으로 같은 실패를 다시 낼 수 있는지 스스로 물어보세요. 채점기는 다섯 열쇠가 정말 그 스팬에 없는지도 확인합니다.
재현에 필요한 값을 속성으로 더한다
/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 이 있으면 그쪽을 씁니다.
orders.payload 가 요청 본문을, orders.body_bytes 가 그 크기를, orders.cache_lookup 과 orders.queue_depth 가 그때의 상태를 돌려줍니다. 결제 실패는 orders.PaymentError 입니다. 계측 프로그램은 /opt/otel-lab/bin/python 로 돌리세요 — 시스템 python3 에는 OpenTelemetry 가 없습니다.
넣으면 안 되는 값은 해시·범주·길이로 바꾼다
/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 입니다.
원문을 통째로 버리지 않고 바꿔 남기는 이유는, 그래도 답할 수 있는 질문이 있기 때문입니다 — 해시는 '같은 사용자에게 반복되는가', 범주는 '특정 카드사에서만 나는가', 길이는 '토큰이 잘려 들어왔는가'. 해시는 hashlib.sha256(문자열.encode("utf-8")).hexdigest() 로 만듭니다.
시점이 있는 사실은 이벤트로 옮긴다
/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단계에서 다시 봅니다.
재시도를 두 번 했다는 것은 하나의 숫자라 속성이고, 첫 재시도가 언제 왜 일어났는가는 시각이 있는 기록이라 이벤트입니다. 둘은 경쟁하지 않습니다 — 세는 것은 속성, 일어난 순간은 이벤트. 상태는 span.set_status(Status(StatusCode.ERROR, "...")) 로 바꿉니다.
열쇠마다 고유값을 세어 예산표를 만든다
/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 입니다.
식별자는 요청마다 달라야 정상이고, 범주형 열쇠는 값이 몇 가지뿐이어야 정상입니다. 범주여야 할 자리에 원문이 새어 들어가면 열쇠 하나가 수천 가지 값을 갖게 됩니다 — 이 덤프에서 leak 이 하나 나오는데, 그게 4단계에서 일부러 넣은 그 속성입니다.
팀 규약을 파일로 쓴다
/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 줄의 허용값과 성격 칸은 - 로 둡니다.
규약을 문장으로만 남기면 다음 사람이 읽지 않습니다. 기계가 읽을 수 있는 표로 만들어야 검사기를 붙일 수 있습니다. 금지 열쇠를 함께 적는 이유는 '넣지 말자' 는 합의가 코드 리뷰에서만 살아 있으면 결국 새기 때문입니다. 줄 수는 열다섯입니다.
규약을 어긴 스팬을 잡는 검사기를 만든다
/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 에 두 줄 <덤프파일이름><탭><깨진 열쇠들을 쉼표로 이은 것> 을 그 순서로 적으세요.
위반을 스팬마다 찍으면 200줄이 나옵니다 — 열쇠 단위로 한 번만 모아서 찍으세요. noisy.jsonl 은 남의 팀 덤프라 고칠 수 없습니다. 이 단계에서 할 일은 '무엇이 어긋났는지 기계가 말해 주게 만드는 것' 이고, 고치는 일은 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 가 나와야 합니다.
링크는 Link(SpanContext(trace_id=int(16진문자열, 16), span_id=int(16진문자열, 16), is_remote=True, trace_flags=TraceFlags(0x01))) 를 만들어 start_as_current_span(..., links=[link]) 로 넘깁니다. 큐 작업을 부모로 붙이면 몇 시간 전에 시작해 지금 끝나는 이상한 부모가 생깁니다 — 관계는 있지만 부모-자식은 아닌 자리가 바로 링크입니다. 검사기가 VIOLATION 을 내면 규약이 아니라 계측을 고치세요.