Skip to content

Commit 1483c7a

Browse files
Merge pull request #7084 from magento-honey-badgers/PWA-1311
[honey] PWA-1311: New Relic is not being given useful transaction names for graphql requests
2 parents 5734ca9 + 160d7b7 commit 1483c7a

File tree

8 files changed

+662
-9
lines changed

8 files changed

+662
-9
lines changed

app/code/Magento/GraphQl/Controller/GraphQl.php

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,22 @@
88
namespace Magento\GraphQl\Controller;
99

1010
use Magento\Framework\App\FrontControllerInterface;
11+
use Magento\Framework\App\ObjectManager;
1112
use Magento\Framework\App\Request\Http;
1213
use Magento\Framework\App\RequestInterface;
14+
use Magento\Framework\App\Response\Http as HttpResponse;
1315
use Magento\Framework\App\ResponseInterface;
16+
use Magento\Framework\Controller\Result\JsonFactory;
1417
use Magento\Framework\GraphQl\Exception\ExceptionFormatter;
18+
use Magento\Framework\GraphQl\Query\Fields as QueryFields;
1519
use Magento\Framework\GraphQl\Query\QueryProcessor;
1620
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
1721
use Magento\Framework\GraphQl\Schema\SchemaGeneratorInterface;
1822
use Magento\Framework\Serialize\SerializerInterface;
1923
use Magento\Framework\Webapi\Response;
20-
use Magento\Framework\App\Response\Http as HttpResponse;
21-
use Magento\Framework\GraphQl\Query\Fields as QueryFields;
22-
use Magento\Framework\Controller\Result\JsonFactory;
23-
use Magento\Framework\App\ObjectManager;
24+
use Magento\GraphQl\Helper\Query\Logger\LogData;
2425
use Magento\GraphQl\Model\Query\ContextFactoryInterface;
26+
use Magento\GraphQl\Model\Query\Logger\LoggerPool;
2527

2628
/**
2729
* Front controller for web API GraphQL area.
@@ -89,6 +91,16 @@ class GraphQl implements FrontControllerInterface
8991
*/
9092
private $contextFactory;
9193

