Contact us
Backend 3 min read · 10/2022

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

bash
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

yaml
sentry:
    dsn: '%env(SENTRY_DSN)%'
    options:
        traces_sample_rate: '%env(float:SENTRY_PERFORMANCE_RATIO)%'

Where

  • traces_sample_rate in the options section 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

yaml
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
<?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

yaml
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.

Share
LinkedIn Facebook

Need more than an article?

Get in touch with no obligation – a few sentences about what you are dealing with are enough.

Read on

Similar articles

More notes from operations and development.

All advice