Skip to content

Commit

Permalink
PWA-1311: New Relic is not being given useful transaction names for g…
Browse files Browse the repository at this point in the history
…raphql requests

 - pull query analysis into helper, add tests
  • Loading branch information
cspruiell committed Aug 26, 2021
1 parent 65231ae commit 50c88eb
Show file tree
Hide file tree
Showing 5 changed files with 487 additions and 88 deletions.
92 changes: 13 additions & 79 deletions app/code/Magento/GraphQl/Controller/GraphQl.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,6 @@

namespace Magento\GraphQl\Controller;

use GraphQL\Error\SyntaxError;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Language\Visitor;
use Magento\Framework\App\FrontControllerInterface;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\Request\Http;
Expand All @@ -27,8 +21,8 @@
use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\Framework\Webapi\Response;
use Magento\GraphQl\Helper\Query\Logger\LogData;
use Magento\GraphQl\Model\Query\ContextFactoryInterface;
use Magento\GraphQl\Model\Query\Logger\LoggerInterface;
use Magento\GraphQl\Model\Query\Logger\LoggerPool;

/**
Expand Down Expand Up @@ -97,6 +91,11 @@ class GraphQl implements FrontControllerInterface
*/
private $contextFactory;

/**
* @var LogData
*/
private $logDataHelper;

/**
* @var LoggerPool
*/
Expand All @@ -114,6 +113,7 @@ class GraphQl implements FrontControllerInterface
* @param JsonFactory|null $jsonFactory
* @param HttpResponse|null $httpResponse
* @param ContextFactoryInterface $contextFactory
* @param LogData $logDataHelper
* @param LoggerPool $loggerPool
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
Expand All @@ -129,6 +129,7 @@ public function __construct(
JsonFactory $jsonFactory = null,
HttpResponse $httpResponse = null,
ContextFactoryInterface $contextFactory = null,
LogData $logDataHelper = null,
LoggerPool $loggerPool = null
) {
$this->response = $response;
Expand All @@ -142,6 +143,7 @@ public function __construct(
$this->jsonFactory = $jsonFactory ?: ObjectManager::getInstance()->get(JsonFactory::class);
$this->httpResponse = $httpResponse ?: ObjectManager::getInstance()->get(HttpResponse::class);
$this->contextFactory = $contextFactory ?: ObjectManager::getInstance()->get(ContextFactoryInterface::class);
$this->logDataHelper = $logDataHelper ?: ObjectManager::getInstance()->get(LogData::class);
$this->loggerPool = $loggerPool ?: ObjectManager::getInstance()->get(LoggerPool::class);
}

Expand All @@ -168,11 +170,12 @@ public function dispatch(RequestInterface $request): ResponseInterface
// Temporal coupling is required for performance optimization
$this->queryFields->setQuery($query, $variables);

// log information about the query
$this->logQueryInformation($request, $data);

$schema = $this->schemaGenerator->generate();

// log information about the query
$queryInformation = $this->logDataHelper->getQueryInformation($request, $data, $schema);
$this->loggerPool->execute($queryInformation);

$result = $this->queryProcessor->process(
$schema,
$query,
Expand Down Expand Up @@ -214,73 +217,4 @@ private function getDataFromRequest(RequestInterface $request): array

return $data;
}

/**
* Logs information about the query
*
* @param RequestInterface $request
* @param array $data
* @throws SyntaxError
*/
private function logQueryInformation(RequestInterface $request, array $data)
{
$query = $data['query'] ?? '';
$queryInformation = [];
$queryInformation[LoggerInterface::HTTP_METHOD] = $request->getMethod();
$queryInformation[LoggerInterface::STORE_HEADER] = $request->getHeader('Store', '');
$queryInformation[LoggerInterface::CURRENCY_HEADER] = $request->getHeader('Currency', '');
$queryInformation[LoggerInterface::AUTH_HEADER_SET] = $request->getHeader('Authorization') ? 'true' : 'false';
$queryInformation[LoggerInterface::IS_CACHEABLE] = $request->getHeader('X-Magento-Cache-Id') ? 'true' : 'false';
$queryInformation[LoggerInterface::NUMBER_OF_QUERIES] = '';
$queryInformation[LoggerInterface::QUERY_NAMES] = $this->getOperationName($data);
$queryInformation[LoggerInterface::HAS_MUTATION] = str_contains($query, 'mutation') ? 'true' : 'false';
$queryInformation[LoggerInterface::QUERY_COMPLEXITY] = $this->getFieldCount($query);
$queryInformation[LoggerInterface::QUERY_LENGTH] = $request->getHeader('Content-Length');

$this->loggerPool->execute($queryInformation);
}

/**
* Get GraphQL query operation name
*
* @param array $data
* @return string
*/
private function getOperationName(array $data): string
{
if (isset($data['operationName']) && is_string($data['operationName']) && $data['operationName'] !== '') {
return $data['operationName'];
}

$fields = $this->queryFields->getFieldsUsedInQuery();
return current($fields) ?: 'operationNameNotFound';
}

/**
* Gets the field count
*
* @param string $query
* @return int
* @throws SyntaxError
* @throws \Exception
*/
private function getFieldCount(string $query): int
{
if (!empty($query)) {
$totalFieldCount = 0;
$queryAst = Parser::parse(new Source($query ?: '', 'GraphQL'));
Visitor::visit(
$queryAst,
[
'leave' => [
NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) {
$totalFieldCount++;
}
]
]
);
return $totalFieldCount;
}
return 0;
}
}
88 changes: 88 additions & 0 deletions app/code/Magento/GraphQl/Helper/Query/Logger/LogData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);

