Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ddtrace/internal/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ def __getitem__(self, k: str) -> Any:
DD_TRACE_TRACESTATE_MAX_BYTES = 512
# Per W3C Trace Context, oversized list-members are preferred targets when truncating by size.
DD_TRACE_TRACESTATE_ITEM_MAX_CHARS = 128
# W3C limits each list-member value to 256 characters.
W3C_DD_LIST_MEMBER_MAX_CHARS = 256

SPAN_EVENTS_HAS_EXCEPTION = "_dd.span_events.has_exception"
COLLECTOR_MAX_SIZE_PER_SPAN = 100
Expand Down
4 changes: 2 additions & 2 deletions ddtrace/internal/opentelemetry/sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

from ddtrace.internal.constants import MAX_UINT_64BITS
from ddtrace.internal.constants import SAMPLING_KNUTH_FACTOR
from ddtrace.internal.constants import W3C_DD_LIST_MEMBER_MAX_CHARS
from ddtrace.internal.constants import W3C_TRACESTATE_KEY
from ddtrace.internal.utils.http import w3c_get_tracestate_list_member
from ddtrace.internal.utils.http import w3c_update_tracestate_list_member


_MAX_THRESHOLD = 1 << 56
_MAX_ENCODABLE_THRESHOLD = _MAX_THRESHOLD - 1
_MAX_OTEL_TRACESTATE_VALUE_CHARS = 256
_VALID_RANDOM_VALUE = re.compile(r"^[0-9a-f]{14}$")
_VALID_THRESHOLD = re.compile(r"^[0-9a-f]{1,14}$")

Expand Down Expand Up @@ -74,7 +74,7 @@ def _build_otel_member(random_value: Optional[str], threshold: Optional[str], un
value_chars = 0
for field in candidate_fields:
field_chars = len(field) + (1 if fields else 0)
if value_chars + field_chars <= _MAX_OTEL_TRACESTATE_VALUE_CHARS:
if value_chars + field_chars <= W3C_DD_LIST_MEMBER_MAX_CHARS:
fields.append(field)
value_chars += field_chars
return ";".join(fields)
Expand Down
14 changes: 9 additions & 5 deletions ddtrace/internal/utils/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from ddtrace.internal.constants import DD_TRACE_TRACESTATE_MAX_ITEMS
from ddtrace.internal.constants import DEFAULT_TIMEOUT
from ddtrace.internal.constants import SAMPLING_DECISION_TRACE_TAG_KEY
from ddtrace.internal.constants import W3C_DD_LIST_MEMBER_MAX_CHARS
from ddtrace.internal.constants import W3C_TRACESTATE_ORIGIN_KEY
from ddtrace.internal.constants import W3C_TRACESTATE_PARENT_ID_KEY
from ddtrace.internal.constants import W3C_TRACESTATE_SAMPLING_PRIORITY_KEY
Expand Down Expand Up @@ -188,6 +189,9 @@ def _connector_context() -> Generator[HTTPConnection, None, None]:
return _connector_context


_W3C_DD_LIST_MEMBER_RESERVED_LEN = len("dd=") + len(f"{W3C_TRACESTATE_PARENT_ID_KEY}:{0:016x};")


def w3c_get_dd_list_member(context):
# Context -> str
tags = []
Expand All @@ -211,7 +215,7 @@ def w3c_get_dd_list_member(context):
if usr_id:
tags.append("t.usr.id:{}".format(w3c_encode_tag((_W3C_TRACESTATE_INVALID_CHARS_REGEX_VALUE, "_", usr_id))))

current_tags_len = sum(len(i) for i in tags)
current_tags_len = _W3C_DD_LIST_MEMBER_RESERVED_LEN + len(";".join(tags))
for k, v in _get_metas_to_propagate(context):
if k not in [SAMPLING_DECISION_TRACE_TAG_KEY, _USER_ID_KEY]:
# for key replace ",", "=", and characters outside the ASCII range 0x20 to 0x7E
Expand All @@ -221,11 +225,11 @@ def w3c_get_dd_list_member(context):
w3c_encode_tag((_W3C_TRACESTATE_INVALID_CHARS_REGEX_KEY, "_", k)),
w3c_encode_tag((_W3C_TRACESTATE_INVALID_CHARS_REGEX_VALUE, "_", v)),
)
# we need to keep the total length under 256 char
potential_current_tags_len = current_tags_len + len(next_tag)
if not potential_current_tags_len > 256:
# account for ; before next tag entry
next_tag_len = len(next_tag) + (1 if tags else 0)
Comment thread
julinvictus marked this conversation as resolved.
if current_tags_len + next_tag_len <= W3C_DD_LIST_MEMBER_MAX_CHARS:
tags.append(next_tag)
current_tags_len += len(next_tag)
current_tags_len += next_tag_len
else:
log.debug("tracestate would exceed 256 char limit with tag: %s. Tag will not be added.", next_tag)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
fixes:
- |
tracing: Fixes an issue where the ``dd=`` list-member of the W3C ``tracestate`` header could exceed
256 characters, causing proxies that enforce the W3C limit to drop the whole ``tracestate`` header.
32 changes: 32 additions & 0 deletions tests/tracer/test_propagation.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,38 @@ def test_inject_deep_child_propagates_trace_level_tags(tracer): # noqa: F811
assert "_dd.p.other=value" in tags


def test_inject_tracestate_dd_member_within_256_chars(tracer): # noqa: F811
"""The injected dd= list-member, including the "dd=" prefix, ";" separators and the
"p:" field added at injection, must not exceed 256 characters.
"""
meta = {
"_dd.p.llmobs_parent_id": "10707708093473052057",
"_dd.p.llmobs_trace_id": "141883241939442010658227520463581072062",
"_dd.p.llmobs_ml_app": "datasciencetiger",
"_dd.p.llmobs_sid": "11a0eec0-ad0a-4f4d-8ae8-7f358ed1b153",
"_dd.p.llmobs_sr": "1",
"_dd.p.llmobs_sd": "1",
"_dd.p.llmobs_pagent_span_id": "14260131734498464268",
"_dd.p.llmobs_pagent_name": "datasciencetiger-orchestrator",
}
ctx = Context(trace_id=1234, sampling_priority=1, dd_origin="rum", meta=meta)
tracer.context_provider.activate(ctx)
with tracer.trace("global_root_span") as span:
headers = {}
HTTPPropagator.inject(span.context, headers)

dd_members = [m for m in headers[_HTTP_HEADER_TRACESTATE].split(",") if m.startswith("dd=")]
assert len(dd_members) == 1
dd_member = dd_members[0]
assert len(dd_member) <= 256, dd_member
assert dd_member.startswith(f"dd=p:{span.span_id:016x};s:1;o:rum;")
# Only whole tags are dropped, never truncated
for tag in dd_member[len("dd=") :].split(";"):
if tag.startswith("t.llmobs_"):
key, _, value = tag.partition(":")
assert meta[key.replace("t.", "_dd.p.", 1)] == value


def test_inject_with_baggage_http_propagation(tracer): # noqa: F811
with override_global_config(dict(_propagation_http_baggage_enabled=True)):
ctx = Context(trace_id=1234, sampling_priority=2, dd_origin="synthetics")
Expand Down
Loading