app/Service/EventSubscriber/TidewaysProfiler.php line 127

Open in your IDE?
  1. <?php
  2. namespace Sq\Service\EventSubscriber;
  3. use Sq\Entity\Schema\ORM as Entity;
  4. use Sq\Event\Queue\CronTaskFinishedEvent;
  5. use Sq\Event\Queue\CronTaskStartedEvent;
  6. use Sq\Event\Queue\WorkerJobFinishedEvent;
  7. use Sq\Event\Queue\WorkerJobStartedEvent;
  8. use Sq\Exception\Controller\AdminAccessDeniedException;
  9. use Sq\Exception\Controller\MemberAccessDeniedException;
  10. use Sq\Exception\Controller\MFARequiredException;
  11. use Sq\Exception\Controller\MFAVerificationException;
  12. use Sq\Service\Config\SqConfig;
  13. use Sq\Service\Log\LogBuilder;
  14. use Sq\Service\Log\LogBuilderInterface;
  15. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  16. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpKernel\Event\ControllerEvent;
  19. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  20. use Symfony\Component\HttpKernel\Event\TerminateEvent;
  21. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  22. use Symfony\Component\HttpKernel\KernelEvents;
  23. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  24. use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
  25. class TidewaysProfiler implements EventSubscriberInterface
  26. {
  27.     protected array $ignoredExceptionClasses = [
  28.         MFARequiredException::class,
  29.         MFAVerificationException::class,
  30.         AdminAccessDeniedException::class,
  31.         MemberAccessDeniedException::class,
  32.         NotFoundHttpException::class,
  33.     ];
  34.     protected array $trackedErrors = [];
  35.     private bool $ignoreTransaction false;
  36.     private bool $staticAttributesAdded false;
  37.     public function __construct(
  38.         private readonly SqConfig $config,
  39.         private readonly ParameterBagInterface $parameterBag,
  40.         private readonly LogBuilderInterface $logBuilder,
  41.         private readonly TokenStorageInterface $tokenStorage,
  42.     ) {
  43.     }
  44.     public static function getSubscribedEvents()
  45.     {
  46.         return [
  47.             KernelEvents::CONTROLLER => ['onController', -64],
  48.             KernelEvents::EXCEPTION => ['onKernelException'],
  49.             KernelEvents::TERMINATE => ['onTerminate'],
  50.             CronTaskStartedEvent::class => ['onCronTaskStarted'],
  51.             CronTaskFinishedEvent::class => ['onCronTaskFinished'],
  52.             WorkerJobStartedEvent::class => ['onWorkerJobStarted'],
  53.             WorkerJobFinishedEvent::class => ['onWorkerJobFinished'],
  54.         ];
  55.     }
  56.     public function onController(ControllerEvent $event): void
  57.     {
  58.         if (!$this->isExtensionEnabled())
  59.         {
  60.             return;
  61.         }
  62.         if (!$event->isMainRequest())
  63.         {
  64.             return;
  65.         }
  66.         $request $event->getRequest();
  67.         $this->ignoreTransaction = !$this->shouldTrackWebRequest($request);
  68.         $this->startTransaction();
  69.         $this->addAttribute('request_method'$request->getMethod());
  70.         $requestId $request->headers->get('X-Request-Id');
  71.         if (is_string($requestId))
  72.         {
  73.             $this->addAttribute('http_request_id'$requestId);
  74.         }
  75.         if (is_graphql_request($request))
  76.         {
  77.             $operationName $request->attributes->get('_gql_operation_name');
  78.             if (is_string($operationName))
  79.             {
  80.                 $this->nameTransaction('gql:' $operationName);
  81.                 if (in_array($operationName$this->getTraceDisabledGqlOperations()))
  82.                 {
  83.                     $this->disableCurrentTrace();
  84.                 }
  85.             }
  86.         }
  87.         $memberId $this->getMemberId();
  88.         if ($memberId !== null)
  89.         {
  90.             $this->addAttribute('m_id'$memberId);
  91.         }
  92.         $userToken $this->tokenStorage->getToken();
  93.         $user $userToken?->getUser();
  94.         if ($user instanceof Entity\User)
  95.         {
  96.             $this->addAttribute('u_id', (string) $user->getId());
  97.         }
  98.         if ($userToken instanceof SwitchUserToken)
  99.         {
  100.             $impersonator $userToken->getOriginalToken()->getUser();
  101.             if ($impersonator instanceof Entity\User)
  102.             {
  103.                 $this->addAttribute('impersonator_u_id', (string) $impersonator->getId());
  104.             }
  105.         }
  106.     }
  107.     public function onKernelException(ExceptionEvent $event): void
  108.     {
  109.         if (!$this->isExtensionEnabled())
  110.         {
  111.             return;
  112.         }
  113.         $this->trackThrowable($event->getThrowable());
  114.     }
  115.     public function onTerminate(TerminateEvent $event): void
  116.     {
  117.         if (!$this->isExtensionEnabled())
  118.         {
  119.             return;
  120.         }
  121.         if ($this->ignoreTransaction)
  122.         {
  123.             $this->ignoreCurrentTransaction();
  124.         }
  125.     }
  126.     public function onCronTaskStarted(CronTaskStartedEvent $event): void
  127.     {
  128.         if (!$this->isExtensionEnabled())
  129.         {
  130.             return;
  131.         }
  132.         $this->ignoreTransaction = !$this->shouldTrackCron();
  133.         $this->startTransaction();
  134.         register_shutdown_function([$this'stopTransaction']);
  135.         $this->nameTransaction('cron:' $event->getName());
  136.     }
  137.     public function onCronTaskFinished(CronTaskFinishedEvent $event): void
  138.     {
  139.         if (!$this->isExtensionEnabled())
  140.         {
  141.             return;
  142.         }
  143.         if ($this->ignoreTransaction)
  144.         {
  145.             $this->ignoreCurrentTransaction();
  146.             return;
  147.         }
  148.         $throwable $event->getThrowable();
  149.         if ($throwable !== null)
  150.         {
  151.             $this->trackThrowable($throwable);
  152.         }
  153.     }
  154.     public function onWorkerJobStarted(WorkerJobStartedEvent $event): void
  155.     {
  156.         if (!$this->isExtensionEnabled())
  157.         {
  158.             return;
  159.         }
  160.         $this->ignoreTransaction = !$this->shouldTrackWorkerJob();
  161.         $this->startTransaction();
  162.         register_shutdown_function([$this'stopTransaction']);
  163.         $this->nameTransaction('job:' $event->getName());
  164.     }
  165.     public function onWorkerJobFinished(WorkerJobFinishedEvent $event): void
  166.     {
  167.         if (!$this->isExtensionEnabled())
  168.         {
  169.             return;
  170.         }
  171.         if ($this->ignoreTransaction)
  172.         {
  173.             $this->ignoreCurrentTransaction();
  174.             return;
  175.         }
  176.         $throwable $event->getThrowable();
  177.         if ($throwable !== null)
  178.         {
  179.             $this->trackThrowable($throwable);
  180.         }
  181.         $this->stopTransaction();
  182.     }
  183.     private function trackThrowable(\Throwable $throwable): void
  184.     {
  185.         if (in_array(get_class($throwable), $this->ignoredExceptionClassestrue))
  186.         {
  187.             return;
  188.         }
  189.         $hash spl_object_hash($throwable);
  190.         if (isset($this->trackedErrors[$hash]))
  191.         {
  192.             return;
  193.         }
  194.         $this->trackedErrors[$hash] = true;
  195.         \Tideways\Profiler::logException($throwable);
  196.     }
  197.     private function addStaticAttributes(): void
  198.     {
  199.         if ($this->staticAttributesAdded)
  200.         {
  201.             return;
  202.         }
  203.         $this->staticAttributesAdded true;
  204.         $this->logBuilder instanceof LogBuilder && $this->addAttribute('process_id'$this->logBuilder->getProcessId());
  205.         $this->addAttribute('instance_id', (string) $this->parameterBag->get('instance.identifier'));
  206.         $this->addAttribute('app_name', (string) $this->parameterBag->get('kernel.app_name'));
  207.         $this->addAttribute('app_type', (string) $this->parameterBag->get('kernel.app_type'));
  208.         $this->addAttribute('environment', (string) $this->parameterBag->get('kernel.environment'));
  209.         $this->addAttribute('run_mode'is_cli() ? 'cli' 'web');
  210.     }
  211.     private function addAttribute(string $name, ?string $value): void
  212.     {
  213.         if ($value === null || $value === '')
  214.         {
  215.             return;
  216.         }
  217.         \Tideways\Profiler::setCustomVariable($name$value);
  218.     }
  219.     private function startTransaction(): void
  220.     {
  221.         if (!$this->isProfilerStarted())
  222.         {
  223.             \Tideways\Profiler::start([]);
  224.         }
  225.         $this->addStaticAttributes();
  226.     }
  227.     private function stopTransaction(): void
  228.     {
  229.         if ($this->isProfilerStarted())
  230.         {
  231.             \Tideways\Profiler::stop();
  232.         }
  233.     }
  234.     private function nameTransaction(string $name): void
  235.     {
  236.         if ($name !== '')
  237.         {
  238.             \Tideways\Profiler::setTransactionName($name);
  239.         }
  240.     }
  241.     private function disableCurrentTrace(): void
  242.     {
  243.         \Tideways\Profiler::disableCallgraphProfiler();
  244.         \Tideways\Profiler::disableTracingProfiler();
  245.     }
  246.     private function ignoreCurrentTransaction(): void
  247.     {
  248.         \Tideways\Profiler::ignoreTransaction();
  249.     }
  250.     private function isExtensionEnabled(): bool
  251.     {
  252.         return extension_loaded('tideways') && class_exists(\Tideways\Profiler::class);
  253.     }
  254.     private function shouldTrackWebRequest(Request $request): bool
  255.     {
  256.         if (!($this->getConfig()['web']['enabled'] ?? true))
  257.         {
  258.             return false;
  259.         }
  260.         if (!empty($this->getConfig()['web']['force_cookie_name']) && $request->cookies->has($this->getConfig()['web']['force_cookie_name']))
  261.         {
  262.             return true;
  263.         }
  264.         $samplePercent $this->getConfig()['web']['sample_rate_pct'] ?? 10;
  265.         if (mt_rand(1100) > $samplePercent)
  266.         {
  267.             return false;
  268.         }
  269.         return true;
  270.     }
  271.     private function shouldTrackWorkerJob(): bool
  272.     {
  273.         if (!($this->getConfig()['worker']['enabled'] ?? true))
  274.         {
  275.             return false;
  276.         }
  277.         $samplePercent $this->getConfig()['worker']['sample_rate_pct'] ?? 10;
  278.         if (mt_rand(1100) > $samplePercent)
  279.         {
  280.             return false;
  281.         }
  282.         return true;
  283.     }
  284.     private function shouldTrackCron(): bool
  285.     {
  286.         if (!($this->getConfig()['cron']['enabled'] ?? true))
  287.         {
  288.             return false;
  289.         }
  290.         $samplePercent $this->getConfig()['cron']['sample_rate_pct'] ?? 10;
  291.         if (mt_rand(1100) > $samplePercent)
  292.         {
  293.             return false;
  294.         }
  295.         return true;
  296.     }
  297.     private function getConfig(): array
  298.     {
  299.         $config $this->config->getConfig()['tideways_profiler'] ?? null;
  300.         return is_array($config) ? $config : [];
  301.     }
  302.     private function getMemberId(): ?string
  303.     {
  304.         $memberId $GLOBALS['m_id'] ?? null;
  305.         if ($memberId === null || $memberId === '')
  306.         {
  307.             return null;
  308.         }
  309.         return (string) $memberId;
  310.     }
  311.     private function isProfilerStarted(): bool
  312.     {
  313.         return \Tideways\Profiler::isStarted();
  314.     }
  315.     private function getTraceDisabledGqlOperations(): array
  316.     {
  317.         return $this->getConfig()['web']['trace_disabled_gql_operations'] ?? [];
  318.     }
  319. }