Skip to content

Commit 7f883ef

Browse files
committed
fixup
1 parent dcb7484 commit 7f883ef

4 files changed

Lines changed: 77 additions & 17 deletions

File tree

py/src/braintrust/logger.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,6 +2478,22 @@ def _braintrust_parent_to_components(braintrust_parent: str):
24782478
return None
24792479

24802480

2481+
def _set_header(carrier: dict, name: str, value: str) -> None:
2482+
"""Set a W3C trace-context header on a carrier, sending the lowercase name.
2483+
2484+
Per the W3C Trace Context spec (§3.2.1 / §3.3.1), vendors SHOULD send these
2485+
header names in lowercase. ``name`` is always the canonical lowercase key.
2486+
A plain ``dict`` carrier is case-sensitive, so any pre-existing case-variant
2487+
(e.g. ``Baggage`` from a framework that title-cases headers) must be removed
2488+
first, otherwise the carrier would end up with two conflicting headers.
2489+
"""
2490+
lowered = name.lower()
2491+
for key in list(carrier.keys()):
2492+
if isinstance(key, str) and key != name and key.lower() == lowered:
2493+
del carrier[key]
2494+
carrier[name] = value
2495+
2496+
24812497
def _inject_into_carrier(
24822498
carrier: dict,
24832499
trace_id: str,
@@ -2496,11 +2512,11 @@ def _inject_into_carrier(
24962512
if traceparent is None:
24972513
# Ids aren't W3C-shaped (e.g. legacy UUID mode); nothing to propagate.
24982514
return
2499-
carrier[TRACEPARENT_HEADER] = traceparent
2515+
_set_header(carrier, TRACEPARENT_HEADER, traceparent)
25002516

25012517
# Forward upstream tracestate (per W3C, only alongside a valid traceparent).
25022518
if tracestate:
2503-
carrier[TRACESTATE_HEADER] = tracestate
2519+
_set_header(carrier, TRACESTATE_HEADER, tracestate)
25042520

25052521
# Merge braintrust.parent into any existing baggage, preserving other keys.
25062522
existing = get_header(carrier, BAGGAGE_HEADER)
@@ -2509,7 +2525,7 @@ def _inject_into_carrier(
25092525
entries[BRAINTRUST_PARENT_KEY] = braintrust_parent
25102526
baggage_value = format_baggage(entries)
25112527
if baggage_value is not None:
2512-
carrier[BAGGAGE_HEADER] = baggage_value
2528+
_set_header(carrier, BAGGAGE_HEADER, baggage_value)
25132529

25142530

25152531
def inject_trace_context(carrier: dict | None = None, span: "Span | None" = None) -> dict:
@@ -2838,9 +2854,13 @@ def start_span(
28382854
# Resolve tracestate from the same source the parent object came from
28392855
# (the explicit `parent` arg, or the ambient current_parent).
28402856
_slug, tracestate = _normalize_parent(parent if parent is not None else state.current_parent.get())
2841-
if parent_obj.row_id and parent_obj.span_id and parent_obj.root_span_id:
2857+
if parent_obj.row_id and _parent_span_ids_match_active_format(parent_obj.span_id, parent_obj.root_span_id):
28422858
parent_span_ids = ParentSpanIds(span_id=parent_obj.span_id, root_span_id=parent_obj.root_span_id)
28432859
else:
2860+
# Either no row id, or the parent slug carries ids in a format that
2861+
# doesn't match the active ID format (e.g. legacy UUID ids while
2862+
# running in the default hex mode). Drop the span linkage and start a
2863+
# fresh root, keeping the object routing from the slug.
28442864
parent_span_ids = None
28452865
return SpanImpl(
28462866
parent_object_type=parent_obj.object_type,

py/src/braintrust/otel/__init__.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -725,8 +725,3 @@ def parent_from_headers(headers: dict[str, str], propagator=None) -> str | None:
725725
)
726726

727727
return components.to_str()
728-
729-
730-
# Alias matching the native propagation naming (`braintrust.extract_trace_context`).
731-
# `parent_from_headers` is retained as the historical name.
732-
extract_trace_context = parent_from_headers

py/src/braintrust/propagation.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,16 @@
88
Trace identity (trace id + parent span id) is carried in ``traceparent``; the
99
Braintrust container the trace belongs to (project/experiment) is carried in
1010
``baggage`` under the ``braintrust.parent`` key.
11-
12-
The deprecated ``x-bt-parent`` header (a serialized span slug) is accepted on
13-
receive for backwards compatibility with older native SDKs, but is never
14-
emitted -- ``traceparent`` takes priority whenever both are present.
1511
"""
1612

1713
import logging
1814
import re
1915

20-
from .http_headers import BT_PARENT
21-
2216

2317
__all__ = [
2418
"TRACEPARENT_HEADER",
2519
"TRACESTATE_HEADER",
2620
"BAGGAGE_HEADER",
27-
"BT_PARENT_HEADER",
2821
"BRAINTRUST_PARENT_KEY",
2922
"parse_traceparent",
3023
"format_traceparent",
@@ -38,7 +31,6 @@
3831
TRACEPARENT_HEADER = "traceparent"
3932
TRACESTATE_HEADER = "tracestate"
4033
BAGGAGE_HEADER = "baggage"
41-
BT_PARENT_HEADER = BT_PARENT # "x-bt-parent"
4234
BRAINTRUST_PARENT_KEY = "braintrust.parent"
4335

4436
# W3C traceparent: version-traceid-parentid-flags, version 00, lowercase hex.

py/src/braintrust/test_propagation.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,33 @@ def test_preexisting_baggage_preserved(self, memory_and_logger):
154154
assert parsed["team"] == "eng"
155155
assert parsed[BRAINTRUST_PARENT_KEY] == "project_name:propagation-test"
156156

157+
def test_title_cased_baggage_emits_single_lowercase_header(self, memory_and_logger):
158+
# Per W3C (§3.3.1) the header name SHOULD be sent lowercase. A carrier
159+
# that arrives with a title-cased `Baggage` (e.g. from a framework that
160+
# normalizes header casing) must be rewritten to a single lowercase
161+
# `baggage` key, not left with two conflicting case-variants.
162+
_mem, logger = memory_and_logger
163+
with logger.start_span(name="svc_a") as span:
164+
carrier = span.inject({"Baggage": "user=alice"})
165+
166+
baggage_keys = [k for k in carrier if k.lower() == BAGGAGE_HEADER]
167+
assert baggage_keys == [BAGGAGE_HEADER]
168+
parsed = parse_baggage(carrier[BAGGAGE_HEADER])
169+
assert parsed["user"] == "alice"
170+
assert parsed[BRAINTRUST_PARENT_KEY] == "project_name:propagation-test"
171+
172+
def test_title_cased_traceparent_emits_single_lowercase_header(self, memory_and_logger):
173+
# A pre-existing title-cased `Traceparent` must be replaced by a single
174+
# lowercase `traceparent` (W3C §3.2.1), with no stale variant remaining.
175+
_mem, logger = memory_and_logger
176+
with logger.start_span(name="svc_a") as span:
177+
carrier = span.inject({"Traceparent": "stale"})
178+
179+
traceparent_keys = [k for k in carrier if k.lower() == TRACEPARENT_HEADER]
180+
assert traceparent_keys == [TRACEPARENT_HEADER]
181+
tp = parse_traceparent(carrier[TRACEPARENT_HEADER])
182+
assert tp == (span.root_span_id, span.span_id)
183+
157184
def test_never_emits_x_bt_parent(self, memory_and_logger):
158185
_mem, logger = memory_and_logger
159186
with logger.start_span(name="svc_a") as span:
@@ -371,6 +398,32 @@ def test_legacy_parent_slug_ignored_in_hex_mode():
371398
assert not child.span_parents
372399

373400

401+
def test_legacy_parent_slug_ignored_in_hex_mode_toplevel_start_span():
402+
# Same as above, but through the module-level `start_span`, which resolves
403+
# the parent slug independently of `Logger.start_span` and must apply the
404+
# same format-mismatch guard.
405+
import uuid
406+
407+
from braintrust.logger import start_span
408+
from braintrust.span_identifier_v3 import SpanComponentsV3
409+
410+
legacy_slug = SpanComponentsV3(
411+
object_type=SpanObjectTypeV3.PROJECT_LOGS,
412+
object_id="legacy-proj",
413+
row_id=str(uuid.uuid4()),
414+
span_id=str(uuid.uuid4()),
415+
root_span_id=str(uuid.uuid4()),
416+
).to_str()
417+
418+
with _internal_with_memory_background_logger():
419+
init_test_logger("legacy-proj")
420+
with start_span(name="child", parent=legacy_slug) as child:
421+
# Fresh hex root: hex-shaped ids, no parent linkage.
422+
assert len(child.span_id) == 16
423+
assert len(child.root_span_id) == 32
424+
assert not child.span_parents
425+
426+
374427
# --------------------------------------------------------------------------- #
375428
# tracestate pass-through (W3C: forward upstream vendor state)
376429
# --------------------------------------------------------------------------- #

0 commit comments

Comments
 (0)