Skip to content

Commit

Permalink
Add support for distributed tracing of the Symfony HTTP client reques…
Browse files Browse the repository at this point in the history
…ts (#606)

Co-authored-by: Alessandro Lai <alessandro.lai85@gmail.com>
Co-authored-by: Stefano Arlandini <sarlandini@alice.it>
  • Loading branch information
3 people committed Jul 14, 2022
1 parent 6db5a35 commit 0c6eee6
Show file tree
Hide file tree
Showing 32 changed files with 1,020 additions and 7 deletions.
6 changes: 5 additions & 1 deletion .github/workflows/tests.yaml
Expand Up @@ -63,6 +63,10 @@ jobs:
- name: Setup Problem Matchers for PHPUnit
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

- name: Update PHPUnit
run: composer require --dev phpunit/phpunit ^9.3.9 --no-update
if: matrix.php == '8.0' && matrix.dependencies == 'lowest'

- name: Install dependencies
uses: ramsey/composer-install@v1
with:
Expand Down Expand Up @@ -112,7 +116,7 @@ jobs:
run: echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"

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

- name: Install dependencies
uses: ramsey/composer-install@v1
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

## Unreleased

- Add support for tracing of the Symfony HTTP client requests (#606)

## 4.3.0 (2022-05-30)
- Fix compatibility issue with Symfony >= 6.1.0 (#635)
- Add `TracingDriverConnectionInterface::getNativeConnection()` method to get the original driver connection (#597)
Expand Down
1 change: 1 addition & 0 deletions composer.json
Expand Up @@ -54,6 +54,7 @@
"symfony/cache": "^4.4.20||^5.0.11||^6.0",
"symfony/dom-crawler": "^4.4.20||^5.0.11||^6.0",
"symfony/framework-bundle": "^4.4.20||^5.0.11||^6.0",
"symfony/http-client": "^4.4.20||^5.0.11||^6.0",
"symfony/messenger": "^4.4.20||^5.0.11||^6.0",
"symfony/monolog-bundle": "^3.4",
"symfony/phpunit-bridge": "^5.2.6||^6.0",
Expand Down
41 changes: 40 additions & 1 deletion phpstan-baseline.neon
Expand Up @@ -105,6 +105,11 @@ parameters:
count: 1
path: src/DependencyInjection/SentryExtension.php

-
message: "#^Parameter \\#2 \\$config of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:registerHttpClientTracingConfiguration\\(\\) expects array\\<string, mixed\\>, mixed given\\.$#"
count: 1
path: src/DependencyInjection/SentryExtension.php

-
message: "#^Parameter \\#2 \\$config of method Sentry\\\\SentryBundle\\\\DependencyInjection\\\\SentryExtension\\:\\:registerMessengerListenerConfiguration\\(\\) expects array\\<string, mixed\\>, mixed given\\.$#"
count: 1
Expand All @@ -122,7 +127,7 @@ parameters:

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

-
Expand Down Expand Up @@ -235,6 +240,31 @@ parameters:
count: 1
path: src/Tracing/Doctrine/DBAL/TracingStatementForV3.php

-
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableHttpClient\\:\\:request\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Tracing/HttpClient/AbstractTraceableHttpClient.php

-
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:toArray\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1
path: src/Tracing/HttpClient/AbstractTraceableResponse.php

-
message: "#^Method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\TraceableHttpClientForV6\\:\\:withOptions\\(\\) has parameter \\$options with no value type specified in iterable type array\\.$#"
count: 1
path: src/Tracing/HttpClient/TraceableHttpClientForV6.php

-
message: "#^Call to an undefined method Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\:\\:toStream\\(\\)\\.$#"
count: 1
path: src/Tracing/HttpClient/TraceableResponseForV5.php

-
message: "#^Call to an undefined method Symfony\\\\Contracts\\\\HttpClient\\\\ResponseInterface\\:\\:toStream\\(\\)\\.$#"
count: 1
path: src/Tracing/HttpClient/TraceableResponseForV6.php

-
message: "#^Cannot access offset 'sample_rate' on mixed\\.$#"
count: 1
Expand Down Expand Up @@ -410,3 +440,12 @@ parameters:
count: 1
path: tests/Tracing/Doctrine/DBAL/TracingStatementForV2Test.php

-
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\\.$#"
count: 1
path: tests/Tracing/HttpClient/TraceableHttpClientTest.php

-
message: "#^Parameter \\#2 \\$responses of static method Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\:\\:stream\\(\\) expects iterable\\<Sentry\\\\SentryBundle\\\\Tracing\\\\HttpClient\\\\AbstractTraceableResponse\\>, array\\<int, stdClass\\> given\\.$#"
count: 1
path: tests/Tracing/HttpClient/TraceableResponseTest.php
2 changes: 2 additions & 0 deletions phpstan.neon
Expand Up @@ -12,6 +12,8 @@ parameters:
- src/aliases.php
- src/Tracing/Doctrine/DBAL/TracingStatementForV2.php
- src/Tracing/Doctrine/DBAL/TracingDriverForV2.php
- src/Tracing/HttpClient/TraceableHttpClientForV4.php
- src/Tracing/HttpClient/TraceableHttpClientForV5.php
- tests/End2End/App
- tests/Tracing/Doctrine/DBAL/TracingDriverForV2Test.php
- tests/EventListener/Fixtures/UserWithoutIdentifierStub.php
Expand Down
29 changes: 29 additions & 0 deletions psalm-baseline.xml
Expand Up @@ -50,6 +50,16 @@
<code>$params</code>
</MoreSpecificImplementedParamType>
</file>
<file src="src/Tracing/HttpClient/TraceableResponseForV5.php">
<UndefinedInterfaceMethod occurrences="1">
<code>toStream</code>
</UndefinedInterfaceMethod>
</file>
<file src="src/Tracing/HttpClient/TraceableResponseForV6.php">
<UndefinedInterfaceMethod occurrences="1">
<code>toStream</code>
</UndefinedInterfaceMethod>
</file>
<file src="src/aliases.php">
<MissingDependency occurrences="1">
<code>TracingDriverForV2</code>
Expand All @@ -63,4 +73,23 @@
<code>PostResponseEvent</code>
</UndefinedClass>
</file>
<file src="src/DependencyInjection/Compiler/CacheTracingPass.php">
<UndefinedDocblockClass occurrences="1">
<code>$container-&gt;getParameter('sentry.tracing.cache.enabled')</code>
</UndefinedDocblockClass>
</file>
<file src="src/DependencyInjection/Compiler/HttpClientTracingPass.php">
<UndefinedDocblockClass occurrences="2">
<code>$container-&gt;getParameter('sentry.tracing.enabled')</code>
<code>$container-&gt;getParameter('sentry.tracing.http_client.enabled')</code>
</UndefinedDocblockClass>
</file>
<file src="src/DependencyInjection/Compiler/DbalTracingPass.php">
<UndefinedDocblockClass occurrences="4">
<code>$container-&gt;getParameter('sentry.tracing.enabled')</code>
<code>$container-&gt;getParameter('sentry.tracing.dbal.enabled')</code>
<code>$container-&gt;getParameter('sentry.tracing.dbal.connections')</code>
<code>$container-&gt;getParameter('doctrine.connections')</code>
</UndefinedDocblockClass>
</file>
</files>
2 changes: 2 additions & 0 deletions psalm.xml
Expand Up @@ -11,6 +11,8 @@
<directory name="src" />
<ignoreFiles>
<file name="src/Tracing/Doctrine/DBAL/TracingStatementForV2.php" />
<file name="src/Tracing/HttpClient/TraceableHttpClientForV4.php" />
<file name="src/Tracing/HttpClient/TraceableHttpClientForV5.php" />
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
Expand Down
32 changes: 32 additions & 0 deletions src/DependencyInjection/Compiler/HttpClientTracingPass.php
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\DependencyInjection\Compiler;

use Sentry\SentryBundle\Tracing\HttpClient\TraceableHttpClient;
use Sentry\State\HubInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

final class HttpClientTracingPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container): void
{
if (!$container->getParameter('sentry.tracing.enabled') || !$container->getParameter('sentry.tracing.http_client.enabled')) {
return;
}

foreach ($container->findTaggedServiceIds('http_client.client') as $id => $tags) {
$container->register('.sentry.traceable.' . $id, TraceableHttpClient::class)
->setDecoratedService($id)
->setArgument(0, new Reference('.sentry.traceable.' . $id . '.inner'))
->setArgument(1, new Reference(HubInterface::class))
->addTag('kernel.reset', ['method' => 'reset']);
}
}
}
4 changes: 4 additions & 0 deletions src/DependencyInjection/Configuration.php
Expand Up @@ -14,6 +14,7 @@
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Messenger\MessageBusInterface;

final class Configuration implements ConfigurationInterface
Expand Down Expand Up @@ -184,6 +185,9 @@ private function addDistributedTracingSection(ArrayNodeDefinition $rootNode): vo
->arrayNode('cache')
->{class_exists(CacheItem::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->end()
->arrayNode('http_client')
->{class_exists(HttpClient::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->end()
->arrayNode('console')
->addDefaultsIfNotSet()
->fixXmlConfig('excluded_command')
Expand Down
17 changes: 17 additions & 0 deletions src/DependencyInjection/SentryExtension.php
Expand Up @@ -37,6 +37,7 @@
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\ErrorHandler\Error\FatalError;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension;

final class SentryExtension extends ConfigurableExtension
Expand Down Expand Up @@ -74,6 +75,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
$this->registerDbalTracingConfiguration($container, $mergedConfig['tracing']);
$this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']);
$this->registerCacheTracingConfiguration($container, $mergedConfig['tracing']);
$this->registerHttpClientTracingConfiguration($container, $mergedConfig['tracing']);
}

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

/**
* @param array<string, mixed> $config
*/
private function registerHttpClientTracingConfiguration(ContainerBuilder $container, array $config): void
{
$isConfigEnabled = $this->isConfigEnabled($container, $config)
&& $this->isConfigEnabled($container, $config['http_client']);

if ($isConfigEnabled && !class_exists(HttpClient::class)) {
throw new LogicException('Http client tracing support cannot be enabled because the symfony/http-client Composer package is not installed.');
}

$container->setParameter('sentry.tracing.http_client.enabled', $isConfigEnabled);
}

/**
* @param string[] $integrations
* @param array<string, mixed> $config
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/config/schema/sentry-1.0.xsd
Expand Up @@ -91,6 +91,7 @@
<xsd:element name="twig" type="tracing-twig" minOccurs="0" maxOccurs="1" />
<xsd:element name="cache" type="tracing-cache" minOccurs="0" maxOccurs="1" />
<xsd:element name="console" type="tracing-console" minOccurs="0" maxOccurs="1" />
<xsd:element name="http-client" type="tracing-http-client" minOccurs="0" maxOccurs="1" />
</xsd:choice>

<xsd:attribute name="enabled" type="xsd:boolean" default="true"/>
Expand All @@ -117,4 +118,8 @@
<xsd:element name="excluded-command" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="tracing-http-client">
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>
</xsd:schema>
2 changes: 2 additions & 0 deletions src/SentryBundle.php
Expand Up @@ -6,6 +6,7 @@

use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass;
use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass;
use Sentry\SentryBundle\DependencyInjection\Compiler\HttpClientTracingPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;

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

$container->addCompilerPass(new DbalTracingPass());
$container->addCompilerPass(new CacheTracingPass());
$container->addCompilerPass(new HttpClientTracingPass());
}
}
98 changes: 98 additions & 0 deletions src/Tracing/HttpClient/AbstractTraceableHttpClient.php
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\Tracing\HttpClient;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerInterface;
use Sentry\State\HubInterface;
use Sentry\Tracing\SpanContext;
use Symfony\Component\HttpClient\Response\ResponseStream;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;

/**
* This is an implementation of the {@see HttpClientInterface} that decorates
* an existing http client to support distributed tracing capabilities.
*
* @internal
*/
abstract class AbstractTraceableHttpClient implements HttpClientInterface, ResetInterface, LoggerAwareInterface
{
/**
* @var HttpClientInterface
*/
protected $client;

/**
* @var HubInterface
*/
protected $hub;

public function __construct(HttpClientInterface $client, HubInterface $hub)
{
$this->client = $client;
$this->hub = $hub;
}

/**
* {@inheritdoc}
*/
public function request(string $method, string $url, array $options = []): ResponseInterface
{
$span = null;
$parent = $this->hub->getSpan();

if (null !== $parent) {
$headers = $options['headers'] ?? [];
$headers['sentry-trace'] = $parent->toTraceparent();
$options['headers'] = $headers;

$context = new SpanContext();
$context->setOp('http.client');
$context->setDescription('HTTP ' . $method);
$context->setTags([
'http.method' => $method,
'http.url' => $url,
]);

$span = $parent->startChild($context);
}

return new TraceableResponse($this->client, $this->client->request($method, $url, $options), $span);
}

/**
* {@inheritdoc}
*/
public function stream($responses, float $timeout = null): ResponseStreamInterface
{
if ($responses instanceof AbstractTraceableResponse) {
$responses = [$responses];
} elseif (!is_iterable($responses)) {
throw new \TypeError(sprintf('"%s()" expects parameter 1 to be an iterable of TraceableResponse objects, "%s" given.', __METHOD__, get_debug_type($responses)));
}

return new ResponseStream(AbstractTraceableResponse::stream($this->client, $responses, $timeout));
}

public function reset(): void
{
if ($this->client instanceof ResetInterface) {
$this->client->reset();
}
}

/**
* {@inheritdoc}
*/
public function setLogger(LoggerInterface $logger): void
{
if ($this->client instanceof LoggerAwareInterface) {
$this->client->setLogger($logger);
}
}
}

0 comments on commit 0c6eee6

Please sign in to comment.