namespace Magento\GraphQl\Helper\Query\Logger;

use GraphQL\Error\SyntaxError;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\NodeKind;
use GraphQL\Language\Parser;
use GraphQL\Language\Source;
use GraphQL\Language\Visitor;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\GraphQl\Schema;
use Magento\GraphQl\Model\Query\Logger\LoggerInterface;

/**
* Helper class to collect data for logging GraphQl queries
*/
class LogData
{
/**
* Extracts relevant information about the query
*
* @param RequestInterface $request
* @param array $data
* @param Schema $schema
* @return array
*
* @throws SyntaxError
*/
public function getQueryInformation(RequestInterface $request, array $data, Schema $schema) : array
{
$query = $data['query'] ?? '';
$queryInformation = [];
$queryInformation[LoggerInterface::HTTP_METHOD] = $request->getMethod();
$queryInformation[LoggerInterface::STORE_HEADER] = $request->getHeader('Store') ?: '';
$queryInformation[LoggerInterface::CURRENCY_HEADER] = $request->getHeader('Currency') ?: '';
$queryInformation[LoggerInterface::HAS_AUTH_HEADER] = $request->getHeader('Authorization') ? 'true' : 'false';
$queryInformation[LoggerInterface::IS_CACHEABLE] = $request->getHeader('X-Magento-Cache-Id') ? 'true' : 'false';
$queryInformation[LoggerInterface::QUERY_LENGTH] = $request->getHeader('Content-Length') ?: '';

$schemaConfig = $schema->getConfig();
$configMutationFields = $schemaConfig->getMutation()->getFields();
$configQueryFields = $schemaConfig->getQuery()->getFields();
$queryInformation[LoggerInterface::HAS_MUTATION] = count($configMutationFields) > 0 ? 'true' : 'false';
$queryInformation[LoggerInterface::NUMBER_OF_QUERIES] =
count($configMutationFields) + count($configQueryFields);

$queryNames = array_merge(array_keys($configMutationFields), array_keys($configQueryFields));
$queryInformation[LoggerInterface::QUERY_NAMES] =
count($queryNames) > 0 ? implode(", ", $queryNames) : 'operationNameNotFound';
$queryInformation[LoggerInterface::QUERY_COMPLEXITY] = $this->getFieldCount($query);

return $queryInformation;
}

/**
* Gets the field count
*
* @param string $query
* @return int
* @throws SyntaxError
* @throws \Exception
*/
private function getFieldCount(string $query): int
{
if (!empty($query)) {
$totalFieldCount = 0;
$queryAst = Parser::parse(new Source($query ?: '', 'GraphQL'));
Visitor::visit(
$queryAst,
[
'leave' => [
NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) {
$totalFieldCount++;
}
]
]
);
return $totalFieldCount;
}
return 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface LoggerInterface
const QUERY_NAMES = 'GraphQlQueryNames';
const STORE_HEADER = 'GraphQlStoreHeader';
const CURRENCY_HEADER = 'GraphQlCurrencyHeader';
const AUTH_HEADER_SET = 'GraphQlAuthHeaderSet';
const HAS_AUTH_HEADER = 'GraphQlHasAuthHeader';
const HTTP_METHOD = 'GraphQlHttpMethod';
const HAS_MUTATION = 'GraphQlHasMutation';
const IS_CACHEABLE = 'GraphQlIsCacheable';
Expand Down
8 changes: 0 additions & 8 deletions app/code/Magento/GraphQl/Model/Query/Logger/LoggerPool.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,11 @@ class LoggerPool implements LoggerInterface
private $loggers;

/**
* @var LoggerInterfaceFactory
*/
private $loggerFactory;

/**
* @param LoggerInterfaceFactory $loggerFactory
* @param LoggerInterface[] $loggers
*/
public function __construct(
LoggerInterfaceFactory $loggerFactory,
$loggers = []
) {
$this->loggerFactory = $loggerFactory;
$this->loggers = $loggers;
}

Expand Down
Loading

0 comments on commit 50c88eb

Please sign in to comment.