diff --git a/ddtrace/internal/constants.py b/ddtrace/internal/constants.py index 2cbd3d91161..c7c67811006 100644 --- a/ddtrace/internal/constants.py +++ b/ddtrace/internal/constants.py @@ -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 diff --git a/ddtrace/internal/opentelemetry/sampling.py b/ddtrace/internal/opentelemetry/sampling.py index 172adb3d92b..9f8fe5caad5 100644 --- a/ddtrace/internal/opentelemetry/sampling.py +++ b/ddtrace/internal/opentelemetry/sampling.py @@ -4,6 +4,7 @@ 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 @@ -11,7 +12,6 @@ _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}$") @@ -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) diff --git a/ddtrace/internal/utils/http.py b/ddtrace/internal/utils/http.py index 2fb6ce190ef..05d6d6f391e 100644 --- a/ddtrace/internal/utils/http.py +++ b/ddtrace/internal/utils/http.py @@ -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 @@ -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 = [] @@ -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 @@ -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) + 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) diff --git a/releasenotes/notes/fix-w3c-tracestate-dd-member-length-3f1c9a7e5d2b4c60.yaml b/releasenotes/notes/fix-w3c-tracestate-dd-member-length-3f1c9a7e5d2b4c60.yaml new file mode 100644 index 00000000000..1b19473b962 --- /dev/null +++ b/releasenotes/notes/fix-w3c-tracestate-dd-member-length-3f1c9a7e5d2b4c60.yaml @@ -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. diff --git a/tests/tracer/test_propagation.py b/tests/tracer/test_propagation.py index 48c07bab993..cc6a967c613 100644 --- a/tests/tracer/test_propagation.py +++ b/tests/tracer/test_propagation.py @@ -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")