Skip to content

Commit 0c6eee6

Browse files
alekittoJean85ste93cry
authored
Add support for distributed tracing of the Symfony HTTP client requests (#606)
Co-authored-by: Alessandro Lai <[email protected]> Co-authored-by: Stefano Arlandini <[email protected]>
1 parent 6db5a35 commit 0c6eee6

32 files changed

+1020
-7
lines changed

.github/workflows/tests.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ jobs:
6363
- name: Setup Problem Matchers for PHPUnit
6464
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
6565

66+
- name: Update PHPUnit
67+
run: composer require --dev phpunit/phpunit ^9.3.9 --no-update
68+
if: matrix.php == '8.0' && matrix.dependencies == 'lowest'
69+
6670
- name: Install dependencies
6771
uses: ramsey/composer-install@v1
6872
with:
@@ -112,7 +116,7 @@ jobs:
112116
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
113117

114118
- name: Remove optional packages
115-
run: composer remove doctrine/dbal doctrine/doctrine-bundle symfony/messenger symfony/twig-bundle symfony/cache --dev --no-update
119+
run: composer remove doctrine/dbal doctrine/doctrine-bundle symfony/messenger symfony/twig-bundle symfony/cache symfony/http-client --dev --no-update
116120

117121
- name: Install dependencies
118122
uses: ramsey/composer-install@v1

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Add support for tracing of the Symfony HTTP client requests (#606)
6+
57
## 4.3.0 (2022-05-30)
68
- Fix compatibility issue with Symfony >= 6.1.0 (#635)
79
- Add `TracingDriverConnectionInterface::getNativeConnection()` method to get the original driver connection (#597)

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
"symfony/cache": "^4.4.20||^5.0.11||^6.0",
5555
"symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0",
5656
"symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0",
57+
"symfony/http-client": "^4.4.20||^5.0.11||^6.0",
5758
"symfony/messenger": "^4.4.20||^5.0.11||^6.0",
5859
"symfony/monolog-bundle": "^3.4",
5960
"symfony/phpunit-bridge": "^5.2.6||^6.0",

phpstan-baseline.neon

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@ parameters:
105105
count: 1
106106
path: src/DependencyInjection/SentryExtension.php
107107

108+
-
109+
message: "#^Parameter \\#2 \\$config of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:registerHttpClientTracingConfiguration\\(\\) expects array\\<string, mixed\\>, mixed given\\.$#"
110+
count: 1
111+
path: src/DependencyInjection/SentryExtension.php
112+
108113
-
109114
message: "#^Parameter \\#2 \\$config of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:registerMessengerListenerConfiguration\\(\\) expects array\\<string, mixed\\>, mixed given\\.$#"
110115
count: 1
@@ -122,7 +127,7 @@ parameters:
122127

123128
-
124129
message: "#^Parameter \\#2 \\$config of method Symfony\\\\Component\\\\DependencyInjection\\\\Extension\\\\Extension\\:\\:isConfigEnabled\\(\\) expects array, mixed given\\.$#"
125-
count: 3
130+
count: 4
126131
path: src/DependencyInjection/SentryExtension.php
127132

128133
-
@@ -235,6 +240,31 @@ parameters:
235240
count: 1
236241
path: src/Tracing/Doctrine/DBAL/TracingStatementForV3.php
237242

243+
-
244+
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableHttpClient\\:\\:request\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
245+
count: 1
246+
path: src/Tracing/HttpClient/AbstractTraceableHttpClient.php
247+
248+
-
249+
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
250+
count: 1
251+
path: src/Tracing/HttpClient/AbstractTraceableResponse.php
252+
253+
-
254+
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\TraceableHttpClientForV6\\:\\:withOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
255+
count: 1
256+
path: src/Tracing/HttpClient/TraceableHttpClientForV6.php
257+
258+
-
259+
message: "#^Call to an undefined method Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\:\\:toStream\\(\\)\\.$#"
260+
count: 1
261+
path: src/Tracing/HttpClient/TraceableResponseForV5.php
262+
263+
-
264+
message: "#^Call to an undefined method Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\:\\:toStream\\(\\)\\.$#"
265+
count: 1
266+
path: src/Tracing/HttpClient/TraceableResponseForV6.php
267+
238268
-
239269
message: "#^Cannot access offset 'sample_rate' on mixed\\.$#"
240270
count: 1
@@ -410,3 +440,12 @@ parameters:
410440
count: 1
411441
path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php
412442

443+
-
444+
message: "#^Parameter \\#1 \\$responses of method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableHttpClient\\:\\:stream\\(\\) expects iterable\\<\\(int\\|string\\), Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\>\\|Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface, stdClass given\\.$#"
445+
count: 1
446+
path: tests/Tracing/HttpClient/TraceableHttpClientTest.php
447+
448+
-
449+
message: "#^Parameter \\#2 \\$responses of static method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:stream\\(\\) expects iterable\\<Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\>, array\\<int, stdClass\\> given\\.$#"
450+
count: 1
451+
path: tests/Tracing/HttpClient/TraceableResponseTest.php

phpstan.neon

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ parameters:
1212
- src/aliases.php
1313
- src/Tracing/Doctrine/DBAL/TracingStatementForV2.php
1414
- src/Tracing/Doctrine/DBAL/TracingDriverForV2.php
15+
- src/Tracing/HttpClient/TraceableHttpClientForV4.php
16+
- src/Tracing/HttpClient/TraceableHttpClientForV5.php
1517
- tests/End2End/App
1618
- tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php
1719
- tests/EventListener/Fixtures/UserWithoutIdentifierStub.php

psalm-baseline.xml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,16 @@
5050
<code>$params</code>
5151
</MoreSpecificImplementedParamType>
5252
</file>
53+
<file src="src/Tracing/HttpClient/TraceableResponseForV5.php">
54+
<UndefinedInterfaceMethod occurrences="1">
55+
<code>toStream</code>
56+
</UndefinedInterfaceMethod>
57+
</file>
58+
<file src="src/Tracing/HttpClient/TraceableResponseForV6.php">
59+
<UndefinedInterfaceMethod occurrences="1">
60+
<code>toStream</code>
61+
</UndefinedInterfaceMethod>
62+
</file>
5363
<file src="src/aliases.php">
5464
<MissingDependency occurrences="1">
5565
<code>TracingDriverForV2</code>
@@ -63,4 +73,23 @@
6373
<code>PostResponseEvent</code>
6474
</UndefinedClass>
6575
</file>
76+
<file src="src/DependencyInjection/Compiler/CacheTracingPass.php">
77+
<UndefinedDocblockClass occurrences="1">
78+
<code>$container-&gt;getParameter('sentry.tracing.cache.enabled')</code>
79+
</UndefinedDocblockClass>
80+
</file>
81+
<file src="src/DependencyInjection/Compiler/HttpClientTracingPass.php">
82+
<UndefinedDocblockClass occurrences="2">
83+
<code>$container-&gt;getParameter('sentry.tracing.enabled')</code>
84+
<code>$container-&gt;getParameter('sentry.tracing.http_client.enabled')</code>
85+
</UndefinedDocblockClass>
86+
</file>
87+
<file src="src/DependencyInjection/Compiler/DbalTracingPass.php">
88+
<UndefinedDocblockClass occurrences="4">
89+
<code>$container-&gt;getParameter('sentry.tracing.enabled')</code>
90+
<code>$container-&gt;getParameter('sentry.tracing.dbal.enabled')</code>
91+
<code>$container-&gt;getParameter('sentry.tracing.dbal.connections')</code>
92+
<code>$container-&gt;getParameter('doctrine.connections')</code>
93+
</UndefinedDocblockClass>
94+
</file>
6695
</files>

psalm.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
<directory name="src" />
1212
<ignoreFiles>
1313
<file name="src/Tracing/Doctrine/DBAL/TracingStatementForV2.php" />
14+
<file name="src/Tracing/HttpClient/TraceableHttpClientForV4.php" />
15+
<file name="src/Tracing/HttpClient/TraceableHttpClientForV5.php" />
1416
<directory name="vendor" />
1517
</ignoreFiles>
1618
</projectFiles>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\DependencyInjection\Compiler;
6+
7+
use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClient;
8+
use Sentry\State\HubInterface;
9+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
10+
use Symfony\Component\DependencyInjection\ContainerBuilder;
11+
use Symfony\Component\DependencyInjection\Reference;
12+
13+
final class HttpClientTracingPass implements CompilerPassInterface
14+
{
15+
/**
16+
* {@inheritdoc}
17+
*/
18+
public function process(ContainerBuilder $container): void
19+
{
20+
if (!$container->getParameter('sentry.tracing.enabled') || !$container->getParameter('sentry.tracing.http_client.enabled')) {
21+
return;
22+
}
23+
24+
foreach ($container->findTaggedServiceIds('http_client.client') as $id => $tags) {
25+
$container->register('.sentry.traceable.' . $id, TraceableHttpClient::class)
26+
->setDecoratedService($id)
27+
->setArgument(0, new Reference('.sentry.traceable.' . $id . '.inner'))
28+
->setArgument(1, new Reference(HubInterface::class))
29+
->addTag('kernel.reset', ['method' => 'reset']);
30+
}
31+
}
32+
}

src/DependencyInjection/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
1515
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1616
use Symfony\Component\Config\Definition\ConfigurationInterface;
17+
use Symfony\Component\HttpClient\HttpClient;
1718
use Symfony\Component\Messenger\MessageBusInterface;
1819

1920
final class Configuration implements ConfigurationInterface
@@ -184,6 +185,9 @@ private function addDistributedTracingSection(ArrayNodeDefinition $rootNode): vo
184185
->arrayNode('cache')
185186
->{class_exists(CacheItem::class) ? 'canBeDisabled' : 'canBeEnabled'}()
186187
->end()
188+
->arrayNode('http_client')
189+
->{class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}()
190+
->end()
187191
->arrayNode('console')
188192
->addDefaultsIfNotSet()
189193
->fixXmlConfig('excluded_command')

src/DependencyInjection/SentryExtension.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
use Symfony\Component\DependencyInjection\Loader;
3838
use Symfony\Component\DependencyInjection\Reference;
3939
use Symfony\Component\ErrorHandler\Error\FatalError;
40+
use Symfony\Component\HttpClient\HttpClient;
4041
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;
4142

4243
final class SentryExtension extends ConfigurableExtension
@@ -74,6 +75,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
7475
$this->registerDbalTracingConfiguration($container, $mergedConfig['tracing']);
7576
$this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']);
7677
$this->registerCacheTracingConfiguration($container, $mergedConfig['tracing']);
78+
$this->registerHttpClientTracingConfiguration($container, $mergedConfig['tracing']);
7779
}
7880

7981
/**
@@ -247,6 +249,21 @@ private function registerCacheTracingConfiguration(ContainerBuilder $container,
247249
$container->setParameter('sentry.tracing.cache.enabled', $isConfigEnabled);
248250
}
249251

252+
/**
253+
* @param array<string, mixed> $config
254+
*/
255+
private function registerHttpClientTracingConfiguration(ContainerBuilder $container, array $config): void
256+
{
257+
$isConfigEnabled = $this->isConfigEnabled($container, $config)
258+
&& $this->isConfigEnabled($container, $config['http_client']);
259+
260+
if ($isConfigEnabled && !class_exists(HttpClient::class)) {
261+
throw new LogicException('Http client tracing support cannot be enabled because the symfony/http-client Composer package is not installed.');
262+
}
263+
264+
$container->setParameter('sentry.tracing.http_client.enabled', $isConfigEnabled);
265+
}
266+
250267
/**
251268
* @param string[] $integrations
252269
* @param array<string, mixed> $config

src/Resources/config/schema/sentry-1.0.xsd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
<xsd:element name="twig" type="tracing-twig" minOccurs="0" maxOccurs="1" />
9292
<xsd:element name="cache" type="tracing-cache" minOccurs="0" maxOccurs="1" />
9393
<xsd:element name="console" type="tracing-console" minOccurs="0" maxOccurs="1" />
94+
<xsd:element name="http-client" type="tracing-http-client" minOccurs="0" maxOccurs="1" />
9495
</xsd:choice>
9596

9697
<xsd:attribute name="enabled" type="xsd:boolean" default="true"/>
@@ -117,4 +118,8 @@
117118
<xsd:element name="excluded-command" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
118119
</xsd:sequence>
119120
</xsd:complexType>
121+
122+
<xsd:complexType name="tracing-http-client">
123+
<xsd:attribute name="enabled" type="xsd:boolean" />
124+
</xsd:complexType>
120125
</xsd:schema>

src/SentryBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass;
88
use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass;
9+
use Sentry\SentryBundle\DependencyInjection\Compiler\HttpClientTracingPass;
910
use Symfony\Component\DependencyInjection\ContainerBuilder;
1011
use Symfony\Component\HttpKernel\Bundle\Bundle;
1112

@@ -19,5 +20,6 @@ public function build(ContainerBuilder $container): void
1920

2021
$container->addCompilerPass(new DbalTracingPass());
2122
$container->addCompilerPass(new CacheTracingPass());
23+
$container->addCompilerPass(new HttpClientTracingPass());
2224
}
2325
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\Tracing\HttpClient;
6+
7+
use Psr\Log\LoggerAwareInterface;
8+
use Psr\Log\LoggerInterface;
9+
use Sentry\State\HubInterface;
10+
use Sentry\Tracing\SpanContext;
11+
use Symfony\Component\HttpClient\Response\ResponseStream;
12+
use Symfony\Contracts\HttpClient\HttpClientInterface;
13+
use Symfony\Contracts\HttpClient\ResponseInterface;
14+
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
15+
use Symfony\Contracts\Service\ResetInterface;
16+
17+
/**
18+
* This is an implementation of the {@see HttpClientInterface} that decorates
19+
* an existing http client to support distributed tracing capabilities.
20+
*
21+
* @internal
22+
*/
23+
abstract class AbstractTraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
24+
{
25+
/**
26+
* @var HttpClientInterface
27+
*/
28+
protected $client;
29+
30+
/**
31+
* @var HubInterface
32+
*/
33+
protected $hub;
34+
35+
public function __construct(HttpClientInterface $client, HubInterface $hub)
36+
{
37+
$this->client = $client;
38+
$this->hub = $hub;
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function request(string $method, string $url, array $options = []): ResponseInterface
45+
{
46+
$span = null;
47+
$parent = $this->hub->getSpan();
48+
49+
if (null !== $parent) {
50+
$headers = $options['headers'] ?? [];
51+
$headers['sentry-trace'] = $parent->toTraceparent();
52+
$options['headers'] = $headers;
53+
54+
$context = new SpanContext();
55+
$context->setOp('http.client');
56+
$context->setDescription('HTTP ' . $method);
57+
$context->setTags([
58+
'http.method' => $method,
59+
'http.url' => $url,
60+
]);
61+
62+
$span = $parent->startChild($context);
63+
}
64+
65+
return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span);
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function stream($responses, float $timeout = null): ResponseStreamInterface
72+
{
73+
if ($responses instanceof AbstractTraceableResponse) {
74+
$responses = [$responses];
75+
} elseif (!is_iterable($responses)) {
76+
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
77+
}
78+
79+
return new ResponseStream(AbstractTraceableResponse::stream($this->client, $responses, $timeout));
80+
}
81+
82+
public function reset(): void
83+
{
84+
if ($this->client instanceof ResetInterface) {
85+
$this->client->reset();
86+
}
87+
}
88+
89+
/**
90+
* {@inheritdoc}
91+
*/
92+
public function setLogger(LoggerInterface $logger): void
93+
{
94+
if ($this->client instanceof LoggerAwareInterface) {
95+
$this->client->setLogger($logger);
96+
}
97+
}
98+
}

0 commit comments

Comments
 (0)