Sentry Performance, PHP and GraphQL
How to use Sentry Performance to monitor the performance of a PHP application, including the performance of a GraphQL API.
Sentry is an excellent tool for monitoring and logging application errors. You can easily use it either as a SaaS service or on your own infrastructure thanks to the available Docker image.
Adding support to a Symfony project is very easy thanks to Composer
composer require sentry/sentry-symfony
In this post I describe how to set up Sentry as a performance monitor for a PHP Symfony application, including a setup for clear monitoring of a GraphQL API implemented with the Overblog GraphQL Bundle. For a really basic setup, all you need is a simple change to the config/packages/sentry.yaml file, as follows
sentry:
dsn: '%env(SENTRY_DSN)%'
options:
traces_sample_rate: '%env(float:SENTRY_PERFORMANCE_RATIO)%'
Where
traces_sample_ratein theoptionssection is the ratio of measured transactions as a number between 0 and 1 (0.2 means that 20% of transactions are measured). On the dev stage we happily use 1.0, in production something like 0.02. 0 turns measurement off.
Additional settings let you specify what exactly should be monitored (dbal, twig, cache, http_client, console, see the Symfony options), so enabling or disabling specific measurements depends on the particular project and its needs. All that is left now is to add the env variables to your .env files, for example like this
SENTRY_DSN=https://sentry.io/dsn-vaseho-projektu
SENTRY_PERFORMANCE_RATIO=0.05
If everything is set up correctly, the performance characteristics of the measured transactions should start showing up with the very first requests to the application, identified by the HTTP method and the request URL, e.g. GET https://www.id-sign.com/poradna. This may be enough for classic websites where every piece of content has its own URL. However, if we want to monitor the performance of a GraphQL API, which has a single endpoint for all content, we need to name the individual API calls after the query being called. Ideally, we also want to add the authenticated user, the exact query and the variables used.
In our projects we use the Overblog GraphQL Bundle, which is very easy to extend for this purpose. All it takes is one of its events, namely graphql.pre_executor. So let's prepare a suitable EventListener, named e.g. GraphQLListener:
<?php
namespace App\EventListener;
use App\Entity\User;
use Overblog\GraphQLBundle\Event\ExecutorArgumentsEvent;
use Sentry\Breadcrumb;
use Sentry\SentrySdk;
use Sentry\State\Scope;
use Symfony\Bundle\SecurityBundle\Security;
use function Sentry\addBreadcrumb;
use function Sentry\captureException;
use function Sentry\configureScope;
readonly class GraphQLListener
{
public function __construct(private Security $security)
{
}
public function onPreExecutor(ExecutorArgumentsEvent $event): void
{
$name = $event->getOperationName() ?? 'Unknown operation';
$sentryTransaction = SentrySdk::getCurrentHub()->getTransaction();
$sentryTransaction?->setName($name);
$authUser = $this->security->getUser();
if ($authUser instanceof User) {
configureScope(function (Scope $scope) use ($authUser): void {
$scope->setUser([
'id' => $authUser->getId(),
]);
});
}
try {
addBreadcrumb(
new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
'GraphQL',
'query',
[
'userId' => $authUser?->getId(),
'operationName' => $name,
'query' => $event->getRequestString(),
'variables' => json_encode($event->getVariableValue(), JSON_THROW_ON_ERROR),
]
)
);
} catch (\JsonException $exception) {
captureException($exception);
}
}
}
and register it in config/services.yaml so that it is called for the event above
App\EventListener\GraphQLListener:
tags:
- { name: kernel.event_listener, event: graphql.pre_executor, method: onPreExecutor }
With this simple extension, our individual transactions are
- named after the query being called – see
$sentryTransaction?->setName(...) - enriched with information about the logged-in user – see
$scope->setUser(...) - and in the breadcrumbs of the whole transaction we can also see the exact query and the variables used – see
addBreadcrumb(...)
There are many more configuration options; for further details I recommend going through the official documentation.
Finally, an important point that follows from the nature of PHP and its synchronous execution. All measurement and communication with Sentry (most likely in most cases) happens in the same process that handles the measured request, which affects how fast the request itself is processed. Sentry offers a solution that reduces this impact almost to zero – a local proxy using Relay, again see the official documentation.