vendor/bugsnag/bugsnag-symfony/EventListener/BugsnagListener.php line 140

Open in your IDE?
  1. <?php
  2. namespace Bugsnag\BugsnagBundle\EventListener;
  3. use Bugsnag\BugsnagBundle\Request\SymfonyResolver;
  4. use Bugsnag\Client;
  5. use Bugsnag\Report;
  6. use InvalidArgumentException;
  7. use Symfony\Component\Console\ConsoleEvents;
  8. use Symfony\Component\Console\Event\ConsoleErrorEvent;
  9. use Symfony\Component\Console\Event\ConsoleExceptionEvent;
  10. use Symfony\Component\Debug\Exception\OutOfMemoryException;
  11. use Symfony\Component\ErrorHandler\Error\OutOfMemoryError;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  14. use Symfony\Component\HttpKernel\Event\GetResponseEvent;
  15. use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
  16. use Symfony\Component\HttpKernel\Event\RequestEvent;
  17. use Symfony\Component\HttpKernel\HttpKernelInterface;
  18. use Symfony\Component\HttpKernel\KernelEvents;
  19. use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
  20. use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
  21. class BugsnagListener implements EventSubscriberInterface
  22. {
  23.     /**
  24.      * The bugsnag client instance.
  25.      *
  26.      * @var \Bugsnag\Client
  27.      */
  28.     protected $client;
  29.     /**
  30.      * The request resolver instance.
  31.      *
  32.      * @var \Bugsnag\BugsnagBundle\Request\SymfonyResolver
  33.      */
  34.     protected $resolver;
  35.     /**
  36.      * If auto notifying is enabled.
  37.      *
  38.      * @var bool
  39.      */
  40.     protected $auto;
  41.     /**
  42.      * A regex that matches Symfony's OOM errors.
  43.      *
  44.      * @var string
  45.      */
  46.     private $oomRegex '/Allowed memory size of (\d+) bytes exhausted \(tried to allocate \d+ bytes\)/';
  47.     /**
  48.      * Create a new bugsnag listener instance.
  49.      *
  50.      * @param \Bugsnag\Client                                $client
  51.      * @param \Bugsnag\BugsnagBundle\Request\SymfonyResolver $resolver
  52.      * @param bool                                           $auto
  53.      *
  54.      * @return void
  55.      */
  56.     public function __construct(Client $clientSymfonyResolver $resolver$auto)
  57.     {
  58.         $this->client $client;
  59.         $this->resolver $resolver;
  60.         $this->auto $auto;
  61.     }
  62.     /**
  63.      * Handle an incoming request.
  64.      *
  65.      * @param GetResponseEvent|RequestEvent $event
  66.      *
  67.      * @return void
  68.      */
  69.     public function onKernelRequest($event)
  70.     {
  71.         // Compatibility with Symfony < 5 and Symfony >=5
  72.         if (!$event instanceof GetResponseEvent && !$event instanceof RequestEvent) {
  73.             throw new InvalidArgumentException('onKernelRequest function only accepts GetResponseEvent and RequestEvent arguments');
  74.         }
  75.         if ($event->getRequestType() !== HttpKernelInterface::MASTER_REQUEST) {
  76.             return;
  77.         }
  78.         $this->client->setFallbackType('HTTP');
  79.         $this->resolver->set($event->getRequest());
  80.     }
  81.     /**
  82.      * Handle an http kernel exception.
  83.      *
  84.      * @param GetResponseForExceptionEvent|ExceptionEvent $event
  85.      *
  86.      * @return void
  87.      */
  88.     public function onKernelException($event)
  89.     {
  90.         $throwable $this->resolveThrowable($event);
  91.         if ($this->isOom($throwable)
  92.             && $this->client->getMemoryLimitIncrease() !== null
  93.             && preg_match($this->oomRegex$throwable->getMessage(), $matches) === 1
  94.         ) {
  95.             $currentMemoryLimit = (int) $matches[1];
  96.             ini_set('memory_limit'$currentMemoryLimit $this->client->getMemoryLimitIncrease());
  97.         }
  98.         $this->sendNotify($throwable, []);
  99.     }
  100.     /**
  101.      * Handle a console exception (used instead of ConsoleErrorEvent before
  102.      * Symfony 3.3 and kept for backwards compatibility).
  103.      *
  104.      * @param \Symfony\Component\Console\Event\ConsoleExceptionEvent $event
  105.      *
  106.      * @return void
  107.      */
  108.     public function onConsoleException(ConsoleExceptionEvent $event)
  109.     {
  110.         $meta = ['status' => $event->getExitCode()];
  111.         if ($event->getCommand()) {
  112.             $meta['name'] = $event->getCommand()->getName();
  113.         }
  114.         $this->sendNotify($event->getException(), ['command' => $meta]);
  115.     }
  116.     /**
  117.      * Handle a console error.
  118.      *
  119.      * @param \Symfony\Component\Console\Event\ConsoleErrorEvent $event
  120.      *
  121.      * @return void
  122.      */
  123.     public function onConsoleError(ConsoleErrorEvent $event)
  124.     {
  125.         $meta = ['status' => $event->getExitCode()];
  126.         if ($event->getCommand()) {
  127.             $meta['name'] = $event->getCommand()->getName();
  128.         }
  129.         $this->sendNotify($event->getError(), ['command' => $meta]);
  130.     }
  131.     /**
  132.      * Handle a failing message.
  133.      *
  134.      * @param \Symfony\Component\Messenger\Event\WorkerMessageFailedEvent $event
  135.      *
  136.      * @return void
  137.      */
  138.     public function onWorkerMessageFailed(WorkerMessageFailedEvent $event)
  139.     {
  140.         $this->sendNotify(
  141.             $event->getThrowable(),
  142.             ['Messenger' => ['willRetry' => $event->willRetry()]]
  143.         );
  144.         // Normally we flush after a message has been handled, but this event
  145.         // doesn't fire for failed messages so we have to flush here instead
  146.         $this->client->flush();
  147.     }
  148.     /**
  149.      * Flush any accumulated reports after a message has been handled.
  150.      *
  151.      * In batch sending mode reports are usually sent on shutdown but workers
  152.      * are (generally) long running processes so this doesn't work. Instead we
  153.      * flush after each handled message
  154.      *
  155.      * @param WorkerMessageHandledEvent $event
  156.      *
  157.      * @return void
  158.      */
  159.     public function onWorkerMessageHandled(WorkerMessageHandledEvent $event)
  160.     {
  161.         $this->client->flush();
  162.     }
  163.     /**
  164.      * @param \Throwable $throwable
  165.      * @param array      $meta
  166.      *
  167.      * @return void
  168.      */
  169.     private function sendNotify($throwable$meta)
  170.     {
  171.         if (!$this->auto) {
  172.             return;
  173.         }
  174.         $report Report::fromPHPThrowable(
  175.             $this->client->getConfig(),
  176.             $throwable
  177.         );
  178.         $report->setUnhandled(true);
  179.         $report->setSeverity('error');
  180.         $report->setSeverityReason([
  181.             'type' => 'unhandledExceptionMiddleware',
  182.             'attributes' => [
  183.                 'framework' => 'Symfony',
  184.             ],
  185.         ]);
  186.         $report->setMetaData($meta);
  187.         $this->client->notify($report);
  188.     }
  189.     /**
  190.      * @param GetResponseForExceptionEvent|ExceptionEvent $event
  191.      *
  192.      * @return \Throwable
  193.      */
  194.     private function resolveThrowable($event)
  195.     {
  196.         // Compatibility with Symfony < 5 and Symfony >=5
  197.         // The additional `method_exists` check is to prevent errors in Symfony 4.3
  198.         // where the ExceptionEvent exists and is used but doesn't implement
  199.         // the `getThrowable` method, which was introduced in Symfony 4.4
  200.         if ($event instanceof ExceptionEvent && method_exists($event'getThrowable')) {
  201.             return $event->getThrowable();
  202.         }
  203.         if ($event instanceof GetResponseForExceptionEvent) {
  204.             return $event->getException();
  205.         }
  206.         throw new InvalidArgumentException('onKernelException function only accepts GetResponseForExceptionEvent and ExceptionEvent arguments');
  207.     }
  208.     /**
  209.      * Check if this $throwable is an OOM.
  210.      *
  211.      * This will be represented by an "OutOfMemoryError" on Symfony 4.4+ or an
  212.      * "OutOfMemoryException" on earlier versions.
  213.      *
  214.      * @param \Throwable $throwable
  215.      *
  216.      * @return bool
  217.      */
  218.     private function isOom($throwable)
  219.     {
  220.         return $throwable instanceof OutOfMemoryError
  221.             || $throwable instanceof OutOfMemoryException;
  222.     }
  223.     /**
  224.      * @return array<string, array{string, int}>
  225.      */
  226.     public static function getSubscribedEvents()
  227.     {
  228.         $listeners = [
  229.             KernelEvents::REQUEST => ['onKernelRequest'256],
  230.             KernelEvents::EXCEPTION => ['onKernelException'128],
  231.         ];
  232.         // Added ConsoleEvents in Symfony 2.3
  233.         if (class_exists(ConsoleEvents::class)) {
  234.             // Added with ConsoleEvents::ERROR in Symfony 3.3 to deprecate ConsoleEvents::EXCEPTION
  235.             if (class_exists(ConsoleErrorEvent::class)) {
  236.                 $listeners[ConsoleEvents::ERROR] = ['onConsoleError'128];
  237.             } else {
  238.                 $listeners[ConsoleEvents::EXCEPTION] = ['onConsoleException'128];
  239.             }
  240.         }
  241.         if (class_exists(WorkerMessageFailedEvent::class)) {
  242.             // This must run after Symfony's "SendFailedMessageForRetryListener"
  243.             // as it sets the "willRetry" flag
  244.             $listeners[WorkerMessageFailedEvent::class] = ['onWorkerMessageFailed'64];
  245.         }
  246.         if (class_exists(WorkerMessageHandledEvent::class)) {
  247.             $listeners[WorkerMessageHandledEvent::class] = ['onWorkerMessageHandled'128];
  248.         }
  249.         return $listeners;
  250.     }
  251. }