From 585a23a7785923aa549d084db7c7ed2d23b2765d Mon Sep 17 00:00:00 2001 From: Alexandre Choura Date: Tue, 9 Dec 2025 13:41:47 +0100 Subject: [PATCH 1/2] fix(guzzle): inject distributed tracing headers before request execution Use install_hook with pre-hook to inject headers into PSR-7 request before it reaches any handler (curl or CoroutineHandler). This ensures headers are propagated in HyperF/Swoole environments where CoroutineHandler reads headers directly from the request object. Fixes distributed tracing continuity for Guzzle in coroutine contexts. --- .../Integrations/Guzzle/GuzzleIntegration.php | 86 ++++++++++++------- 1 file changed, 56 insertions(+), 30 deletions(-) diff --git a/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php b/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php index 664027a05b..f75f7479f4 100644 --- a/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php +++ b/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php @@ -2,6 +2,7 @@ namespace DDTrace\Integrations\Guzzle; +use DDTrace\HookData; use DDTrace\Http\Urls; use DDTrace\Integrations\HttpClientIntegrationHelper; use DDTrace\Integrations\Integration; @@ -33,10 +34,61 @@ public static function handlePromiseResponse($response, SpanData $span) public static function init(): int { - /* Until we support both pre- and post- hooks on the same function, do - * not send distributed tracing headers; curl will almost guaranteed do - * it for us anyway. Just do a post-hook to get the response. - */ + \DDTrace\install_hook( + 'GuzzleHttp\Client::transfer', + static function (HookData $hook) { + if (!isset($hook->args[0])) { + return; + } + + $request = $hook->args[0]; + + if (!($request instanceof \Psr\Http\Message\RequestInterface)) { + return; + } + + $dtHeaders = \DDTrace\generate_distributed_tracing_headers(); + + if (empty($dtHeaders)) { + return; + } + + foreach ($dtHeaders as $name => $value) { + if (!$request->hasHeader($name)) { + $request = $request->withHeader($name, $value); + } + } + + $hook->args[0] = $request; + $hook->overrideArguments($hook->args); + }, + static function (HookData $hook) { + $span = $hook->span(); + if (!$span) { + return; + } + + $span->resource = 'transfer'; + $span->name = 'GuzzleHttp\Client.transfer'; + Integration::handleInternalSpanServiceName($span, self::NAME); + $span->type = Type::HTTP_CLIENT; + $span->meta[Tag::SPAN_KIND] = Tag::SPAN_KIND_VALUE_CLIENT; + $span->meta[Tag::COMPONENT] = self::NAME; + $span->peerServiceSources = HttpClientIntegrationHelper::PEER_SERVICE_SOURCES; + + if (isset($hook->args[0])) { + self::addRequestInfo($span, $hook->args[0]); + } + + if (isset($hook->returned)) { + $response = $hook->returned; + if (\is_a($response, 'GuzzleHttp\Promise\PromiseInterface')) { + self::handlePromiseResponse($response, $span); + } + } + } + ); + \DDTrace\trace_method( 'GuzzleHttp\Client', 'send', @@ -52,8 +104,6 @@ static function (SpanData $span, $args, $retval) { \defined('GuzzleHttp\ClientInterface::VERSION') && substr(\GuzzleHttp\ClientInterface::VERSION, 0, 2) === '5.' ) { - // On Guzzle 6+, we do not need to generate peer.service for the send span, - // as the terminal span is 'transfer' $span->peerServiceSources = HttpClientIntegrationHelper::PEER_SERVICE_SOURCES; } @@ -80,30 +130,6 @@ static function (SpanData $span, $args, $retval) { } ); - \DDTrace\trace_method( - 'GuzzleHttp\Client', - 'transfer', - static function (SpanData $span, $args, $retval) { - $span->resource = 'transfer'; - $span->name = 'GuzzleHttp\Client.transfer'; - Integration::handleInternalSpanServiceName($span, self::NAME); - $span->type = Type::HTTP_CLIENT; - $span->meta[Tag::SPAN_KIND] = Tag::SPAN_KIND_VALUE_CLIENT; - $span->meta[Tag::COMPONENT] = self::NAME; - $span->peerServiceSources = HttpClientIntegrationHelper::PEER_SERVICE_SOURCES; - - if (isset($args[0])) { - self::addRequestInfo($span, $args[0]); - } - if (isset($retval)) { - $response = $retval; - if (\is_a($response, 'GuzzleHttp\Promise\PromiseInterface')) { - self::handlePromiseResponse($response, $span); - } - } - } - ); - return Integration::LOADED; } From ca3cf295946bd9585e4812c6885016e52d74c29c Mon Sep 17 00:00:00 2001 From: Alexandre Choura Date: Fri, 19 Dec 2025 09:15:03 +0100 Subject: [PATCH 2/2] ALWAYS call overrideArguments() to prevent JIT compilation issues --- .../Integrations/Guzzle/GuzzleIntegration.php | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php b/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php index f75f7479f4..bad265254d 100644 --- a/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php +++ b/src/DDTrace/Integrations/Guzzle/GuzzleIntegration.php @@ -37,29 +37,33 @@ public static function init(): int \DDTrace\install_hook( 'GuzzleHttp\Client::transfer', static function (HookData $hook) { - if (!isset($hook->args[0])) { - return; - } - - $request = $hook->args[0]; - - if (!($request instanceof \Psr\Http\Message\RequestInterface)) { - return; - } + // Note: We must ALWAYS call overrideArguments() to prevent JIT compilation issues. + // See ext/hook/uhook.c: "hooks wishing to override args must do so unconditionally" - $dtHeaders = \DDTrace\generate_distributed_tracing_headers(); + $modified = false; - if (empty($dtHeaders)) { - return; - } - - foreach ($dtHeaders as $name => $value) { - if (!$request->hasHeader($name)) { - $request = $request->withHeader($name, $value); + if (isset($hook->args[0])) { + $request = $hook->args[0]; + + if ($request instanceof \Psr\Http\Message\RequestInterface) { + $dtHeaders = \DDTrace\generate_distributed_tracing_headers(); + + if (!empty($dtHeaders)) { + foreach ($dtHeaders as $name => $value) { + if (!$request->hasHeader($name)) { + $request = $request->withHeader($name, $value); + $modified = true; + } + } + + if ($modified) { + $hook->args[0] = $request; + } + } } } - $hook->args[0] = $request; + // CRITICAL: Always call overrideArguments to prevent JIT from breaking header injection $hook->overrideArguments($hook->args); }, static function (HookData $hook) {