diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 1cf6ad217ff..55912015fe1 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -1995,6 +1995,13 @@ "default": "false" } ], + "DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT": [ + { + "implementation": "B", + "type": "string", + "default": "continue" + } + ], "DD_TRACE_PROPAGATION_STYLE": [ { "implementation": "D", diff --git a/tests/ext/distributed_tracing/propagation_behavior_extract_config.phpt b/tests/ext/distributed_tracing/propagation_behavior_extract_config.phpt new file mode 100644 index 00000000000..00d1431dff4 --- /dev/null +++ b/tests/ext/distributed_tracing/propagation_behavior_extract_config.phpt @@ -0,0 +1,45 @@ +--TEST-- +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT config parsing: case-insensitive, invalid falls back to continue +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +--FILE-- + 42, + "x-datadog-parent-id" => 10, + ]); + + $span = DDTrace\start_span(); + $result = DDTrace\root_span()->traceId === "0000000000000000000000000000002a" ? "continue" : "restart_or_ignore"; + DDTrace\close_span(); + + return $result; +} + +// Lowercase values +echo "continue: " . check_behavior("continue") . "\n"; +echo "restart: " . check_behavior("restart") . "\n"; +echo "ignore: " . check_behavior("ignore") . "\n"; + +// Case-insensitive +echo "CONTINUE: " . check_behavior("CONTINUE") . "\n"; +echo "RESTART: " . check_behavior("RESTART") . "\n"; +echo "Ignore: " . check_behavior("Ignore") . "\n"; + +// Invalid value falls back to default (continue) +echo "invalid: " . check_behavior("invalid_value") . "\n"; + +?> +--EXPECT-- +continue: continue +restart: restart_or_ignore +ignore: restart_or_ignore +CONTINUE: continue +RESTART: restart_or_ignore +Ignore: restart_or_ignore +invalid: continue diff --git a/tests/ext/distributed_tracing/propagation_behavior_extract_continue.phpt b/tests/ext/distributed_tracing/propagation_behavior_extract_continue.phpt new file mode 100644 index 00000000000..3427183934e --- /dev/null +++ b/tests/ext/distributed_tracing/propagation_behavior_extract_continue.phpt @@ -0,0 +1,32 @@ +--TEST-- +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue inherits upstream context +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=continue +--FILE-- + 42, + "x-datadog-parent-id" => 10, + "x-datadog-sampling-priority" => 1, + "baggage" => "user.id=123", +]); + +$span = DDTrace\start_span(); +$root = DDTrace\root_span(); + +echo "trace_id: " . $root->traceId . "\n"; +echo "parent_id: " . $root->parentId . "\n"; +echo "links_count: " . count($root->links) . "\n"; + +$headers = DDTrace\generate_distributed_tracing_headers(['baggage']); +echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n"; + +DDTrace\close_span(); +?> +--EXPECT-- +trace_id: 0000000000000000000000000000002a +parent_id: 10 +links_count: 0 +baggage: user.id=123 diff --git a/tests/ext/distributed_tracing/propagation_behavior_extract_ignore.phpt b/tests/ext/distributed_tracing/propagation_behavior_extract_ignore.phpt new file mode 100644 index 00000000000..5b57cea6a1f --- /dev/null +++ b/tests/ext/distributed_tracing/propagation_behavior_extract_ignore.phpt @@ -0,0 +1,42 @@ +--TEST-- +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore drops all incoming context including baggage +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=ignore +--FILE-- + 42, + "x-datadog-parent-id" => 10, + "x-datadog-sampling-priority" => 2, + "x-datadog-tags" => "_dd.p.dm=-4", + "baggage" => "user.id=123", +]); + +$span = DDTrace\start_span(); +$root = DDTrace\root_span(); + +// fresh trace: not from upstream +echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n"; +echo "parent_id: " . $root->parentId . "\n"; + +// no span link (context discarded entirely) +echo "links_count: " . count($root->links) . "\n"; + +// baggage dropped +$headers = DDTrace\generate_distributed_tracing_headers(['baggage']); +echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n"; + +// upstream sampling priority not carried over +$dd_headers = DDTrace\generate_distributed_tracing_headers(['datadog']); +echo "sampling_priority: " . ($dd_headers['x-datadog-sampling-priority'] ?? 'none') . "\n"; + +DDTrace\close_span(); +?> +--EXPECT-- +same_as_upstream: no +parent_id: 0 +links_count: 0 +baggage: none +sampling_priority: none diff --git a/tests/ext/distributed_tracing/propagation_behavior_extract_restart.phpt b/tests/ext/distributed_tracing/propagation_behavior_extract_restart.phpt new file mode 100644 index 00000000000..f25c0140b6a --- /dev/null +++ b/tests/ext/distributed_tracing/propagation_behavior_extract_restart.phpt @@ -0,0 +1,67 @@ +--TEST-- +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart starts fresh trace with span link +--ENV-- +DD_TRACE_GENERATE_ROOT_SPAN=0 +DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT=restart +DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED=0 +DD_TRACE_DEBUG_PRNG_SEED=42 +--FILE-- + 42, + "x-datadog-parent-id" => 10, + "x-datadog-sampling-priority" => 1, + "x-datadog-tags" => "_dd.p.foo=bar", + "baggage" => "user.id=123", +]); + +$span = DDTrace\start_span(); +$root = DDTrace\root_span(); + +// fresh trace: different from upstream trace_id 42 +echo "same_as_upstream: " . ($root->traceId === "0000000000000000000000000000002a" ? "yes" : "no") . "\n"; + +// span link attached to root span +echo "links_count: " . count($root->links) . "\n"; + +$link = $root->links[0] ?? null; +if ($link !== null) { + // link captures upstream trace/span ids + echo "link_trace_id: " . $link->traceId . "\n"; + echo "link_span_id: " . $link->spanId . "\n"; + echo "link_reason: " . ($link->attributes['reason'] ?? 'missing') . "\n"; + echo "link_context_headers: " . ($link->attributes['context_headers'] ?? 'missing') . "\n"; + // _dd.p.foo captured in link attributes (upstream propagation context preserved in link) + echo "link_has_foo: " . (isset($link->attributes['_dd.p.foo']) ? "yes" : "no") . "\n"; +} + +$tid = $root->traceId; +echo "trace_id_valid: " . (preg_match('/^[0-9a-f]{32}$/', $tid) && $tid !== "00000000000000000000000000000000" ? "yes" : "no") . "\n"; + +// baggage preserved +$headers = DDTrace\generate_distributed_tracing_headers(['baggage']); +echo "baggage: " . ($headers['baggage'] ?? 'none') . "\n"; + +// upstream _dd.p.foo not in outbound tags +$dd_headers = DDTrace\generate_distributed_tracing_headers(['datadog']); +$tags = $dd_headers['x-datadog-tags'] ?? ''; +echo "foo_in_tags: " . (str_contains($tags, '_dd.p.foo') ? "yes" : "no") . "\n"; + +// upstream sampling priority must not leak into the fresh trace +echo "sampling_priority: " . ($dd_headers['x-datadog-sampling-priority'] ?? 'none') . "\n"; + +DDTrace\close_span(); +?> +--EXPECT-- +same_as_upstream: no +links_count: 1 +link_trace_id: 0000000000000000000000000000002a +link_span_id: 000000000000000a +link_reason: propagation_behavior_extract +link_context_headers: datadog +link_has_foo: yes +trace_id_valid: yes +baggage: user.id=123 +foo_in_tags: no +sampling_priority: none diff --git a/tracer/configuration.h b/tracer/configuration.h index 45790f6a649..ab888552af3 100644 --- a/tracer/configuration.h +++ b/tracer/configuration.h @@ -99,6 +99,7 @@ CONFIG(SET_LOWERCASE, DD_TRACE_PROPAGATION_STYLE, "datadog,tracecontext,baggage", \ .env_config_fallback = ddtrace_conf_otel_propagators) \ CONFIG(SET, DD_TRACE_BAGGAGE_TAG_KEYS, "user.id, session.id, account.id") \ + CONFIG(CUSTOM(INT), DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT, "continue", .parser = dd_parse_propagation_behavior_extract) \ CONFIG(BOOL, DD_TRACE_IGNORE_AGENT_SAMPLING_RATES, "false", .ini_change = zai_config_system_ini_change) \ CONFIG(SET, DD_TRACE_TRACED_INTERNAL_FUNCTIONS, "") \ CONFIG(INT, DD_TRACE_DEBUG_PRNG_SEED, "-1", .ini_change = ddtrace_reseed_seed_change) \ @@ -192,6 +193,12 @@ enum ddtrace_sampling_rules_format { DD_TRACE_SAMPLING_RULES_FORMAT_GLOB }; +enum ddtrace_propagation_behavior_extract { + DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_CONTINUE = 0, + DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_RESTART, + DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_IGNORE, +}; + #define DD_CONFIGURATION DDTRACE_CONFIGURATION #include diff --git a/tracer/configuration_dependencies.h b/tracer/configuration_dependencies.h index 70ad64dc081..c47a31ea744 100644 --- a/tracer/configuration_dependencies.h +++ b/tracer/configuration_dependencies.h @@ -22,6 +22,21 @@ static bool dd_parse_dbm_mode(zai_str value, zval *decoded_value, bool persisten return true; } +static bool dd_parse_propagation_behavior_extract(zai_str value, zval *decoded_value, bool persistent) { + UNUSED(persistent); + if (zai_str_eq_ci_cstr(value, "continue")) { + ZVAL_LONG(decoded_value, DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_CONTINUE); + } else if (zai_str_eq_ci_cstr(value, "restart")) { + ZVAL_LONG(decoded_value, DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_RESTART); + } else if (zai_str_eq_ci_cstr(value, "ignore")) { + ZVAL_LONG(decoded_value, DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_IGNORE); + } else { + return false; + } + + return true; +} + static bool dd_parse_sampling_rules_format(zai_str value, zval *decoded_value, bool persistent) { UNUSED(persistent); if (zai_str_eq_ci_cstr(value, "regex")) { diff --git a/tracer/ddtrace.c b/tracer/ddtrace.c index 7f5e9681d92..60181d437f2 100644 --- a/tracer/ddtrace.c +++ b/tracer/ddtrace.c @@ -436,6 +436,7 @@ static void dd_initialize_request(void) { DDTRACE_G(default_priority_sampling) = DDTRACE_PRIORITY_SAMPLING_UNKNOWN; DDTRACE_G(propagated_priority_sampling) = DDTRACE_PRIORITY_SAMPLING_UNSET; DDTRACE_G(inferred_span_created) = false; + ZVAL_NULL(&DDTRACE_G(pending_upstream_span_link)); zend_hash_init(&DDTRACE_G(root_span_tags_preset), 8, unused, ZVAL_PTR_DTOR, 0); zend_hash_init(&DDTRACE_G(propagated_root_span_tags), 8, unused, ZVAL_PTR_DTOR, 0); zend_hash_init(&DDTRACE_G(tracestate_unknown_dd_keys), 8, unused, ZVAL_PTR_DTOR, 0); @@ -527,6 +528,8 @@ static void dd_clean_globals(void) { zend_hash_destroy(&DDTRACE_G(tracestate_unknown_dd_keys)); zend_hash_destroy(&DDTRACE_G(propagated_root_span_tags)); zend_hash_destroy(&DDTRACE_G(baggage)); + zval_ptr_dtor(&DDTRACE_G(pending_upstream_span_link)); + ZVAL_NULL(&DDTRACE_G(pending_upstream_span_link)); if (DDTRACE_G(curl_multi_injecting_spans)) { if (GC_DELREF(DDTRACE_G(curl_multi_injecting_spans)) == 0) { diff --git a/tracer/ddtrace_globals.h b/tracer/ddtrace_globals.h index 3865b2e48f9..1bfc1a9ad6d 100644 --- a/tracer/ddtrace_globals.h +++ b/tracer/ddtrace_globals.h @@ -89,6 +89,7 @@ typedef struct { zend_object *git_object; bool inferred_span_created; + zval pending_upstream_span_link; // span link queued by PROPAGATION_BEHAVIOR_EXTRACT=restart; consumed on root span open HashTable resource_weak_storage; dtor_func_t resource_dtor_func; diff --git a/tracer/distributed_tracing_headers.c b/tracer/distributed_tracing_headers.c index 78d02939b37..79a62ed8c85 100644 --- a/tracer/distributed_tracing_headers.c +++ b/tracer/distributed_tracing_headers.c @@ -519,6 +519,9 @@ ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids(ddtrace_ } result = func(read_header, data); + if (result.trace_id.low || result.trace_id.high) { + result.context_headers = zend_string_copy(extraction_style); + } // As an exception, the x-datadog-origin can be submitted standalone, without valid trace id if (existing_origin) { @@ -617,6 +620,56 @@ void apply_baggage_span_tags(zend_string *key, zval *val, zend_array *meta) { void ddtrace_apply_distributed_tracing_result(ddtrace_distributed_tracing_result *result, ddtrace_root_span_data *span) { zval zv; + switch (get_DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT()) { + // behavior=ignore: drop all extracted context including baggage + case DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_IGNORE: + zend_hash_destroy(&result->propagated_tags); + zend_hash_destroy(&result->meta_tags); + zend_hash_destroy(&result->tracestate_unknown_dd_keys); + zend_hash_destroy(&result->baggage); + if (result->origin) { zend_string_release(result->origin); } + if (result->tracestate) { zend_string_release(result->tracestate); } + if (result->context_headers) { zend_string_release(result->context_headers); } + return; + // behavior=restart: start fresh trace; upstream captured as span link, baggage preserved + case DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_RESTART: + if (result->trace_id.low || result->trace_id.high) { + zval link_zv; + object_init_ex(&link_zv, ddtrace_ce_span_link); + ddtrace_span_link *link = (ddtrace_span_link *)Z_OBJ(link_zv); + ddtrace_build_span_link_from_result(result, link); + + result->trace_id = (datadog_trace_id){0}; + result->parent_id = 0; + result->priority_sampling = DDTRACE_PRIORITY_SAMPLING_UNKNOWN; + + zend_hash_clean(&result->meta_tags); + zend_hash_clean(&result->propagated_tags); + + zval reason_str; + ZVAL_STR(&reason_str, zend_string_init(ZEND_STRL("propagation_behavior_extract"), 0)); + zend_hash_str_update(Z_ARR(link->property_attributes), ZEND_STRL("reason"), &reason_str); + + if (result->context_headers) { + zval context_headers_zv; + ZVAL_STR(&context_headers_zv, zend_string_copy(result->context_headers)); + zend_hash_str_update(Z_ARR(link->property_attributes), ZEND_STRL("context_headers"), &context_headers_zv); + } + + if (span) { + zend_array *links = ddtrace_property_array(&span->property_links); + zend_hash_next_index_insert(links, &link_zv); + } else { + zval_ptr_dtor(&DDTRACE_G(pending_upstream_span_link)); + ZVAL_COPY_VALUE(&DDTRACE_G(pending_upstream_span_link), &link_zv); + } + } + break; + // behavior=continue: inherit upstream context normally + case DD_TRACE_PROPAGATION_BEHAVIOR_EXTRACT_CONTINUE: + break; + } + zend_array *root_meta = span ? ddtrace_property_array(&span->property_meta) : &DDTRACE_G(root_span_tags_preset); if (span) { zend_string *tagname; @@ -728,6 +781,8 @@ void ddtrace_apply_distributed_tracing_result(ddtrace_distributed_tracing_result ddtrace_set_priority_sampling_on_span(span, result->priority_sampling, DD_MECHANISM_DEFAULT); } } + + if (result->context_headers) { zend_string_release(result->context_headers); } } bool ddtrace_read_zai_header(zai_str zai_header, const char *lowercase_header, zend_string **header_value, void *data) { diff --git a/tracer/distributed_tracing_headers.h b/tracer/distributed_tracing_headers.h index 9956444431b..61645780e87 100644 --- a/tracer/distributed_tracing_headers.h +++ b/tracer/distributed_tracing_headers.h @@ -17,6 +17,7 @@ typedef struct { int priority_sampling; enum dd_sampling_mechanism sampling_mechanism; bool conflicting_sampling_priority; // propagated priority does not match tracestate priority + zend_string *context_headers; // name of the extractor that produced the trace context (e.g. "datadog", "tracecontext") } ddtrace_distributed_tracing_result; typedef bool (ddtrace_read_header)(zai_str zai_header, const char *lowercase_header, zend_string **header_value, void *data); @@ -24,5 +25,6 @@ ddtrace_distributed_tracing_result ddtrace_read_distributed_tracing_ids(ddtrace_ void ddtrace_apply_distributed_tracing_result(ddtrace_distributed_tracing_result *result, ddtrace_root_span_data *span); bool ddtrace_read_zai_header(zai_str zai_header, const char *lowercase_header, zend_string **header_value, void *data); bool ddtrace_read_array_header(zai_str zai_header, const char *lowercase_header, zend_string **header_value, void *data); +void ddtrace_build_span_link_from_result(ddtrace_distributed_tracing_result *result, ddtrace_span_link *link); #endif // DD_DISTRIBUTED_TRACING_HEADERS_H diff --git a/tracer/functions.c b/tracer/functions.c index aca31566a7a..56f96e96797 100644 --- a/tracer/functions.c +++ b/tracer/functions.c @@ -231,6 +231,25 @@ PHP_METHOD(DDTrace_SpanLink, jsonSerialize) { RETURN_ARR(array); } +void ddtrace_build_span_link_from_result(ddtrace_distributed_tracing_result *result, ddtrace_span_link *link) { + ZVAL_STR(&link->property_trace_id, datadog_trace_id_as_hex_string(result->trace_id)); + ZVAL_STR(&link->property_span_id, ddtrace_span_id_as_hex_string(result->parent_id)); + array_init(&link->property_attributes); + // The span link owns its own references to the copied values, independent of the + // result->meta_tags lifetime: callers may keep meta_tags alive and hand it to other + // consumers (e.g. the root span's meta) or destroy it right after. + zend_hash_copy(Z_ARR(link->property_attributes), &result->meta_tags, (copy_ctor_func_t)zval_add_ref); + + zend_string *propagated_tags = ddtrace_format_propagated_tags(&result->propagated_tags, &result->meta_tags); + zend_string *full_tracestate = ddtrace_format_tracestate(result->tracestate, 0, result->origin, result->priority_sampling, propagated_tags, &result->tracestate_unknown_dd_keys); + if (propagated_tags) { + zend_string_release(propagated_tags); + } + if (full_tracestate) { + ZVAL_STR(&link->property_trace_state, full_tracestate); + } +} + static ddtrace_distributed_tracing_result dd_parse_distributed_tracing_headers_function(INTERNAL_FUNCTION_PARAMETERS, bool *success); ZEND_METHOD(DDTrace_SpanLink, fromHeaders) { bool success; @@ -245,21 +264,10 @@ ZEND_METHOD(DDTrace_SpanLink, fromHeaders) { return; } - ZVAL_STR(&link->property_trace_id, datadog_trace_id_as_hex_string(result.trace_id)); - ZVAL_STR(&link->property_span_id, ddtrace_span_id_as_hex_string(result.parent_id)); - array_init(&link->property_attributes); - zend_hash_copy(Z_ARR(link->property_attributes), &result.meta_tags, NULL); - - zend_string *propagated_tags = ddtrace_format_propagated_tags(&result.propagated_tags, &result.meta_tags); - zend_string *full_tracestate = ddtrace_format_tracestate(result.tracestate, 0, result.origin, result.priority_sampling, propagated_tags, &result.tracestate_unknown_dd_keys); - if (propagated_tags) { - zend_string_release(propagated_tags); - } - if (full_tracestate) { - ZVAL_STR(&link->property_trace_state, full_tracestate); - } + ddtrace_build_span_link_from_result(&result, link); - result.meta_tags.pDestructor = NULL; // we moved values directly + // The span link took its own references (zval_add_ref) on the copied values, so destroy + // meta_tags normally to release the references it still owns. zend_hash_destroy(&result.meta_tags); zend_hash_destroy(&result.propagated_tags); zend_hash_destroy(&result.tracestate_unknown_dd_keys); diff --git a/tracer/span.c b/tracer/span.c index a744e020cc2..c0becb2be7f 100644 --- a/tracer/span.c +++ b/tracer/span.c @@ -296,6 +296,13 @@ ddtrace_span_data *ddtrace_open_span(enum ddtrace_span_dataype type) { span->parent = NULL; ddtrace_set_root_span_properties(root); + + if (primary_stack && Z_TYPE(DDTRACE_G(pending_upstream_span_link)) == IS_OBJECT) { + // attach upstream link queued by PROPAGATION_BEHAVIOR_EXTRACT=restart + zend_array *links = ddtrace_property_array(&span->property_links); + zend_hash_next_index_insert(links, &DDTRACE_G(pending_upstream_span_link)); + ZVAL_NULL(&DDTRACE_G(pending_upstream_span_link)); + } } else { ++parent_span->active_child_spans;