Where Distributed Tracing Breaks
We Trusted the Baggage That Came From Outside
Goal
You put in and take out baggage and tracestate yourself, confirm with numbers that baggage does not become a span attribute on its own and that its header is carried on every downstream request, and build an allowlist filter for context that came in from outside, handling even a second service with the same rules.
Why it matters
Even when traces are connected, the span six hops down lacks "which tenant's request is this." Baggage is the place meant to carry that value across the whole request, but merely putting it in makes it appear nowhere — each service has to take it out and write it onto its own span before it becomes data you can query. Conversely, if you put in values generously, those bytes attach to every request going downstream, so only paths with many calls get slower and hit the proxy's header limit. And baggage received in front of a public API is a string someone else wrote, so if you move it as it is into attributes, someone else decides your metric cardinality. Tracestate is carried on the same request but is not a place to put business values; it is where tracing tools write down their own position, and it has rules set by the specification for the number and length of entries.
Steps
- Create
/root/tp-baggage/carry.py. Read the dump path from the environment variableTRACELAB_OUT, and if it is absent use/root/tp-baggage/carry.jsonl. The service name isshop-edgeand the span name ischeckout. Inside that span, puttenant=acmeandcheckout.tier=goldinto baggage, injecttraceparentandbaggagetogether into an empty dict, and write that dict as JSON incarry.jsonin the same directory as the dump. Then run the program once. - Create
/root/tp-baggage/attrs.py. The default dump path is/root/tp-baggage/attrs.jsonl. Take the headers of the request whosenameisokfrom/opt/app/tracelab/tp_baggage/requests.json, extract the context, and create two SERVER spans under that context. The first,price.raw, gets no attributes at all, and on the second,price.tagged, write the values taken from baggage as the attributestenantandcheckout.tier. The service name isshop-pricing. After running the program, compare theattributesof the two spans by eye. - Create
/root/tp-baggage/budget.py, build each of the four candidate bundles in/opt/app/tracelab/tp_baggage/candidates.jsoninto a baggage header, and measure the cost. The default dump path is/root/tp-baggage/budget.jsonl, and write four lines with no header, tab-separated, inbudget.tsvin the same directory as the dump —<id>,<넣은 항목 수>,<헤더에 실제로 실린 항목 수>,<헤더 바이트 수>, and<keep|drop>(the placeholders are the id, the number of entries put in, the number of entries actually carried in the header, the header byte count, and keep or drop). The last column iskeepif it is within the range (the number of entries and the byte count) for which the W3C Baggage specification guarantees propagation, anddropif it goes beyond. Leave one span namedbudgetfor each candidate and attach the attributesbaggage.id,baggage.entries, andbaggage.bytes. - Create
/root/tp-baggage/sanitize.py. The default dump path is/root/tp-baggage/sanitize.jsonl. After extracting the baggage from thehostilerequest in/opt/app/tracelab/tp_baggage/requests.json, keep only the entries whose key is one oftenant,checkout.tier, andregionand whose value matches the regular expression^[a-z0-9][a-z0-9._-]{0,31}$. Build a new context containing only what remains, inject it as a baggage header, and write it inkept.jsonin the same directory as the dump. Create a SERVER spanPOST /checkoutunder that request's parent, put the counts in the attributesbaggage.in,baggage.kept, andbaggage.dropped, and in thekeysattribute of the eventbaggage.droppedwrite the dropped keys joined with commas in alphabetical order. The service name isshop-edge. - Create
/root/tp-baggage/tsread.pyand judge the six headers in/opt/app/tracelab/tp_baggage/tracestates.json. The default dump path is/root/tp-baggage/tsread.jsonl, and write six lines intsreport.tsvin the same directory as the dump with<id>,<항목 수>,<헤더 글자 수>, and<ok|bad>separated by tabs (the placeholders are the id, the number of entries, the header character count, and ok or bad).okmeans the number of entries is at or below the specification's limit, each key starts with a lowercase letter or digit and uses only lowercase letters, digits, and_,-,*,/, and each value is at or below the specification's character limit and contains no comma or equals sign. In addition, write three lines,max_members=,max_value_chars=, andpropagate_min_chars=, in/root/tp-baggage/05-limits.txtwith the numbers you read from the specification. Leave onetracestate.readspan per entry and attach the attributests.id,ts.members, andts.ok. - Create
/root/tp-baggage/tsmutate.pyand turn the same six headers into outgoing headers. There are four rules — (1) our entry islabhub=r1and always comes at the far left, (2) if our key already exists, delete it and put the new one at the front (it must not appear twice), (3) leave the order of the remaining entries as it is, and (4) if the number of entries exceeds the specification's limit, delete entries over 128 characters first from the back, and if it still exceeds, delete from the end. The default dump path is/root/tp-baggage/tsmutate.jsonl, and write six lines intsout.tsvin the same directory as the dump with<id>and<나가는 헤더>separated by a tab (the placeholders are the id and the outgoing header). Leave atracestate.outspan per entry and attach the attributests.idandts.out. - Gather the values you entered by hand in earlier steps into one place,
/root/tp-baggage/policy.json. Underbaggage, putallow(an array of allowed keys),value_pattern(a value regular expression),max_entries, andmax_bytes, and undertracestate, putkey,value,max_members,drop_over_chars, andposition.max_entriesandmax_bytesmust be within the range the specification guarantees,max_membersanddrop_over_charsmust equal the specification's numbers, andpositionisleft. Do not put keys that identify a person inallow. - Create
/root/tp-baggage/gateway.py, read/root/tp-baggage/policy.json, and process all four requests in/opt/app/tracelab/tp_baggage/requests.json. For each request, filter the baggage by the rules and transform the tracestate by the step 6 rules, then leave one SERVER spangatewayunder that request's parent and attach the attributesreq.name,baggage.kept,baggage.dropped, andtracestate.members. The default dump path is/root/tp-baggage/gateway.jsonl, and write four lines ingateway.tsvin the same directory as the dump with<name>,<남긴 수>,<버린 수>, and<나가는 tracestate 항목 수>separated by tabs (the placeholders are the name, the number kept, the number dropped, and the number of outgoing tracestate entries). The service name isshop-gateway.
Notes
- The working directory is
/root/tp-baggage. If it does not exist, create it first. - Always run the instrumented programs with
/opt/otel-lab/bin/python. The systempython3does not have OpenTelemetry. - For the dump path, always read the environment variable
TRACELAB_OUTfirst, and use the default path given in the task only when it is absent. Write side products (such ascarry.jsonandbudget.tsv) in the same directory as the dump too. This is because the grader runs the same program once more in its own temporary directory and compares. - The dump file is appended to, so if you run the program several times, spans pile up. Empty it at the start with
open(OUT, "w").close(). - The materials are in
/opt/app/tracelab/tp_baggage/—requests.json(four incoming requests),candidates.json(four candidate baggage bundles), andtracestates.json(six tracestate headers). You do not edit these files. - To view the dump in a human-readable way, use
python3 /opt/lab/checks/_tplib.py summary <덤프>(the placeholder is the dump). - Common mistake: after putting in baggage, injecting the current context instead of the context that
set_baggagereturned. Then the header goes out empty. - Common mistake: when extracting incoming baggage, not taking an empty
Context()as the base, so our own values get mixed in. - W3C Baggage · W3C Trace Context — tracestate · OpenTelemetry — Baggage concepts · OpenTelemetry Python — Propagation · OpenTelemetry — Context propagation
Put in baggage and flow it on to the next service
Create /root/tp-baggage/carry.py. Read the dump path from the environment variable TRACELAB_OUT, and if it is absent use /root/tp-baggage/carry.jsonl. The service name is shop-edge and the span name is checkout. Inside that span, put tenant=acme and checkout.tier=gold into baggage, inject traceparent and baggage together into an empty dict, and write that dict as JSON in carry.json in the same directory as the dump. Then run the program once.
opentelemetry.baggage.set_baggage(key, value, context=...) returns a new context with the value added. If you pass that context to both W3CBaggagePropagator().inject(carrier, context=...) and TraceContextTextMapPropagator().inject(...), both headers go into one dict. Run the instrumented program with /opt/otel-lab/bin/python.
Baggage does not become a span attribute on its own
Create /root/tp-baggage/attrs.py. The default dump path is /root/tp-baggage/attrs.jsonl. Take the headers of the request whose name is ok from /opt/app/tracelab/tp_baggage/requests.json, extract the context, and create two SERVER spans under that context. The first, price.raw, gets no attributes at all, and on the second, price.tagged, write the values taken from baggage as the attributes tenant and checkout.tier. The service name is shop-pricing. After running the program, compare the attributes of the two spans by eye.
Two extractors share the job of taking the context out of the headers — TraceContextTextMapPropagator for traceparent and W3CBaggagePropagator for baggage. You have to pass the earlier result as the later context= for the two to gather in one context. You see the whole extracted baggage with baggage.get_all(ctx). start_as_current_span(..., context=ctx, kind=SpanKind.SERVER).
One line of baggage rides on every outgoing request
Create /root/tp-baggage/budget.py, build each of the four candidate bundles in /opt/app/tracelab/tp_baggage/candidates.json into a baggage header, and measure the cost. The default dump path is /root/tp-baggage/budget.jsonl, and write four lines with no header, tab-separated, in budget.tsv in the same directory as the dump — <id>, <넣은 항목 수>, <헤더에 실제로 실린 항목 수>, <헤더 바이트 수>, and <keep|drop> (the placeholders are the id, the number of entries put in, the number of entries actually carried in the header, the header byte count, and keep or drop). The last column is keep if it is within the range (the number of entries and the byte count) for which the W3C Baggage specification guarantees propagation, and drop if it goes beyond. Leave one span named budget for each candidate and attach the attributes baggage.id, baggage.entries, and baggage.bytes.
You make an empty context with opentelemetry.context.Context(). The baggage value of the injected dict is the header string itself, the number of entries is the count when split by commas, and the byte count is the length encoded in UTF-8. The two numbers of the guaranteed range are written in the specification's Limits section. One of the four candidates has a different number of entries put in and entries carried — look at the header yourself to see why.
Put an allowlist on baggage that came from outside
Create /root/tp-baggage/sanitize.py. The default dump path is /root/tp-baggage/sanitize.jsonl. After extracting the baggage from the hostile request in /opt/app/tracelab/tp_baggage/requests.json, keep only the entries whose key is one of tenant, checkout.tier, and region and whose value matches the regular expression ^[a-z0-9][a-z0-9._-]{0,31}$. Build a new context containing only what remains, inject it as a baggage header, and write it in kept.json in the same directory as the dump. Create a SERVER span POST /checkout under that request's parent, put the counts in the attributes baggage.in, baggage.kept, and baggage.dropped, and in the keys attribute of the event baggage.dropped write the dropped keys joined with commas in alphabetical order. The service name is shop-edge.
To see only the incoming baggage, when extracting you must take an empty Context() as the base rather than the current context. Filtering only by key is not enough — this request has a case mixed in where a strange value arrives under an allowed key. You leave an event with span.add_event(이름, {속성}) (the placeholders are the name and the attributes).
Read the tracestate rules from the specification and apply them
Create /root/tp-baggage/tsread.py and judge the six headers in /opt/app/tracelab/tp_baggage/tracestates.json. The default dump path is /root/tp-baggage/tsread.jsonl, and write six lines in tsreport.tsv in the same directory as the dump with <id>, <항목 수>, <헤더 글자 수>, and <ok|bad> separated by tabs (the placeholders are the id, the number of entries, the header character count, and ok or bad). ok means the number of entries is at or below the specification's limit, each key starts with a lowercase letter or digit and uses only lowercase letters, digits, and _, -, *, /, and each value is at or below the specification's character limit and contains no comma or equals sign. In addition, write three lines, max_members=, max_value_chars=, and propagate_min_chars=, in /root/tp-baggage/05-limits.txt with the numbers you read from the specification. Leave one tracestate.read span per entry and attach the attributes ts.id, ts.members, and ts.ok.
The three numbers are written as they are in the tracestate Limits section and the Key and Value sections of the Trace Context specification. propagate_min_chars is not an upper limit but the length vendors must propagate at minimum. An empty header is not an error — the specification says to accept it.
Preserve the incoming tracestate and put our entry at the front
Create /root/tp-baggage/tsmutate.py and turn the same six headers into outgoing headers. There are four rules — (1) our entry is labhub=r1 and always comes at the far left, (2) if our key already exists, delete it and put the new one at the front (it must not appear twice), (3) leave the order of the remaining entries as it is, and (4) if the number of entries exceeds the specification's limit, delete entries over 128 characters first from the back, and if it still exceeds, delete from the end. The default dump path is /root/tp-baggage/tsmutate.jsonl, and write six lines in tsout.tsv in the same directory as the dump with <id> and <나가는 헤더> separated by a tab (the placeholders are the id and the outgoing header). Leave a tracestate.out span per entry and attach the attributes ts.id and ts.out.
The specification says "move a modified key to the left and preserve the order of entries you did not touch." That is why the order of deleting our entry and putting it back at the front matters. When truncating you must drop whole entries, and the Limits section writes which entries the specification names to drop first. Of the six inputs, two are truncated for different reasons.
Harden the rules into a machine-readable file
Gather the values you entered by hand in earlier steps into one place, /root/tp-baggage/policy.json. Under baggage, put allow (an array of allowed keys), value_pattern (a value regular expression), max_entries, and max_bytes, and under tracestate, put key, value, max_members, drop_over_chars, and position. max_entries and max_bytes must be within the range the specification guarantees, max_members and drop_over_chars must equal the specification's numbers, and position is left. Do not put keys that identify a person in allow.
This file is read by the next step's program. The allowlist and regular expression you wrote in step 4 and the key and value of ours you wrote in step 6 just move over here as they are. You may set max_entries and max_bytes below the specification's limits — a limit means "up to here it gets delivered," not "fill up to here."
Apply the rules as they are to a second service
Create /root/tp-baggage/gateway.py, read /root/tp-baggage/policy.json, and process all four requests in /opt/app/tracelab/tp_baggage/requests.json. For each request, filter the baggage by the rules and transform the tracestate by the step 6 rules, then leave one SERVER span gateway under that request's parent and attach the attributes req.name, baggage.kept, baggage.dropped, and tracestate.members. The default dump path is /root/tp-baggage/gateway.jsonl, and write four lines in gateway.tsv in the same directory as the dump with <name>, <남긴 수>, <버린 수>, and <나가는 tracestate 항목 수> separated by tabs (the placeholders are the name, the number kept, the number dropped, and the number of outgoing tracestate entries). The service name is shop-gateway.
Do not write the rules out again as constants; read them from policy.json — that is the point of this step. Of the four requests, one has neither baggage nor tracestate, and one already has a full tracestate. Both must pass without errors.