Skip to content

Commit 8f7a737

Browse files
rjd22ste93cry
andauthored
Implement distributed tracing support for requests (#423)
Co-authored-by: Stefano Arlandini <[email protected]>
1 parent de86b84 commit 8f7a737

14 files changed

+960
-7
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Add support for distributed tracing of Symfony request events (#423)
56
- Add support for distributed tracing of Twig template rendering (#430)
67
- Add support for distributed tracing of SQL queries while using Doctrine DBAL (#426)
78
- Added missing `capture-soft-fails` config schema option (#417)

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"symfony/dependency-injection": "^3.4.44||^4.4.12||^5.0.11",
2929
"symfony/event-dispatcher": "^3.4.44||^4.4.12||^5.0.11",
3030
"symfony/http-kernel": "^3.4.44||^4.4.12||^5.0.11",
31+
"symfony/polyfill-php80": "^1.22",
3132
"symfony/psr-http-message-bridge": "^2.0",
3233
"symfony/security-core": "^3.4.44||^4.4.12||^5.0.11"
3334
},
@@ -49,7 +50,6 @@
4950
"symfony/messenger": "^4.4.12||^5.0.11",
5051
"symfony/monolog-bundle": "^3.4",
5152
"symfony/phpunit-bridge": "^5.0",
52-
"symfony/polyfill-php80": "^1.22",
5353
"symfony/twig-bundle": "^3.4.44||^4.4.12||^5.0.11",
5454
"symfony/yaml": "^3.4.44||^4.4.12||^5.0.11",
5555
"vimeo/psalm": "^4.3"

phpstan-baseline.neon

+10
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ parameters:
105105
count: 2
106106
path: src/aliases.php
107107

108+
-
109+
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterResponseEvent not found\\.$#"
110+
count: 1
111+
path: src/aliases.php
112+
113+
-
114+
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\PostResponseEvent not found\\.$#"
115+
count: 1
116+
path: src/aliases.php
117+
108118
-
109119
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterControllerEvent not found\\.$#"
110120
count: 1

phpunit.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/schema/8.5.xsd"
44
colors="true"
5-
bootstrap="vendor/autoload.php"
5+
bootstrap="tests/bootstrap.php"
66
cacheResult="false"
77
beStrictAboutOutputDuringTests="true"
88
>

src/DependencyInjection/SentryExtension.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
6262
$this->registerConfiguration($container, $mergedConfig);
6363
$this->registerErrorListenerConfiguration($container, $mergedConfig);
6464
$this->registerMessengerListenerConfiguration($container, $mergedConfig['messenger']);
65-
$this->registerTracingConfiguration($container, $mergedConfig['tracing']);
66-
$this->registerTracingTwigExtensionConfiguration($container, $mergedConfig['tracing']);
65+
$this->registerDbalTracingConfiguration($container, $mergedConfig['tracing']);
66+
$this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']);
6767
}
6868

6969
/**
@@ -159,7 +159,7 @@ private function registerMessengerListenerConfiguration(ContainerBuilder $contai
159159
/**
160160
* @param array<string, mixed> $config
161161
*/
162-
private function registerTracingConfiguration(ContainerBuilder $container, array $config): void
162+
private function registerDbalTracingConfiguration(ContainerBuilder $container, array $config): void
163163
{
164164
$isConfigEnabled = $this->isConfigEnabled($container, $config['dbal']);
165165

@@ -178,7 +178,7 @@ private function registerTracingConfiguration(ContainerBuilder $container, array
178178
/**
179179
* @param array<string, mixed> $config
180180
*/
181-
private function registerTracingTwigExtensionConfiguration(ContainerBuilder $container, array $config): void
181+
private function registerTwigTracingConfiguration(ContainerBuilder $container, array $config): void
182182
{
183183
$isConfigEnabled = $this->isConfigEnabled($container, $config['twig']);
184184

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\EventListener;
6+
7+
use Sentry\State\HubInterface;
8+
use Symfony\Component\HttpFoundation\Request;
9+
use Symfony\Component\HttpFoundation\Response;
10+
use Symfony\Component\Routing\Route;
11+
12+
abstract class AbstractTracingRequestListener
13+
{
14+
/**
15+
* @var HubInterface The current hub
16+
*/
17+
protected $hub;
18+
19+
/**
20+
* Constructor.
21+
*
22+
* @param HubInterface $hub The current hub
23+
*/
24+
public function __construct(HubInterface $hub)
25+
{
26+
$this->hub = $hub;
27+
}
28+
29+
/**
30+
* This method is called once a response for the current HTTP request is
31+
* created, but before it is sent off to the client. Its use is mainly for
32+
* gathering information like the HTTP status code and attaching them as
33+
* tags of the span/transaction.
34+
*
35+
* @param RequestListenerResponseEvent $event The event
36+
*/
37+
public function handleKernelResponseEvent(RequestListenerResponseEvent $event): void
38+
{
39+
/** @var Response $response */
40+
$response = $event->getResponse();
41+
$span = $this->hub->getSpan();
42+
43+
if (null === $span) {
44+
return;
45+
}
46+
47+
$span->setHttpStatus($response->getStatusCode());
48+
}
49+
50+
/**
51+
* Gets the name of the route or fallback to the controller FQCN if the
52+
* route is anonymous (e.g. a subrequest).
53+
*
54+
* @param Request $request The HTTP request
55+
*/
56+
protected function getRouteName(Request $request): string
57+
{
58+
$route = $request->attributes->get('_route');
59+
60+
if ($route instanceof Route) {
61+
$route = $route->getPath();
62+
}
63+
64+
if (null === $route) {
65+
$route = $request->attributes->get('_controller');
66+
67+
if (\is_array($route) && \is_callable($route, true)) {
68+
$route = sprintf('%s::%s', \is_object($route[0]) ? get_debug_type($route[0]) : $route[0], $route[1]);
69+
}
70+
}
71+
72+
return \is_string($route) ? $route : '<unknown>';
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\EventListener;
6+
7+
use Sentry\Tracing\Transaction;
8+
use Sentry\Tracing\TransactionContext;
9+
use Symfony\Component\HttpFoundation\Request;
10+
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
11+
12+
/**
13+
* This event listener acts on the master requests and starts a transaction
14+
* to report performance data to Sentry. It gathers useful data like the
15+
* HTTP status code of the response or the name of the route that handles
16+
* the request and add them as tags.
17+
*/
18+
final class TracingRequestListener extends AbstractTracingRequestListener
19+
{
20+
/**
21+
* This method is called for each subrequest handled by the framework and
22+
* starts a new {@see Transaction}.
23+
*
24+
* @param RequestListenerRequestEvent $event The event
25+
*/
26+
public function handleKernelRequestEvent(RequestListenerRequestEvent $event): void
27+
{
28+
if (!$event->isMasterRequest()) {
29+
return;
30+
}
31+
32+
/** @var Request $request */
33+
$request = $event->getRequest();
34+
$requestStartTime = $request->server->get('REQUEST_TIME_FLOAT', microtime(true));
35+
36+
$context = TransactionContext::fromSentryTrace($request->headers->get('sentry-trace', ''));
37+
$context->setOp('http.server');
38+
$context->setName(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo()));
39+
$context->setStartTimestamp($requestStartTime);
40+
$context->setTags($this->getTags($request));
41+
42+
$this->hub->setSpan($this->hub->startTransaction($context));
43+
}
44+
45+
/**
46+
* This method is called for each request handled by the framework and
47+
* ends the tracing.
48+
*
49+
* @param FinishRequestEvent $event The event
50+
*/
51+
public function handleKernelFinishRequestEvent(FinishRequestEvent $event): void
52+
{
53+
if (!$event->isMasterRequest()) {
54+
return;
55+
}
56+
57+
$transaction = $this->hub->getTransaction();
58+
59+
if (null === $transaction) {
60+
return;
61+
}
62+
63+
$transaction->finish();
64+
}
65+
66+
/**
67+
* Gets the tags to attach to the transaction.
68+
*
69+
* @param Request $request The HTTP request
70+
*
71+
* @return array<string, string>
72+
*/
73+
private function getTags(Request $request): array
74+
{
75+
$client = $this->hub->getClient();
76+
$tags = [
77+
'net.host.port' => (string) $request->getPort(),
78+
'http.method' => $request->getMethod(),
79+
'http.url' => $request->getUri(),
80+
'http.flavor' => $this->getHttpFlavor($request),
81+
'route' => $this->getRouteName($request),
82+
];
83+
84+
if (false !== filter_var($request->getHost(), \FILTER_VALIDATE_IP)) {
85+
$tags['net.host.ip'] = $request->getHost();
86+
} else {
87+
$tags['net.host.name'] = $request->getHost();
88+
}
89+
90+
if (null !== $request->getClientIp() && null !== $client && $client->getOptions()->shouldSendDefaultPii()) {
91+
$tags['net.peer.ip'] = $request->getClientIp();
92+
}
93+
94+
return $tags;
95+
}
96+
97+
/**
98+
* Gets the HTTP flavor from the request.
99+
*
100+
* @param Request $request The HTTP request
101+
*/
102+
protected function getHttpFlavor(Request $request): string
103+
{
104+
$protocolVersion = $request->getProtocolVersion();
105+
106+
if (str_starts_with($protocolVersion, 'HTTP/')) {
107+
return substr($protocolVersion, \strlen('HTTP/'));
108+
}
109+
110+
return $protocolVersion;
111+
}
112+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\EventListener;
6+
7+
use Sentry\Tracing\Span;
8+
use Sentry\Tracing\SpanContext;
9+
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;
10+
11+
/**
12+
* This event listener acts on the sub requests and starts a child span of the
13+
* current transaction to gather performance data for each of them.
14+
*/
15+
final class TracingSubRequestListener extends AbstractTracingRequestListener
16+
{
17+
/**
18+
* This method is called for each subrequest handled by the framework and
19+
* traces each by starting a new {@see Span}.
20+
*
21+
* @param SubRequestListenerRequestEvent $event The event
22+
*/
23+
public function handleKernelRequestEvent(SubRequestListenerRequestEvent $event): void
24+
{
25+
if ($event->isMasterRequest()) {
26+
return;
27+
}
28+
29+
$request = $event->getRequest();
30+
$span = $this->hub->getSpan();
31+
32+
if (null === $span) {
33+
return;
34+
}
35+
36+
$spanContext = new SpanContext();
37+
$spanContext->setOp('http.server');
38+
$spanContext->setDescription(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo()));
39+
$spanContext->setTags([
40+
'http.method' => $request->getMethod(),
41+
'http.url' => $request->getUri(),
42+
'route' => $this->getRouteName($request),
43+
]);
44+
45+
$this->hub->setSpan($span->startChild($spanContext));
46+
}
47+
48+
/**
49+
* This method is called for each subrequest handled by the framework and
50+
* ends the tracing.
51+
*
52+
* @param FinishRequestEvent $event The event
53+
*/
54+
public function handleKernelFinishRequestEvent(FinishRequestEvent $event): void
55+
{
56+
if ($event->isMasterRequest()) {
57+
return;
58+
}
59+
60+
$span = $this->hub->getSpan();
61+
62+
if (null === $span) {
63+
return;
64+
}
65+
66+
$span->finish();
67+
}
68+
}

src/Resources/config/services.xml

+16
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@
5454
<tag name="kernel.event_listener" event="kernel.finish_request" method="handleKernelFinishRequestEvent" priority="5" />
5555
</service>
5656

57+
<service id="Sentry\SentryBundle\EventListener\TracingRequestListener" class="Sentry\SentryBundle\EventListener\TracingRequestListener">
58+
<argument type="service" id="Sentry\State\HubInterface" />
59+
60+
<tag name="kernel.event_listener" event="kernel.request" method="handleKernelRequestEvent" priority="4" />
61+
<tag name="kernel.event_listener" event="kernel.finish_request" method="handleKernelFinishRequestEvent" priority="5" />
62+
<tag name="kernel.event_listener" event="kernel.response" method="handleKernelResponseEvent" priority="15" />
63+
</service>
64+
65+
<service id="Sentry\SentryBundle\EventListener\TracingSubRequestListener" class="Sentry\SentryBundle\EventListener\TracingSubRequestListener">
66+
<argument type="service" id="Sentry\State\HubInterface" />
67+
68+
<tag name="kernel.event_listener" event="kernel.request" method="handleKernelRequestEvent" priority="2" />
69+
<tag name="kernel.event_listener" event="kernel.finish_request" method="handleKernelFinishRequestEvent" priority="10" />
70+
<tag name="kernel.event_listener" event="kernel.response" method="handleKernelResponseEvent" priority="15" />
71+
</service>
72+
5773
<service id="Sentry\SentryBundle\EventListener\MessengerListener" class="Sentry\SentryBundle\EventListener\MessengerListener">
5874
<argument type="service" id="Sentry\State\HubInterface" />
5975

0 commit comments

Comments
 (0)