Skip to content

Profiler feature #696

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 1 commit into from
Jul 8, 2020
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ Documentation
- [Disable introspection](docs/security/disable_introspection.md)
- [Errors handling](docs/error-handling/index.md)
- [Events](docs/events/index.md)
- [Profiler](docs/profiler/index.md)

Talks and slides to help you start
----------------------------------
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@
"symfony/process": "^4.3 || ^5.0",
"symfony/security-bundle": "^4.3 || ^5.0",
"symfony/validator": "^4.3 || ^5.0",
"symfony/yaml": "^4.3 || ^5.0"
"symfony/var-dumper": "^4.3 || ^5.0",
"symfony/yaml": "^4.3 || ^5.0",
"twig/twig": "^2.10|^3.0"
},
"extra": {
"branch-alias": {
Expand Down
20 changes: 20 additions & 0 deletions docs/profiler/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
> _The profiler feature was introduced in the version **1.0**_

# Profiler

This bundle provides a profiler to monitor your GraphQL queries and mutations.

## Configuration

In order to display only GraphQL related requests, the profiler will filter requests based on the requested url.
By default, it will display requests matching the configured endpoint url (ie. The route `overblog_graphql_endpoint`).

If you need to change the behavior (for example if you have parameters in your endpoint url), you can change the matching with the following option:

```yaml
overblog_graphql:
profiler:
query_match: my_string_to_match
```

In the example above, only the requests containing the string `my_string_to_match` will be displayed.
22 changes: 21 additions & 1 deletion phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ parameters:

-
message: "#^Cannot call method end\\(\\) on Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\|null\\.$#"
count: 1
count: 2
path: src/DependencyInjection/Configuration.php

-
Expand Down Expand Up @@ -1224,3 +1224,23 @@ parameters:
message: "#^Array \\(array<Symfony\\\\Component\\\\Validator\\\\Mapping\\\\ClassMetadataInterface>\\) does not accept Symfony\\\\Component\\\\Validator\\\\Mapping\\\\MetadataInterface\\.$#"
count: 1
path: src/Validator/InputValidator.php

-
message: "#^Cannot call method loadProfile\\(\\) on Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler|null\\.$#"
count: 1
path: src/Controller/ProfilerController.php

-
message: "#^Parameter \\#1 \\$ip of method Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler::find\\(\\) expects string, null given\\.$#"
count: 1
path: src/Controller/ProfilerController.php

-
message: "#^Parameter \\#5 \\$start of method Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler::find\\(\\) expects string, null given\\.$#"
count: 1
path: src/Controller/ProfilerController.php

-
message: "#^Parameter \\#6 \\$end of method Symfony\\\\Component\\\\HttpKernel\\\\Profiler\\\\Profiler::find\\(\\) expects string, null given\\.$#"
count: 1
path: src/Controller/ProfilerController.php
72 changes: 72 additions & 0 deletions src/Controller/ProfilerController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Overblog\GraphQLBundle\Controller;

use GraphQL\Utils\SchemaPrinter;
use Overblog\GraphQLBundle\Request\Executor as RequestExecutor;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Profiler\Profiler;
use Symfony\Component\Routing\RouterInterface;
use Twig\Environment;

class ProfilerController
{
private ?Profiler $profiler;
private ?Environment $twig;
private string $endpointUrl;
private RequestExecutor $requestExecutor;
private ?string $queryMatch;

public function __construct(?Profiler $profiler, ?Environment $twig, RouterInterface $router, RequestExecutor $requestExecutor, ?string $queryMatch)
{
$this->profiler = $profiler;
$this->twig = $twig;
$this->endpointUrl = $router->generate('overblog_graphql_endpoint');
$this->requestExecutor = $requestExecutor;
$this->queryMatch = $queryMatch;
}

/**
* @throws ServiceNotFoundException
*/
public function __invoke(Request $request, string $token): Response
{
if (null === $this->profiler) {
throw new ServiceNotFoundException('The profiler must be enabled.');
}

if (null === $this->twig) {
throw new ServiceNotFoundException('The GraphQL Profiler require twig');
}

$this->profiler->disable();

$profile = $this->profiler->loadProfile($token);

$tokens = \array_map(function ($tokenData) {
$profile = $this->profiler->loadProfile($tokenData['token']);
$graphql = $profile ? $profile->getCollector('graphql') : null;
$tokenData['graphql'] = $graphql;

return $tokenData;
}, $this->profiler->find(null, $this->queryMatch ?: $this->endpointUrl, '100', 'POST', null, null, null));

$schemas = [];
foreach ($this->requestExecutor->getSchemasNames() as $schemaName) {
$schemas[$schemaName] = SchemaPrinter::doPrint($this->requestExecutor->getSchema($schemaName));
}

return new Response($this->twig->render('@OverblogGraphQL/profiler/graphql.html.twig', [
'request' => $request,
'profile' => $profile,
'tokens' => $tokens,
'token' => $token,
'panel' => null,
'schemas' => $schemas,
]), 200, ['Content-Type' => 'text/html']);
}
}
169 changes: 169 additions & 0 deletions src/DataCollector/GraphQLCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

declare(strict_types=1);

namespace Overblog\GraphQLBundle\DataCollector;

use GraphQL\Error\SyntaxError;
use GraphQL\Language\AST\DocumentNode;
use GraphQL\Language\AST\FieldNode;
use GraphQL\Language\AST\OperationDefinitionNode;
use GraphQL\Language\Parser;
use Overblog\GraphQLBundle\Event\ExecutorResultEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\DataCollector;

class GraphQLCollector extends DataCollector
{
/**
* GraphQL Batchs executed.
*/
protected array $batches = [];

public function collect(Request $request, Response $response, \Throwable $exception = null): void
{
$error = false;
$count = 0;
foreach ($this->batches as $batch) {
if (isset($batch['error'])) {
$error = true;
}
$count += $batch['count'];
}

$this->data = [
'schema' => $request->attributes->get('_route_params')['schemaName'] ?? 'default',
'batches' => $this->batches,
'count' => $count,
'error' => $error,
];
}

/**
* Check if we have an error.
*/
public function getError(): bool
{
return $this->data['error'] ?? false;
}

/**
* Count the number of executed queries.
*/
public function getCount(): int
{
return $this->data['count'] ?? 0;
}

/**
* Return the targeted schema.
*/
public function getSchema(): string
{
return $this->data['schema'] ?? 'default';
}

/**
* Return the list of executed batch.
*/
public function getBatches(): array
{
return $this->data['batches'] ?? [];
}

/**
* {@inheritdoc}
*/
public function reset(): void
{
$this->data = [];
}

/**
* {@inheritdoc}
*/
public function getName()
{
return 'graphql';
}

/**
* Hook into the GraphQL events to populate the collector.
*/
public function onPostExecutor(ExecutorResultEvent $event): void
{
$executorArgument = $event->getExecutorArguments();
$queryString = $executorArgument->getRequestString();
$operationName = $executorArgument->getOperationName();
$variables = $executorArgument->getVariableValue();
$queryTime = \microtime(true) - $executorArgument->getStartTime();

$result = $event->getResult()->toArray();

$batch = [
'queryString' => $queryString,
'queryTime' => $queryTime,
'variables' => $this->cloneVar($variables),
'result' => $this->cloneVar($result),
'count' => 0,
];

try {
$parsed = Parser::parse($queryString);
$batch['graphql'] = $this->extractGraphql($parsed, $operationName);
if (isset($batch['graphql']['fields'])) {
$batch['count'] += \count($batch['graphql']['fields']);
}
$error = $result['errors'][0] ?? false;
if ($error) {
$batch['error'] = [
'message' => $error['message'],
'location' => $error['locations'][0] ?? false,
];
}
} catch (SyntaxError $error) {
$location = $error->getLocations()[0] ?? false;
$batch['error'] = ['message' => $error->getMessage(), 'location' => $location];
}

$this->batches[] = $batch;
}

/**
* Extract GraphQL Information from the documentNode.
*/
protected function extractGraphql(DocumentNode $document, ?string $operationName): array
{
$operation = null;
$fields = [];

foreach ($document->definitions as $definition) {
if ($definition instanceof OperationDefinitionNode) {
$definitionOperation = $definition->name->value ?? null;
if ($operationName != $definitionOperation) {
continue;
}

$operation = $definition->operation;
foreach ($definition->selectionSet->selections as $selection) {
if ($selection instanceof FieldNode) {
$name = $selection->name->value;
$alias = $selection->alias ? $selection->alias->value : null;

$fields[] = [
'name' => $name,
'alias' => $alias,
];
}
}
}
}

return [
'operation' => $operation,
'operationName' => $operationName,
'fields' => $fields,
];
}
}
18 changes: 17 additions & 1 deletion src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public function getConfigTreeBuilder()
->append($this->servicesSection())
->append($this->securitySection())
->append($this->doctrineSection())
->append($this->profilerSection())
->end();

return $treeBuilder;
Expand Down Expand Up @@ -210,7 +211,7 @@ private function definitionsSchemaSection()
->arrayNode('resolver_maps')
->defaultValue([])
->prototype('scalar')->end()
->setDeprecated('The "%path%.%node%" configuration is deprecated since version 0.13 and will be removed in 0.14. Add the "overblog_graphql.resolver_map" tag to the services instead.')
->setDeprecated('The "%path%.%node%" configuration is deprecated since version 0.13 and will be removed in 0.14. Add the "overblog_graphql.resolver_map" tag to the services instead.', '0.13')
->end()
->arrayNode('types')
->defaultValue([])
Expand Down Expand Up @@ -290,6 +291,21 @@ private function doctrineSection()
return $node;
}

private function profilerSection()
{
$builder = new TreeBuilder('profiler');
/** @var ArrayNodeDefinition $node */
$node = self::getRootNodeWithoutDeprecation($builder, 'profiler');
$node
->addDefaultsIfNotSet()
->children()
->scalarNode('query_match')->defaultNull()->end()
->end()
;

return $node;
}

/**
* @param string $name
*
Expand Down
7 changes: 7 additions & 0 deletions src/DependencyInjection/OverblogGraphQLExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function load(array $configs, ContainerBuilder $container): void
$this->setConfigBuilders($config, $container);
$this->setDebugListener($config, $container);
$this->setDefinitionParameters($config, $container);
$this->setProfilerParameters($config, $container);
$this->setClassLoaderListener($config, $container);
$this->setCompilerCacheWarmer($config, $container);
$this->registerForAutoconfiguration($container);
Expand Down Expand Up @@ -82,6 +83,7 @@ private function loadConfigFiles(ContainerBuilder $container): void
$loader->load('expression_language_functions.yaml');
$loader->load('definition_config_processors.yaml');
$loader->load('aliases.yaml');
$loader->load('profiler.yaml');
}

private function registerForAutoconfiguration(ContainerBuilder $container): void
Expand Down Expand Up @@ -155,6 +157,11 @@ private function setDefinitionParameters(array $config, ContainerBuilder $contai
$container->setParameter($this->getAlias().'.use_experimental_executor', $config['definitions']['use_experimental_executor']);
}

private function setProfilerParameters(array $config, ContainerBuilder $container): void
{
$container->setParameter($this->getAlias().'.profiler.query_match', $config['profiler']['query_match']);
}

private function setBatchingMethod(array $config, ContainerBuilder $container): void
{
$container->setParameter($this->getAlias().'.batching_method', $config['batching_method']);
Expand Down
Loading