Skip to content

Initial implementation of sentry tracing with tests #423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Mar 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Unreleased

- Add support for distributed tracing of Symfony request events (#423)
- Add support for distributed tracing of Twig template rendering (#430)
- Add support for distributed tracing of SQL queries while using Doctrine DBAL (#426)
- Added missing `capture-soft-fails` config schema option (#417)
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"symfony/dependency-injection": "^3.4.44||^4.4.12||^5.0.11",
"symfony/event-dispatcher": "^3.4.44||^4.4.12||^5.0.11",
"symfony/http-kernel": "^3.4.44||^4.4.12||^5.0.11",
"symfony/polyfill-php80": "^1.22",
"symfony/psr-http-message-bridge": "^2.0",
"symfony/security-core": "^3.4.44||^4.4.12||^5.0.11"
},
Expand All @@ -49,7 +50,6 @@
"symfony/messenger": "^4.4.12||^5.0.11",
"symfony/monolog-bundle": "^3.4",
"symfony/phpunit-bridge": "^5.0",
"symfony/polyfill-php80": "^1.22",
"symfony/twig-bundle": "^3.4.44||^4.4.12||^5.0.11",
"symfony/yaml": "^3.4.44||^4.4.12||^5.0.11",
"vimeo/psalm": "^4.3"
Expand Down
10 changes: 10 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,16 @@ parameters:
count: 2
path: src/aliases.php

-
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterResponseEvent not found\\.$#"
count: 1
path: src/aliases.php

-
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\PostResponseEvent not found\\.$#"
count: 1
path: src/aliases.php

-
message: "#^Class Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\FilterControllerEvent not found\\.$#"
count: 1
Expand Down
2 changes: 1 addition & 1 deletion phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/schema/8.5.xsd"
colors="true"
bootstrap="vendor/autoload.php"
bootstrap="tests/bootstrap.php"
cacheResult="false"
beStrictAboutOutputDuringTests="true"
>
Expand Down
8 changes: 4 additions & 4 deletions src/DependencyInjection/SentryExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
$this->registerConfiguration($container, $mergedConfig);
$this->registerErrorListenerConfiguration($container, $mergedConfig);
$this->registerMessengerListenerConfiguration($container, $mergedConfig['messenger']);
$this->registerTracingConfiguration($container, $mergedConfig['tracing']);
$this->registerTracingTwigExtensionConfiguration($container, $mergedConfig['tracing']);
$this->registerDbalTracingConfiguration($container, $mergedConfig['tracing']);
$this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']);
}

/**
Expand Down Expand Up @@ -159,7 +159,7 @@ private function registerMessengerListenerConfiguration(ContainerBuilder $contai
/**
* @param array<string, mixed> $config
*/
private function registerTracingConfiguration(ContainerBuilder $container, array $config): void
private function registerDbalTracingConfiguration(ContainerBuilder $container, array $config): void
{
$isConfigEnabled = $this->isConfigEnabled($container, $config['dbal']);

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

Expand Down
74 changes: 74 additions & 0 deletions src/EventListener/AbstractTracingRequestListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\EventListener;

use Sentry\State\HubInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Route;

abstract class AbstractTracingRequestListener
{
/**
* @var HubInterface The current hub
*/
protected $hub;

/**
* Constructor.
*
* @param HubInterface $hub The current hub
*/
public function __construct(HubInterface $hub)
{
$this->hub = $hub;
}

/**
* This method is called once a response for the current HTTP request is
* created, but before it is sent off to the client. Its use is mainly for
* gathering information like the HTTP status code and attaching them as
* tags of the span/transaction.
*
* @param RequestListenerResponseEvent $event The event
*/
public function handleKernelResponseEvent(RequestListenerResponseEvent $event): void
{
/** @var Response $response */
$response = $event->getResponse();
$span = $this->hub->getSpan();

if (null === $span) {
return;
}

$span->setHttpStatus($response->getStatusCode());
}

/**
* Gets the name of the route or fallback to the controller FQCN if the
* route is anonymous (e.g. a subrequest).
*
* @param Request $request The HTTP request
*/
protected function getRouteName(Request $request): string
{
$route = $request->attributes->get('_route');

if ($route instanceof Route) {
$route = $route->getPath();
}

if (null === $route) {
$route = $request->attributes->get('_controller');

if (\is_array($route) && \is_callable($route, true)) {
$route = sprintf('%s::%s', \is_object($route[0]) ? get_debug_type($route[0]) : $route[0], $route[1]);
}
}

return \is_string($route) ? $route : '<unknown>';
}
}
112 changes: 112 additions & 0 deletions src/EventListener/TracingRequestListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\EventListener;

use Sentry\Tracing\Transaction;
use Sentry\Tracing\TransactionContext;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;

/**
* This event listener acts on the master requests and starts a transaction
* to report performance data to Sentry. It gathers useful data like the
* HTTP status code of the response or the name of the route that handles
* the request and add them as tags.
*/
final class TracingRequestListener extends AbstractTracingRequestListener
{
/**
* This method is called for each subrequest handled by the framework and
* starts a new {@see Transaction}.
*
* @param RequestListenerRequestEvent $event The event
*/
public function handleKernelRequestEvent(RequestListenerRequestEvent $event): void
{
if (!$event->isMasterRequest()) {
return;
}

/** @var Request $request */
$request = $event->getRequest();
$requestStartTime = $request->server->get('REQUEST_TIME_FLOAT', microtime(true));

$context = TransactionContext::fromSentryTrace($request->headers->get('sentry-trace', ''));
$context->setOp('http.server');
$context->setName(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo()));
$context->setStartTimestamp($requestStartTime);
$context->setTags($this->getTags($request));

$this->hub->setSpan($this->hub->startTransaction($context));
}

/**
* This method is called for each request handled by the framework and
* ends the tracing.
*
* @param FinishRequestEvent $event The event
*/
public function handleKernelFinishRequestEvent(FinishRequestEvent $event): void
{
if (!$event->isMasterRequest()) {
return;
}

$transaction = $this->hub->getTransaction();

if (null === $transaction) {
return;
}

$transaction->finish();
}

/**
* Gets the tags to attach to the transaction.
*
* @param Request $request The HTTP request
*
* @return array<string, string>
*/
private function getTags(Request $request): array
{
$client = $this->hub->getClient();
$tags = [
'net.host.port' => (string) $request->getPort(),
'http.method' => $request->getMethod(),
'http.url' => $request->getUri(),
'http.flavor' => $this->getHttpFlavor($request),
'route' => $this->getRouteName($request),
];

if (false !== filter_var($request->getHost(), \FILTER_VALIDATE_IP)) {
$tags['net.host.ip'] = $request->getHost();
} else {
$tags['net.host.name'] = $request->getHost();
}

if (null !== $request->getClientIp() && null !== $client && $client->getOptions()->shouldSendDefaultPii()) {
$tags['net.peer.ip'] = $request->getClientIp();
}

return $tags;
}

/**
* Gets the HTTP flavor from the request.
*
* @param Request $request The HTTP request
*/
protected function getHttpFlavor(Request $request): string
{
$protocolVersion = $request->getProtocolVersion();

if (str_starts_with($protocolVersion, 'HTTP/')) {
return substr($protocolVersion, \strlen('HTTP/'));
}

return $protocolVersion;
}
}
68 changes: 68 additions & 0 deletions src/EventListener/TracingSubRequestListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\EventListener;

use Sentry\Tracing\Span;
use Sentry\Tracing\SpanContext;
use Symfony\Component\HttpKernel\Event\FinishRequestEvent;

/**
* This event listener acts on the sub requests and starts a child span of the
* current transaction to gather performance data for each of them.
*/
final class TracingSubRequestListener extends AbstractTracingRequestListener
{
/**
* This method is called for each subrequest handled by the framework and
* traces each by starting a new {@see Span}.
*
* @param SubRequestListenerRequestEvent $event The event
*/
public function handleKernelRequestEvent(SubRequestListenerRequestEvent $event): void
{
if ($event->isMasterRequest()) {
return;
}

$request = $event->getRequest();
$span = $this->hub->getSpan();

if (null === $span) {
return;
}

$spanContext = new SpanContext();
$spanContext->setOp('http.server');
$spanContext->setDescription(sprintf('%s %s%s%s', $request->getMethod(), $request->getSchemeAndHttpHost(), $request->getBaseUrl(), $request->getPathInfo()));
$spanContext->setTags([
'http.method' => $request->getMethod(),
'http.url' => $request->getUri(),
'route' => $this->getRouteName($request),
]);

$this->hub->setSpan($span->startChild($spanContext));
}

/**
* This method is called for each subrequest handled by the framework and
* ends the tracing.
*
* @param FinishRequestEvent $event The event
*/
public function handleKernelFinishRequestEvent(FinishRequestEvent $event): void
{
if ($event->isMasterRequest()) {
return;
}

$span = $this->hub->getSpan();

if (null === $span) {
return;
}

$span->finish();
}
}
16 changes: 16 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@
<tag name="kernel.event_listener" event="kernel.finish_request" method="handleKernelFinishRequestEvent" priority="5" />
</service>

<service id="Sentry\SentryBundle\EventListener\TracingRequestListener" class="Sentry\SentryBundle\EventListener\TracingRequestListener">
<argument type="service" id="Sentry\State\HubInterface" />

<tag name="kernel.event_listener" event="kernel.request" method="handleKernelRequestEvent" priority="4" />
<tag name="kernel.event_listener" event="kernel.finish_request" method="handleKernelFinishRequestEvent" priority="5" />
<tag name="kernel.event_listener" event="kernel.response" method="handleKernelResponseEvent" priority="15" />
</service>

<service id="Sentry\SentryBundle\EventListener\TracingSubRequestListener" class="Sentry\SentryBundle\EventListener\TracingSubRequestListener">
<argument type="service" id="Sentry\State\HubInterface" />

<tag name="kernel.event_listener" event="kernel.request" method="handleKernelRequestEvent" priority="2" />
<tag name="kernel.event_listener" event="kernel.finish_request" method="handleKernelFinishRequestEvent" priority="10" />
<tag name="kernel.event_listener" event="kernel.response" method="handleKernelResponseEvent" priority="15" />
</service>

<service id="Sentry\SentryBundle\EventListener\MessengerListener" class="Sentry\SentryBundle\EventListener\MessengerListener">
<argument type="service" id="Sentry\State\HubInterface" />

Expand Down
Loading