94+
/**
95+
* @var LogData
96+
*/
97+
private $logDataHelper;
98+
99+
/**
100+
* @var LoggerPool
101+
*/
102+
private $loggerPool;
103+
92104
/**
93105
* @param Response $response
94106
* @param SchemaGeneratorInterface $schemaGenerator
@@ -101,6 +113,8 @@ class GraphQl implements FrontControllerInterface
101113
* @param JsonFactory|null $jsonFactory
102114
* @param HttpResponse|null $httpResponse
103115
* @param ContextFactoryInterface $contextFactory
116+
* @param LogData $logDataHelper
117+
* @param LoggerPool $loggerPool
104118
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
105119
*/
106120
public function __construct(
@@ -114,7 +128,9 @@ public function __construct(
114128
QueryFields $queryFields,
115129
JsonFactory $jsonFactory = null,
116130
HttpResponse $httpResponse = null,
117-
ContextFactoryInterface $contextFactory = null
131+
ContextFactoryInterface $contextFactory = null,
132+
LogData $logDataHelper = null,
133+
LoggerPool $loggerPool = null
118134
) {
119135
$this->response = $response;
120136
$this->schemaGenerator = $schemaGenerator;
@@ -127,6 +143,8 @@ public function __construct(
127143
$this->jsonFactory = $jsonFactory ?: ObjectManager::getInstance()->get(JsonFactory::class);
128144
$this->httpResponse = $httpResponse ?: ObjectManager::getInstance()->get(HttpResponse::class);
129145
$this->contextFactory = $contextFactory ?: ObjectManager::getInstance()->get(ContextFactoryInterface::class);
146+
$this->logDataHelper = $logDataHelper ?: ObjectManager::getInstance()->get(LogData::class);
147+
$this->loggerPool = $loggerPool ?: ObjectManager::getInstance()->get(LoggerPool::class);
130148
}
131149

132150
/**
@@ -136,15 +154,18 @@ public function __construct(
136154
* @return ResponseInterface
137155
* @since 100.3.0
138156
*/
139-
public function dispatch(RequestInterface $request) : ResponseInterface
157+
public function dispatch(RequestInterface $request): ResponseInterface
140158
{
141159
$statusCode = 200;
142160
$jsonResult = $this->jsonFactory->create();
161+
$data = $this->getDataFromRequest($request);
162+
$result = [];
163+
164+
$schema = null;
143165
try {
144166
/** @var Http $request */
145167
$this->requestProcessor->validateRequest($request);
146168

147-
$data = $this->getDataFromRequest($request);
148169
$query = $data['query'] ?? '';
149170
$variables = $data['variables'] ?? null;
150171

@@ -160,14 +181,21 @@ public function dispatch(RequestInterface $request) : ResponseInterface
160181
$data['variables'] ?? []
161182
);
162183
} catch (\Exception $error) {
163-
$result['errors'] = isset($result) && isset($result['errors']) ? $result['errors'] : [];
184+
$result['errors'] = isset($result['errors']) ? $result['errors'] : [];
164185
$result['errors'][] = $this->graphQlError->create($error);
165186
$statusCode = ExceptionFormatter::HTTP_GRAPH_QL_SCHEMA_ERROR_STATUS;
166187
}
167188

168189
$jsonResult->setHttpResponseCode($statusCode);
169190
$jsonResult->setData($result);
170191
$jsonResult->renderResult($this->httpResponse);
192+
193+
// log information about the query, unless it is an introspection query
194+
if (strpos($data['query'], 'IntrospectionQuery') === false) {
195+
$queryInformation = $this->logDataHelper->getLogData($request, $data, $schema, $this->httpResponse);
196+
$this->loggerPool->execute($queryInformation);
197+
}
198+
171199
return $this->httpResponse;
172200
}
173201

@@ -177,7 +205,7 @@ public function dispatch(RequestInterface $request) : ResponseInterface
177205
* @param RequestInterface $request
178206
* @return array
179207
*/
180-
private function getDataFromRequest(RequestInterface $request) : array
208+
private function getDataFromRequest(RequestInterface $request): array
181209
{
182210
/** @var Http $request */
183211
if ($request->isPost()) {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\GraphQl\Helper\Query\Logger;
9+
10+
use GraphQL\Error\SyntaxError;
11+
use GraphQL\Language\AST\Node;
12+
use GraphQL\Language\AST\NodeKind;
13+
use GraphQL\Language\Parser;
14+
use GraphQL\Language\Source;
15+
use GraphQL\Language\Visitor;
16+
use Magento\Framework\App\RequestInterface;
17+
use Magento\Framework\App\Response\Http as HttpResponse;
18+
use Magento\Framework\GraphQl\Schema;
19+
use Magento\GraphQl\Model\Query\Logger\LoggerInterface;
20+
21+
/**
22+
* Helper class to collect data for logging GraphQl requests
23+
*/
24+
class LogData
25+
{
26+
/**
27+
* Extracts relevant information about the request
28+
*
29+
* @param RequestInterface $request
30+
* @param array $data
31+
* @param Schema|null $schema
32+
* @param HttpResponse|null $response
33+
* @return array
34+
* @SuppressWarnings(PHPMD.EmptyCatchBlock)
35+
*/
36+
public function getLogData(
37+
RequestInterface $request,
38+
array $data,
39+
?Schema $schema,
40+
?HttpResponse $response
41+
) : array {
42+
$logData = [];
43+
$logData = array_merge($logData, $this->gatherRequestInformation($request));
44+
45+
try {
46+
$complexity = $this->getFieldCount($data['query'] ?? '');
47+
$logData[LoggerInterface::COMPLEXITY] = $complexity;
48+
if ($schema) {
49+
$logData = array_merge($logData, $this->gatherQueryInformation($schema));
50+
}
51+
} catch (\Exception $exception) {} //@codingStandardsIgnoreLine
52+
53+
if ($response) {
54+
$logData = array_merge($logData, $this->gatherResponseInformation($response));
55+
}
56+
57+
return $logData;
58+
}
59+
60+
/**
61+
* Gets the information needed from the request
62+
*
63+
* @param RequestInterface $request
64+
* @return array
65+
*/
66+
private function gatherRequestInformation(RequestInterface $request) : array
67+
{
68+
$requestInformation[LoggerInterface::HTTP_METHOD] = $request->getMethod();
69+
$requestInformation[LoggerInterface::STORE_HEADER] = $request->getHeader('Store') ?: '';
70+
$requestInformation[LoggerInterface::CURRENCY_HEADER] = $request->getHeader('Currency') ?: '';
71+
$requestInformation[LoggerInterface::HAS_AUTH_HEADER] = $request->getHeader('Authorization') ? 'true' : 'false';
72+
$requestInformation[LoggerInterface::REQUEST_LENGTH] = $request->getHeader('Content-Length') ?: '';
73+
return $requestInformation;
74+
}
75+
76+
/**
77+
* Gets the information needed from the schema
78+
*
79+
* @param Schema $schema
80+
* @return array
81+
*/
82+
private function gatherQueryInformation(Schema $schema) : array
83+
{
84+
$schemaConfig = $schema->getConfig();
85+
$mutationOperations = $schemaConfig->getMutation()->getFields();
86+
$queryOperations = $schemaConfig->getQuery()->getFields();
87+
$queryInformation[LoggerInterface::HAS_MUTATION] = count($mutationOperations) > 0 ? 'true' : 'false';
88+
$queryInformation[LoggerInterface::NUMBER_OF_OPERATIONS] =
89+
count($mutationOperations) + count($queryOperations);
90+
$operationNames = array_merge(array_keys($mutationOperations), array_keys($queryOperations));
91+
$queryInformation[LoggerInterface::OPERATION_NAMES] =
92+
count($operationNames) > 0 ? implode(",", $operationNames) : 'operationNameNotFound';
93+
return $queryInformation;
94+
}
95+
96+
/**
97+
* Gets the information needed from the response
98+
*
99+
* @param HttpResponse $response
100+
* @return array
101+
*/
102+
private function gatherResponseInformation(HttpResponse $response) : array
103+
{
104+
$responseInformation[LoggerInterface::X_MAGENTO_CACHE_ID] =
105+
$response->getHeader('X-Magento-Cache-Id')
106+
? $response->getHeader('X-Magento-Cache_Id')->getFieldValue()
107+
: '';
108+
$responseInformation[LoggerInterface::HTTP_RESPONSE_CODE] = $response->getHttpResponseCode();
109+
return $responseInformation;
110+
}
111+
112+
/**
113+
* Gets the field count for the whole request
114+
*
115+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
116+
*
117+
* @param string $query
118+
* @return int
119+
* @throws SyntaxError
120+
* @throws /Exception
121+
*/
122+
private function getFieldCount(string $query): int
123+
{
124+
if (!empty($query)) {
125+
$totalFieldCount = 0;
126+
$queryAst = Parser::parse(new Source($query ?: '', 'GraphQL'));
127+
Visitor::visit(
128+
$queryAst,
129+
[
130+
'leave' => [
131+
NodeKind::FIELD => function (Node $node) use (&$totalFieldCount) {
132+
$totalFieldCount++;
133+
}
134+
]
135+
]
136+
);
137+
return $totalFieldCount;
138+
}
139+
return 0;
140+
}
141+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\GraphQl\Model\Query\Logger;
8+
9+
/**
10+
* Defines Logger interface for GraphQL queries
11+
*/
12+
interface LoggerInterface
13+
{
14+
/**
15+
* Names of properties to be logged
16+
*/
17+
const NUMBER_OF_OPERATIONS = 'GraphQlNumberOfOperations';
18+
const OPERATION_NAMES = 'GraphQlOperationNames';
19+
const STORE_HEADER = 'GraphQlStoreHeader';
20+
const CURRENCY_HEADER = 'GraphQlCurrencyHeader';
21+
const HAS_AUTH_HEADER = 'GraphQlHasAuthHeader';
22+
const HTTP_METHOD = 'GraphQlHttpMethod';
23+
const HAS_MUTATION = 'GraphQlHasMutation';
24+
const COMPLEXITY = 'GraphQlComplexity';
25+
const REQUEST_LENGTH = 'GraphQlRequestLength';
26+
const HTTP_RESPONSE_CODE = 'GraphQlHttpResponseCode';
27+
const X_MAGENTO_CACHE_ID = 'GraphQlXMagentoCacheId';
28+
29+
/**
30+
* Execute logger
31+
*
32+
* @param array $queryDetails
33+
* @return void
34+
*/
35+
public function execute(array $queryDetails);
36+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
7+
namespace Magento\GraphQl\Model\Query\Logger;
8+
9+
use InvalidArgumentException;
10+
11+
/**
12+
* GraphQl logger pool
13+
*/
14+
class LoggerPool implements LoggerInterface
15+
{
16+
/**
17+
* @var LoggerInterface[]
18+
*/
19+
private $loggers;
20+
21+
/**
22+
* @param LoggerInterface[] $loggers
23+
*/
24+
public function __construct(
25+
$loggers = []
26+
) {
27+
$this->loggers = $loggers;
28+
}
29+
30+
/**
31+
* Logs details of GraphQl query
32+
*
33+
* @param array $queryDetails
34+
* @return void
35+
*/
36+
public function execute(
37+
array $queryDetails
38+
) {
39+
foreach ($this->loggers as $logger) {
40+
$logger->execute($queryDetails);
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)