外から来た baggage をそのまま信じてしまった
目標
baggageとtracestateを自分で入れて取り出しながら、baggageが自然にはスパン属性にならないことと、そのヘッダーがすべての下流リクエストに載ることを数字で確認し、外から入ってきたコンテキストに許可リストフィルターを作って、2つ目のサービスまで同じルールで処理します。
なぜ重要なのか
トレースがつながっても、6ホップ下のスパンには「どのテナントのリクエストか」がありません。baggageはその値をリクエスト全体に運ぶための場所ですが、入れておくだけではどこにも現れません。各サービスが取り出して自分のスパンに書き写して初めて、クエリできるデータになります。逆に、値をたっぷり入れると、そのバイトが下流へ出ていくすべてのリクエストに付き、呼び出しが多い経路でだけ遅延が増え、プロキシのヘッダー上限に引っかかります。そして、公開APIの手前で受け取ったbaggageは他人が書いた文字列なので、そのまま属性に移すと、メトリクスのカーディナリティを他人が決めることになります。tracestateは同じリクエストに載りますが、業務の値を入れる場所ではなく、トレーシングツールが自分の位置を書く場所で、項目数と長さに規格が定めたルールがあります。
ステップ
/root/tp-baggage/carry.pyを作成してください。ダンプのパスは環境変数TRACELAB_OUTから読み、なければ/root/tp-baggage/carry.jsonlを使います。サービス名はshop-edge、スパン名はcheckoutです。そのスパンの中で、baggageにtenant=acmeとcheckout.tier=goldを入れ、空のdictにtraceparentとbaggageを一緒に注入したあと、そのdictをダンプと同じディレクトリのcarry.jsonにJSONで書いてください。そしてプログラムを1回実行してください。/root/tp-baggage/attrs.pyを作成してください。デフォルトのダンプのパスは/root/tp-baggage/attrs.jsonlです。/opt/app/tracelab/tp_baggage/requests.jsonからnameがokのリクエストのヘッダーを取り出してコンテキストを抽出し、そのコンテキストの下にSERVERスパンを2つ作ります。1つ目のprice.rawには何も属性を付けず、2つ目のprice.taggedには、baggageから取り出した値を属性tenantとcheckout.tierとして書き写してください。サービス名はshop-pricingです。プログラムを実行したあと、2つのスパンのattributesを目で比べてください。/root/tp-baggage/budget.pyを作成して、/opt/app/tracelab/tp_baggage/candidates.jsonの候補4束をそれぞれbaggageヘッダーにして、コストを測ってください。デフォルトのダンプのパスは/root/tp-baggage/budget.jsonlで、ダンプと同じディレクトリのbudget.tsvに、ヘッダーなしで4行をタブで区切って書きます:<id>、<넣은 항목 수>、<헤더에 실제로 실린 항목 수>、<헤더 바이트 수>、<keep|drop>(プレースホルダーは順に、入れた項目数、ヘッダーに実際に載った項目数、ヘッダーのバイト数です)。最後の欄は、W3C Baggage規格が伝達を保証する範囲(項目数とバイト数)の中ならkeep、外れたらdropです。候補ごとにbudgetというスパンを1つずつ残し、属性baggage.id・baggage.entries・baggage.bytesを付けてください。/root/tp-baggage/sanitize.pyを作成してください。デフォルトのダンプのパスは/root/tp-baggage/sanitize.jsonlです。/opt/app/tracelab/tp_baggage/requests.jsonのhostileリクエストからbaggageを抽出したあと、キーがtenant・checkout.tier・regionのいずれかで、値が正規表現^[a-z0-9][a-z0-9._-]{0,31}$に合う項目だけを残してください。残ったものだけを入れた新しいコンテキストを作ってbaggageヘッダーとして注入し、ダンプと同じディレクトリのkept.jsonに書きます。SERVERスパンPOST /checkoutをそのリクエストの親の下に作り、属性baggage.in・baggage.kept・baggage.droppedに個数を入れ、イベントbaggage.droppedの属性keysに、捨てたキーを辞書順にカンマでつないで書いてください。サービス名はshop-edgeです。/root/tp-baggage/tsread.pyを作成して、/opt/app/tracelab/tp_baggage/tracestates.jsonのヘッダー6つを判定してください。デフォルトのダンプのパスは/root/tp-baggage/tsread.jsonlで、ダンプと同じディレクトリのtsreport.tsvに、<id>、<항목 수>、<헤더 글자 수>、<ok|bad>(プレースホルダーは順に、項目数、ヘッダーの文字数です)をタブで区切って6行書きます。okは、項目数が規格の上限以下で、キーが小文字・数字で始まり、小文字・数字と_・-・*・/だけを使い、値が規格の文字数上限以下で、カンマや等号を含まないときです。さらに、/root/tp-baggage/05-limits.txtに3行max_members=・max_value_chars=・propagate_min_chars=を、規格から読み取った数字で書いてください。項目ごとにtracestate.readスパンを1つずつ残し、属性ts.id・ts.members・ts.okを付けます。/root/tp-baggage/tsmutate.pyを作成して、同じ6つのヘッダーを出ていくヘッダーに変えてください。ルールは4つです。(1)自分たちの項目はlabhub=r1で、常に一番左に置きます、(2)自分たちのキーがすでにあれば削除して、新しく先頭に入れます(2回出てはいけません)、(3)残りの項目の順序はそのままにします、(4)項目数が規格の上限を超えたら、128文字を超える項目を後ろから先に削除し、それでも超えるなら、末尾から削除します。デフォルトのダンプのパスは/root/tp-baggage/tsmutate.jsonlで、ダンプと同じディレクトリのtsout.tsvに、<id>と<나가는 헤더>(プレースホルダーはIDと、出ていくヘッダーです)をタブで区切って6行書きます。項目ごとにtracestate.outスパンを残し、属性ts.id・ts.outを付けてください。- 前のステップで手で入れていた値を、
/root/tp-baggage/policy.jsonの1か所にまとめてください。baggageの下にallow(許可するキーの配列)、value_pattern(値の正規表現)、max_entries、max_bytesを置き、tracestateの下にkey、value、max_members、drop_over_chars、positionを置きます。max_entriesとmax_bytesは規格が保証する範囲の中でなければならず、max_membersとdrop_over_charsは規格の数字と同じでなければならず、positionはleftです。allowには、人を識別するキーを入れないでください。 /root/tp-baggage/gateway.pyを作成して、/root/tp-baggage/policy.jsonを読み、/opt/app/tracelab/tp_baggage/requests.jsonのリクエスト4件すべてを処理してください。リクエストごとにbaggageをルールどおりにふるい、tracestateをステップ6のルールどおりに変えたあと、そのリクエストの親の下にSERVERスパンgatewayを1つずつ残し、属性req.name・baggage.kept・baggage.dropped・tracestate.membersを付けます。デフォルトのダンプのパスは/root/tp-baggage/gateway.jsonlで、ダンプと同じディレクトリのgateway.tsvに、<name>、<남긴 수>、<버린 수>、<나가는 tracestate 항목 수>(プレースホルダーは順に、リクエスト名、残した数、捨てた数、出ていくtracestateの項目数です)をタブで区切って4行書きます。サービス名はshop-gatewayです。
参考
- 作業ディレクトリは
/root/tp-baggageです。なければ先に作ってください。 - 計装プログラムは必ず
/opt/otel-lab/bin/pythonで実行します。システムのpython3にはOpenTelemetryがありません。 - ダンプのパスは、常に環境変数
TRACELAB_OUTを先に読み、なければ課題に書かれたデフォルトのパスを使います。付随する成果物(carry.json・budget.tsvなど)もダンプと同じディレクトリに書いてください。採点ツールが同じプログラムを自分の一時ディレクトリでもう一度実行して照合するためです。 - ダンプファイルは追記なので、プログラムを何度も実行するとスパンがたまります。開始時に
open(OUT, "w").close()で空にしてください。 - 材料は
/opt/app/tracelab/tp_baggage/にあります。requests.json(入ってくるリクエスト4件)、candidates.json(baggageの候補4束)、tracestates.json(tracestateヘッダー6つ)です。これらのファイルは編集しません。 - ダンプを人が読みやすい形で見るには、
python3 /opt/lab/checks/_tplib.py summary <덤프>(プレースホルダーはダンプのパスです)を使ってください。 - よくある間違い: baggageを入れたあと、
set_baggageが返したコンテキストを使わずに、現在のコンテキストを注入してしまいます。そうするとヘッダーが空で出ていきます。 - よくある間違い: 入ってきたbaggageを抽出するとき、空の
Context()を基準にしないため、自分たちの側の値が混ざってしまいます。 - W3C Baggage・W3C Trace Context: tracestate・OpenTelemetry: Baggageの概念・OpenTelemetry Python: Propagation・OpenTelemetry: Context propagation
baggageを入れて次のサービスへ流す
/root/tp-baggage/carry.pyを作成してください。ダンプのパスは環境変数TRACELAB_OUTから読み、なければ/root/tp-baggage/carry.jsonlを使います。サービス名はshop-edge、スパン名はcheckoutです。そのスパンの中で、baggageにtenant=acmeとcheckout.tier=goldを入れ、空のdictにtraceparentとbaggageを一緒に注入したあと、そのdictをダンプと同じディレクトリのcarry.jsonにJSONで書いてください。そしてプログラムを1回実行してください。
opentelemetry.baggage.set_baggage(key, value, context=...)は、値を載せた新しいコンテキストを返します。そのコンテキストをW3CBaggagePropagator().inject(carrier, context=...)とTraceContextTextMapPropagator().inject(...)の両方に渡せば、1つのdictに2つのヘッダーが入ります。計装プログラムは/opt/otel-lab/bin/pythonで実行します。
baggageは自然にはスパン属性にならない
/root/tp-baggage/attrs.pyを作成してください。デフォルトのダンプのパスは/root/tp-baggage/attrs.jsonlです。/opt/app/tracelab/tp_baggage/requests.jsonからnameがokのリクエストのヘッダーを取り出してコンテキストを抽出し、そのコンテキストの下にSERVERスパンを2つ作ります。1つ目のprice.rawには何も属性を付けず、2つ目のprice.taggedには、baggageから取り出した値を属性tenantとcheckout.tierとして書き写してください。サービス名はshop-pricingです。プログラムを実行したあと、2つのスパンのattributesを目で比べてください。
ヘッダーからコンテキストを取り出す作業は、2つのプロパゲーターが分担します。traceparentはTraceContextTextMapPropagator、baggageはW3CBaggagePropagatorです。前の結果を、後ろのcontext=に渡して初めて、2つが1つのコンテキストに集まります。取り出したbaggage全体はbaggage.get_all(ctx)で見ます。start_as_current_span(..., context=ctx, kind=SpanKind.SERVER)を使います。
baggage1行が、出ていくすべてのリクエストに載る
/root/tp-baggage/budget.pyを作成して、/opt/app/tracelab/tp_baggage/candidates.jsonの候補4束をそれぞれbaggageヘッダーにして、コストを測ってください。デフォルトのダンプのパスは/root/tp-baggage/budget.jsonlで、ダンプと同じディレクトリのbudget.tsvに、ヘッダーなしで4行をタブで区切って書きます: <id>、<넣은 항목 수>、<헤더에 실제로 실린 항목 수>、<헤더 바이트 수>、<keep|drop>(プレースホルダーは順に、入れた項目数、ヘッダーに実際に載った項目数、ヘッダーのバイト数です)。最後の欄は、W3C Baggage規格が伝達を保証する範囲(項目数とバイト数)の中ならkeep、外れたらdropです。候補ごとにbudgetというスパンを1つずつ残し、属性baggage.id・baggage.entries・baggage.bytesを付けてください。
空のコンテキストはopentelemetry.context.Context()で作ります。注入したdictのbaggageの値がそのままヘッダー文字列で、項目数はカンマで区切った個数、バイト数はUTF-8でエンコードした長さです。保証範囲の2つの数字は、規格のLimitsの節に書かれています。4つの候補のうち1つは、入れた項目数と載った項目数が異なります。なぜそうなるのか、ヘッダーを直接見てください。
外から入ってきたbaggageに許可リストをかける
/root/tp-baggage/sanitize.pyを作成してください。デフォルトのダンプのパスは/root/tp-baggage/sanitize.jsonlです。/opt/app/tracelab/tp_baggage/requests.jsonのhostileリクエストからbaggageを抽出したあと、キーがtenant・checkout.tier・regionのいずれかで、値が正規表現^[a-z0-9][a-z0-9._-]{0,31}$に合う項目だけを残してください。残ったものだけを入れた新しいコンテキストを作ってbaggageヘッダーとして注入し、ダンプと同じディレクトリのkept.jsonに書きます。SERVERスパンPOST /checkoutをそのリクエストの親の下に作り、属性baggage.in・baggage.kept・baggage.droppedに個数を入れ、イベントbaggage.droppedの属性keysに、捨てたキーを辞書順にカンマでつないで書いてください。サービス名はshop-edgeです。
入ってきたbaggageだけを見るには、抽出するときに現在のコンテキストではなく、空のContext()を基準にする必要があります。キーだけをふるっても足りません。許可されたキーに、おかしな値が入ってくるケースが、このリクエストには混ざっています。イベントはspan.add_event(이름, {속성})(プレースホルダーはイベント名と属性です)で残します。
tracestateのルールを規格から読んで適用する
/root/tp-baggage/tsread.pyを作成して、/opt/app/tracelab/tp_baggage/tracestates.jsonのヘッダー6つを判定してください。デフォルトのダンプのパスは/root/tp-baggage/tsread.jsonlで、ダンプと同じディレクトリのtsreport.tsvに、<id>、<항목 수>、<헤더 글자 수>、<ok|bad>(プレースホルダーは順に、項目数、ヘッダーの文字数です)をタブで区切って6行書きます。okは、項目数が規格の上限以下で、キーが小文字・数字で始まり、小文字・数字と_・-・*・/だけを使い、値が規格の文字数上限以下で、カンマや等号を含まないときです。さらに、/root/tp-baggage/05-limits.txtに3行max_members=・max_value_chars=・propagate_min_chars=を、規格から読み取った数字で書いてください。項目ごとにtracestate.readスパンを1つずつ残し、属性ts.id・ts.members・ts.okを付けます。
3つの数字は、Trace Context規格のtracestateのLimitsの節と、KeyおよびValueの節にそのまま書かれています。propagate_min_charsは上限ではなく、ベンダーが最低限伝達しなければならない長さです。空のヘッダーはエラーではありません。規格が受け入れるよう書いています。
入ってきたtracestateを保ちながら、自分たちの項目を先頭に入れる
/root/tp-baggage/tsmutate.pyを作成して、同じ6つのヘッダーを出ていくヘッダーに変えてください。ルールは4つです。(1)自分たちの項目はlabhub=r1で、常に一番左に置きます、(2)自分たちのキーがすでにあれば削除して、新しく先頭に入れます(2回出てはいけません)、(3)残りの項目の順序はそのままにします、(4)項目数が規格の上限を超えたら、128文字を超える項目を後ろから先に削除し、それでも超えるなら、末尾から削除します。デフォルトのダンプのパスは/root/tp-baggage/tsmutate.jsonlで、ダンプと同じディレクトリのtsout.tsvに、<id>と<나가는 헤더>(プレースホルダーはIDと、出ていくヘッダーです)をタブで区切って6行書きます。項目ごとにtracestate.outスパンを残し、属性ts.id・ts.outを付けてください。
規格は「変更したキーは左へ移し、触れていない項目の順序は保て」と書いています。そのため、自分たちの項目を削除してから、もう一度先頭に付ける順序が重要です。切り捨てるときは項目を丸ごと捨てる必要があり、規格が先に捨てるよう指定した項目が何かは、Limitsの節に書かれています。6つの入力のうち2つが、互いに異なる理由で切り捨てられます。
ルールを機械が読めるファイルに固める
前のステップで手で入れていた値を、/root/tp-baggage/policy.jsonの1か所にまとめてください。baggageの下にallow(許可するキーの配列)、value_pattern(値の正規表現)、max_entries、max_bytesを置き、tracestateの下にkey、value、max_members、drop_over_chars、positionを置きます。max_entriesとmax_bytesは規格が保証する範囲の中でなければならず、max_membersとdrop_over_charsは規格の数字と同じでなければならず、positionはleftです。allowには、人を識別するキーを入れないでください。
このファイルは、次のステップのプログラムが読みます。ステップ4で書いた許可リストと正規表現、ステップ6で書いた自分たちのキーと値が、そのままここへ移ってくれば済みます。max_entries・max_bytesは、規格の上限より小さく設定してもかまいません。上限は「ここまでは伝達される」という意味であり、「ここまで埋めてよい」という意味ではありません。
2つ目のサービスにルールをそのまま適用する
/root/tp-baggage/gateway.pyを作成して、/root/tp-baggage/policy.jsonを読み、/opt/app/tracelab/tp_baggage/requests.jsonのリクエスト4件すべてを処理してください。リクエストごとにbaggageをルールどおりにふるい、tracestateをステップ6のルールどおりに変えたあと、そのリクエストの親の下にSERVERスパンgatewayを1つずつ残し、属性req.name・baggage.kept・baggage.dropped・tracestate.membersを付けます。デフォルトのダンプのパスは/root/tp-baggage/gateway.jsonlで、ダンプと同じディレクトリのgateway.tsvに、<name>、<남긴 수>、<버린 수>、<나가는 tracestate 항목 수>(プレースホルダーは順に、リクエスト名、残した数、捨てた数、出ていくtracestateの項目数です)をタブで区切って4行書きます。サービス名はshop-gatewayです。
ルールを定数として書き直さず、policy.jsonから読んでください。このステップの要点はそこです。4つのリクエストのうち1つはbaggageもtracestateもなく、1つはtracestateがすでにいっぱいです。どちらもエラーなく通過する必要があります。