src/EventsSubscriber/Exception/HttpErrorLogSubscriber.php line 37

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\EventsSubscriber\Exception;
  4. use Psr\Log\LoggerInterface;
  5. use Sentry\Severity;
  6. use Sentry\State\HubInterface;
  7. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  8. use Symfony\Component\HttpFoundation\Response;
  9. use Symfony\Component\HttpKernel\Event\ExceptionEvent;
  10. use Symfony\Component\HttpKernel\Event\ResponseEvent;
  11. use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
  12. use Symfony\Component\HttpKernel\KernelEvents;
  13. /**
  14.  * Ensures 5xx failures are logged to stderr (CloudWatch). Sentry exception capture is handled by
  15.  * register_error_listener; this subscriber also reports HTTP 500 responses that never threw.
  16.  */
  17. final class HttpErrorLogSubscriber implements EventSubscriberInterface
  18. {
  19.     public function __construct(
  20.         private readonly LoggerInterface $logger,
  21.         private readonly HubInterface $sentryHub,
  22.     ) {
  23.     }
  24.     public static function getSubscribedEvents(): array
  25.     {
  26.         return [
  27.             KernelEvents::EXCEPTION => ['onKernelException'0],
  28.             KernelEvents::RESPONSE => ['onKernelResponse', -256],
  29.         ];
  30.     }
  31.     public function onKernelException(ExceptionEvent $event): void
  32.     {
  33.         if (!$event->isMainRequest()) {
  34.             return;
  35.         }
  36.         $exception $event->getThrowable();
  37.         $statusCode $exception instanceof HttpExceptionInterface
  38.             $exception->getStatusCode()
  39.             : Response::HTTP_INTERNAL_SERVER_ERROR;
  40.         if ($statusCode Response::HTTP_INTERNAL_SERVER_ERROR) {
  41.             return;
  42.         }
  43.         $request $event->getRequest();
  44.         $context $this->buildRequestContext($request$statusCode);
  45.         $this->logger->error(
  46.             sprintf('Unhandled %d: %s'$statusCode$exception->getMessage()),
  47.             array_merge($context, ['exception' => $exception]),
  48.         );
  49.     }
  50.     public function onKernelResponse(ResponseEvent $event): void
  51.     {
  52.         if (!$event->isMainRequest()) {
  53.             return;
  54.         }
  55.         $response $event->getResponse();
  56.         if ($response->getStatusCode() < Response::HTTP_INTERNAL_SERVER_ERROR) {
  57.             return;
  58.         }
  59.         $request $event->getRequest();
  60.         $context $this->buildRequestContext($request$response->getStatusCode());
  61.         $this->logger->error(
  62.             sprintf('HTTP %d response without logged exception'$response->getStatusCode()),
  63.             $context,
  64.         );
  65.         $this->sentryHub->captureMessage(
  66.             sprintf(
  67.                 'HTTP %d on %s %s',
  68.                 $response->getStatusCode(),
  69.                 $request->getMethod(),
  70.                 $request->getPathInfo(),
  71.             ),
  72.             Severity::error(),
  73.         );
  74.     }
  75.     /**
  76.      * @return array<string, mixed>
  77.      */
  78.     private function buildRequestContext(\Symfony\Component\HttpFoundation\Request $requestint $statusCode): array
  79.     {
  80.         return [
  81.             'status_code' => $statusCode,
  82.             'method' => $request->getMethod(),
  83.             'path' => $request->getPathInfo(),
  84.             'query' => $request->query->all(),
  85.             'route' => $request->attributes->get('_route'),
  86.             'request_id' => $request->headers->get('X-Amzn-Trace-Id')
  87.                 ?? $request->headers->get('X-Request-Id'),
  88.         ];
  89.     }
  90. }