177 lines
5.9 KiB
PHP
177 lines
5.9 KiB
PHP
<?php
|
|
|
|
namespace KupShop\MetricsBundle\EventSubscriber;
|
|
|
|
use Doctrine\DBAL\Connection;
|
|
use Doctrine\DBAL\Logging\DebugStack;
|
|
use KupShop\KupShopBundle\Util\RequestUtil;
|
|
use KupShop\MetricsBundle\PrometheusRegistryAccessor;
|
|
use KupShop\MetricsBundle\PrometheusWrapper;
|
|
use KupShop\MetricsBundle\Util\SQLTimeLogger;
|
|
use KupShop\ResponseCacheBundle\Controller\FragmentController;
|
|
use Prometheus\CollectorRegistry;
|
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
|
use Symfony\Component\HttpKernel\Event\ControllerEvent;
|
|
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
|
use Symfony\Component\HttpKernel\Event\TerminateEvent;
|
|
use Symfony\Component\HttpKernel\KernelEvents;
|
|
use Symfony\Contracts\Service\Attribute\Required;
|
|
|
|
class RequestMetricsSubscriber implements EventSubscriberInterface
|
|
{
|
|
public static function getSubscribedEvents()
|
|
{
|
|
return [
|
|
KernelEvents::REQUEST => [
|
|
['requestStart', 10000],
|
|
],
|
|
KernelEvents::CONTROLLER => [
|
|
['requestController', -10000],
|
|
],
|
|
KernelEvents::TERMINATE => [
|
|
['requestFinish', -9999], // process before asyncQueue jobs
|
|
],
|
|
];
|
|
}
|
|
|
|
/** @var CollectorRegistry */
|
|
private $registry;
|
|
|
|
/** @var PrometheusWrapper */
|
|
private $prometheus;
|
|
|
|
/** @var SQLTimeLogger */
|
|
private $doctrineLogger;
|
|
|
|
/** @var float */
|
|
private $timestamp;
|
|
|
|
/** @var string */
|
|
private $controllerClassName;
|
|
|
|
/** @var string */
|
|
private $actionName;
|
|
|
|
private $doctrineDebugStack;
|
|
|
|
#[Required]
|
|
public RequestUtil $requestUtil;
|
|
|
|
public function __construct(PrometheusRegistryAccessor $registryAccessor, PrometheusWrapper $prometheus, Connection $connection)
|
|
{
|
|
$this->registry = $registryAccessor->getRegistry();
|
|
$this->prometheus = $prometheus;
|
|
if (isProduction()) {
|
|
$timeLogger = new SQLTimeLogger();
|
|
$connection->getConfiguration()->setSQLLogger($timeLogger);
|
|
}
|
|
$this->doctrineLogger = $connection->getConfiguration()->getSQLLogger();
|
|
}
|
|
|
|
public function requestStart(RequestEvent $event)
|
|
{
|
|
$this->timestamp = microtime(true);
|
|
|
|
// Disable redirect to https or correct domain
|
|
if ($event->getRequest()->getPathInfo() === '/_prometheus/metrics/') {
|
|
$event->getRequest()->attributes->set('ignore_checks', true);
|
|
}
|
|
}
|
|
|
|
public function requestController(ControllerEvent $event)
|
|
{
|
|
[$className, $actionName] = $this->requestUtil->getControllerFromEvent($event);
|
|
|
|
// skip fragments
|
|
if ($className === FragmentController::class) {
|
|
return;
|
|
}
|
|
$this->controllerClassName = $className;
|
|
$this->actionName = $actionName;
|
|
}
|
|
|
|
public function requestFinish(TerminateEvent $event)
|
|
{
|
|
// Ignore requests without timestamp - requestStart not called
|
|
if (!$this->timestamp) {
|
|
return;
|
|
}
|
|
|
|
$wpjTransactionInfo = $this->requestUtil->getTransactionInfo($event->getRequest());
|
|
|
|
/*
|
|
* Use info from wpj_transaction attribute if available.
|
|
* With response_cache module, the HttpCache symfony bundle omits request attributes before calling the terminate event,
|
|
* so the wpj_transaction attribute on request doesnt have to be available in every case
|
|
*/
|
|
if ($wpjTransactionInfo) {
|
|
$this->controllerClassName = $wpjTransactionInfo['controller'] ?? '';
|
|
$this->actionName = $wpjTransactionInfo['action'] ?? '';
|
|
$query = $wpjTransactionInfo['query'] ?? null;
|
|
}
|
|
|
|
$labels = ['controller' => $this->controllerClassName, 'action' => $this->actionName, 'query' => $query ?? null] + $this->prometheus->getLabels();
|
|
|
|
if (findModule(\Modules::COMPONENTS) || findModule(\Modules::METRICS, \Modules::SUB_REQUESTS_HISTOGRAM)) {
|
|
// try histograms only for components or submodule, so it doesn't generate so much series
|
|
$this->prometheus->setHistogram(
|
|
'request',
|
|
'duration',
|
|
'Complete symfony request duration in milliseconds',
|
|
(microtime(true) - $this->timestamp) * 1000,
|
|
$labels,
|
|
[100, 300, 500, 750, 1000, 2000]
|
|
);
|
|
} else {
|
|
$this->prometheus->setCounter(
|
|
'request',
|
|
'duration_count',
|
|
'Complete symfony request count',
|
|
1,
|
|
$labels,
|
|
);
|
|
|
|
$this->prometheus->setCounter(
|
|
'request',
|
|
'duration_sum',
|
|
'Complete symfony request duration in milliseconds',
|
|
(microtime(true) - $this->timestamp) * 1000,
|
|
$labels,
|
|
);
|
|
}
|
|
|
|
$queryTime = 0.0;
|
|
$queryCount = 0;
|
|
if ($this->doctrineLogger instanceof SQLTimeLogger) {
|
|
$queryTime = $this->doctrineLogger->time;
|
|
$queryCount = $this->doctrineLogger->count;
|
|
}
|
|
if ($this->doctrineLogger instanceof DebugStack) {
|
|
foreach ($this->doctrineDebugStack->queries ?? [] as $query) {
|
|
$queryTime += $query['executionMS']; // executionMS contains value in seconds wtf
|
|
}
|
|
$queryCount = count($this->doctrineDebugStack->queries);
|
|
}
|
|
|
|
$labels = ['controller' => $this->controllerClassName, 'action' => $this->actionName, 'query' => $query ?? null];
|
|
|
|
$this->prometheus->setCounter(
|
|
'request',
|
|
'query_time_count',
|
|
'Database queries count',
|
|
$queryCount,
|
|
$labels,
|
|
);
|
|
|
|
$this->prometheus->setCounter(
|
|
'request',
|
|
'query_time_sum',
|
|
'Database queries execution time in milliseconds',
|
|
$queryTime * 1000,
|
|
$labels,
|
|
);
|
|
|
|
$this->prometheus->flush();
|
|
}
|
|
}
|