[ ['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(); } }