Skip to content

Commit 0b59677

Browse files
author
Mikolaj Misiurewicz
committed
feat(debug): new solr debug panel in symfony profiler
1 parent 0b7161e commit 0b59677

File tree

4 files changed

+376
-1
lines changed

4 files changed

+376
-1
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@
3030
"twig/twig": "~1.12|~2.0",
3131
"markup/json": "^0.1",
3232
"doctrine/collections": "^1.5",
33-
"doctrine/orm": "^2.4"
33+
"doctrine/orm": "^2.4",
34+
"ext-json": "*"
3435
},
3536
"require-dev": {
3637
"phpunit/phpunit": "^7.5",
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Markup\NeedleBundle\DataCollector;
5+
6+
use Psr\Log\LoggerInterface;
7+
use Solarium\Core\Client\Endpoint as SolariumEndpoint;
8+
use Solarium\Core\Client\Request as SolariumRequest;
9+
use Solarium\Core\Client\Response as SolariumResponse;
10+
use Solarium\Core\Event\Events as SolariumEvents;
11+
use Solarium\Core\Event\PostExecuteRequest as SolariumPostExecuteRequestEvent;
12+
use Solarium\Core\Event\PreExecuteRequest as SolariumPreExecuteRequestEvent;
13+
use Solarium\Core\Plugin\AbstractPlugin as SolariumPlugin;
14+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
15+
use Symfony\Component\HttpFoundation\Request as HttpRequest;
16+
use Symfony\Component\HttpFoundation\Response as HttpResponse;
17+
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
18+
19+
/**
20+
* Copied from Nelmio\SolariumBundle\Logger and modified.
21+
*/
22+
class SolrDataCollector extends SolariumPlugin implements DataCollectorInterface, \Serializable
23+
{
24+
/**
25+
* @var array
26+
*/
27+
protected $data = [];
28+
29+
/**
30+
* @var array
31+
*/
32+
protected $queries = [];
33+
34+
/**
35+
* @var SolariumRequest|null
36+
*/
37+
protected $currentRequest;
38+
39+
/**
40+
* @var float|null
41+
*/
42+
protected $currentStartTime;
43+
44+
/**
45+
* @var SolariumEndpoint|null
46+
*/
47+
protected $currentEndpoint;
48+
49+
/**
50+
* @var EventDispatcherInterface[]
51+
*/
52+
protected $eventDispatchers = [];
53+
54+
/**
55+
* @var LoggerInterface
56+
*/
57+
protected $logger;
58+
59+
public function setLogger(LoggerInterface $logger): void
60+
{
61+
$this->logger = $logger;
62+
}
63+
64+
/**
65+
* Parse the solr queries logged and save for use in debugging activities.
66+
*
67+
* @param HttpRequest $request
68+
* @param HttpResponse $response
69+
* @param \Exception|null $exception
70+
*/
71+
public function collect(HttpRequest $request, HttpResponse $response, \Exception $exception = null): void
72+
{
73+
if (isset($this->currentRequest)) {
74+
$this->failCurrentRequest();
75+
}
76+
77+
$time = 0.0;
78+
foreach ($this->queries as $queryStruct) {
79+
$time += $queryStruct['durationMs'];
80+
}
81+
$this->data = [
82+
'queries' => $this->queries,
83+
'totalTimeMs' => $time,
84+
];
85+
}
86+
87+
public function getName(): string
88+
{
89+
return 'solr';
90+
}
91+
92+
public function reset()
93+
{
94+
$this->data = [];
95+
}
96+
97+
public function preExecuteRequest(SolariumPreExecuteRequestEvent $event): void
98+
{
99+
if (isset($this->currentRequest)) {
100+
$this->failCurrentRequest();
101+
}
102+
103+
$this->currentRequest = $event->getRequest();
104+
$this->currentEndpoint = $event->getEndpoint();
105+
106+
$this->logger->debug($event->getEndpoint()->getBaseUri().$this->currentRequest->getUri());
107+
$this->currentStartTime = microtime(true);
108+
}
109+
110+
public function postExecuteRequest(SolariumPostExecuteRequestEvent $event): void
111+
{
112+
$endTime = microtime(true) - $this->currentStartTime;
113+
if (!isset($this->currentRequest)) {
114+
throw new \RuntimeException('Request not set');
115+
}
116+
if ($this->currentRequest !== $event->getRequest()) {
117+
throw new \RuntimeException('Requests differ');
118+
}
119+
120+
$this->log($event->getRequest(), $event->getResponse(), $event->getEndpoint(), $endTime);
121+
122+
$this->currentRequest = null;
123+
$this->currentStartTime = null;
124+
$this->currentEndpoint = null;
125+
}
126+
127+
public function getQueryCount(): int
128+
{
129+
return count($this->getQueries());
130+
}
131+
132+
public function getQueries(): array
133+
{
134+
return array_key_exists('queries', $this->data) ? $this->data['queries'] : [];
135+
}
136+
137+
public function getTotalTimeMs(): float
138+
{
139+
return array_key_exists('totalTimeMs', $this->data) ? $this->data['totalTimeMs'] : 0.0;
140+
}
141+
142+
public function serialize(): ?string
143+
{
144+
return serialize($this->data);
145+
}
146+
147+
public function unserialize($serialized): void
148+
{
149+
$this->data = unserialize($serialized);
150+
}
151+
152+
/**
153+
* Plugin init function
154+
*
155+
* Register event listeners
156+
*/
157+
protected function initPluginType()
158+
{
159+
/** @var EventDispatcherInterface $dispatcher */
160+
$dispatcher = $this->client->getEventDispatcher();
161+
if (!in_array($dispatcher, $this->eventDispatchers, true)) {
162+
$dispatcher->addListener(SolariumEvents::PRE_EXECUTE_REQUEST, [$this, 'preExecuteRequest'], 1000);
163+
$dispatcher->addListener(SolariumEvents::POST_EXECUTE_REQUEST, [$this, 'postExecuteRequest'], -1000);
164+
$this->eventDispatchers[] = $dispatcher;
165+
}
166+
}
167+
168+
protected function log(
169+
SolariumRequest $request,
170+
?SolariumResponse $response,
171+
SolariumEndpoint $endpoint,
172+
float $durationSec
173+
): void {
174+
$requestUri = str_replace(
175+
'://localhost:',
176+
'://gant-dev-solr.usemarkup.com:',
177+
$endpoint->getBaseUri().$request->getUri()
178+
);
179+
180+
$isPost = !empty($request->getRawData());
181+
$requestParams = $request->getParams();
182+
if ($isPost) {
183+
$requestParams = $this->normalizePostParams($requestParams, $request->getRawData());
184+
}
185+
186+
$responseBody = $response ? $response->getBody() : null;
187+
if ($responseBody) {
188+
$jsonBody = json_decode($responseBody);
189+
if (!empty($jsonBody)) {
190+
$responseBody = str_replace(
191+
'\"',
192+
'"',
193+
json_encode($jsonBody, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) ?: ''
194+
);
195+
}
196+
}
197+
198+
$this->queries[] = [
199+
'isPost' => $isPost,
200+
'requestUri' => $requestUri,
201+
'requestParams' => $requestParams,
202+
'statusCode' => $response ? $response->getStatusCode() : null,
203+
'responseBody' => $responseBody,
204+
'durationMs' => $durationSec * 1000,
205+
];
206+
}
207+
208+
protected function failCurrentRequest(): void
209+
{
210+
$endTime = microtime(true) - $this->currentStartTime;
211+
if (!$this->currentRequest || !$this->currentEndpoint) {
212+
throw new \RuntimeException('Incorrect fail method usage.');
213+
}
214+
$this->log($this->currentRequest, null, $this->currentEndpoint, $endTime);
215+
216+
$this->currentRequest = null;
217+
$this->currentStartTime = null;
218+
$this->currentEndpoint = null;
219+
}
220+
221+
protected function normalizePostParams(array $requestParams, string $rawData): array
222+
{
223+
$postParams = explode('&', $rawData);
224+
foreach ($postParams as $oneParam) {
225+
list($paramName, $paramValue) = explode('=', $oneParam, 2);
226+
$paramValue = urldecode($paramValue);
227+
if (array_key_exists($paramName, $requestParams)) {
228+
if (!is_array($requestParams[$paramName])) {
229+
$requestParams[$paramName] = [$requestParams[$paramName]];
230+
}
231+
$requestParams[$paramName][] = $paramValue;
232+
} else {
233+
$requestParams[$paramName] = $paramValue;
234+
}
235+
}
236+
return $requestParams;
237+
}
238+
}

src/Resources/config/services.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,15 @@ services:
159159

160160
Markup\NeedleBundle\Context\ContextSortAttributeFactory:
161161
autowire: true
162+
163+
solarium.data_collector:
164+
class: Markup\NeedleBundle\DataCollector\SolrDataCollector
165+
public: false
166+
tags:
167+
- name: 'data_collector'
168+
template: 'MarkupNeedleBundle:DataCollector:solr.html.twig'
169+
id: 'solr'
170+
- name: 'monolog.logger'
171+
channel: 'solr'
172+
calls:
173+
- [ 'setLogger', [ '@logger' ] ]
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
{% extends app.request.isXmlHttpRequest ? '@WebProfiler/Profiler/ajax_layout.html.twig' : '@WebProfiler/Profiler/layout.html.twig' %}
2+
3+
{% block toolbar %}
4+
{% if collector.queryCount > 0 %}
5+
{% set icon %}
6+
{{ include('@NelmioSolarium/DataCollector/solr.svg') }}
7+
<span class="sf-toolbar-value">{{ collector.queryCount }}</span>
8+
<span class="sf-toolbar-info-piece-additional-detail">
9+
<span class="sf-toolbar-label">in</span>
10+
<span class="sf-toolbar-value">{{ '%0.2f'|format(collector.totalTimeMs) }}</span>
11+
<span class="sf-toolbar-label">ms</span>
12+
</span>
13+
{% endset %}
14+
{% set text %}
15+
<div class="sf-toolbar-info-piece">
16+
<b>Solr Queries</b>
17+
<span class="sf-toolbar-status">{{ collector.queryCount }}</span>
18+
</div>
19+
<div class="sf-toolbar-info-piece">
20+
<b>Query time</b>
21+
<span>{{ '%0.2f'|format(collector.totalTimeMs) }} ms</span>
22+
</div>
23+
{% endset %}
24+
{% include '@WebProfiler/Profiler/toolbar_item.html.twig' with { 'link': profiler_url } %}
25+
{% endif %}
26+
{% endblock %}
27+
28+
{% block menu %}
29+
<span class="label {{ collector.queryCount ? '' : 'disabled' }}">
30+
<span class="icon">{{ include('@NelmioSolarium/DataCollector/solr.svg') }}</span>
31+
<strong>Solr</strong>
32+
{% if collector.queryCount > 0 %}
33+
<span class="count">
34+
<span>{{ collector.queryCount }}</span>
35+
<span>{{ '%0.0f'|format(collector.totalTimeMs) }} ms</span>
36+
</span>
37+
{% endif %}
38+
</span>
39+
{% endblock %}
40+
41+
{% block panel %}
42+
{% if collector.queries is empty %}
43+
<p>
44+
<em>No queries.</em>
45+
</p>
46+
{% else %}
47+
{% for i, query in collector.queries %}
48+
<div>
49+
<h2>
50+
Query {{ loop.index }}
51+
(
52+
{%- if query.statusCode -%}
53+
status: {{ query.statusCode }}, duration: {{ '%0.2f'|format(query.durationMs) }} ms
54+
{%- else -%}
55+
request failed
56+
{%- endif -%}
57+
)
58+
</h2>
59+
<div class="sf-tabs" style="padding: 0 0 30px 30px;">
60+
<div class="tab">
61+
<h3 class="tab-title">Request</h3>
62+
<div class="tab-content">
63+
<p>
64+
{{ query.isPost ? 'POST' : 'GET' }}
65+
<a href="{{ query.requestUri }}">{{ query.requestUri }}</a>
66+
</p>
67+
{% if query.isPost %}
68+
<form action="{{ query.requestUri }}" method="post" target="_blank">
69+
{% for key, value in query.requestParams %}
70+
{% if value is iterable %}
71+
{% for subValue in value %}
72+
<input type="hidden" name="{{ key }}" value="{{ subValue }}">
73+
{% endfor %}
74+
{% else %}
75+
<input type="hidden" name="{{ key }}" value="{{ value }}">
76+
{% endif %}
77+
{% endfor %}
78+
<input type="submit" value="Execute POST Request">
79+
</form>
80+
{% endif %}
81+
</div>
82+
</div>
83+
84+
<div class="tab">
85+
<h3 class="tab-title">Params</h3>
86+
<div class="tab-content">
87+
<table style="width:45%">
88+
<thead>
89+
<tr>
90+
<th scope="col">Key</th>
91+
<th scope="col">Value</th>
92+
</tr>
93+
</thead>
94+
<tbody>
95+
{% for key, value in query.requestParams %}
96+
<tr>
97+
<td><b>{{ key }}</b></td>
98+
{% if value is iterable %}
99+
<td>{{ value|join('<br />')|raw }}</td>
100+
{% else %}
101+
<td>{{ value }}</td>
102+
{% endif %}
103+
</tr>
104+
{% endfor %}
105+
</tbody>
106+
</table>
107+
</div>
108+
</div>
109+
110+
<div class="tab">
111+
<h3 class="tab-title">JSON Response</h3>
112+
<div class="tab-content">
113+
{% if query.statusCode %}
114+
<pre style="overflow:visible;">{{ query.responseBody }}</pre>
115+
{% else %}
116+
<p>Request failed, no response logged</p>
117+
{% endif %}
118+
</div>
119+
</div>
120+
</div>
121+
</div>
122+
{% endfor %}
123+
{% endif %}
124+
{% endblock %}

0 commit comments

Comments
 (0)