Symfony4.2中Exception EventListener无法捕获RabbitMQ消费者异常
问题根源:
kernel.exception事件不支持CLI环境 你遇到的核心问题很明确:kernel.exception事件是Symfony为HTTP请求生命周期设计的,而你的RabbitMQ消费者运行在CLI命令行环境中,不属于HTTP请求上下文,所以这个事件根本不会被触发。这就是为什么你的Listener只输出了初始化日志,却从未执行异常捕获逻辑的原因。
解决方案:两种可行方案
方案1:抽离自定义日志逻辑为服务(推荐)
既然你已经在代码中主动捕获了所有异常,最直接的方式是把你想要的日志格式化逻辑抽成一个可复用的服务,然后在各个catch块中调用它,这样既能保证日志格式统一,又不需要依赖HTTP相关的事件。
步骤1:创建异常日志服务
// src/Service/ExceptionLoggerService.php namespace App\Service; use Psr\Log\LoggerInterface; use Symfony\Component\Serializer\SerializerInterface; class ExceptionLoggerService { private $logger; private $serializer; public function __construct(LoggerInterface $logger, SerializerInterface $serializer) { $this->logger = $logger; $this->serializer = $serializer; } public function logException(\Exception $exception): void { $log = [ 'code' => $exception->getCode(), 'message' => $exception->getMessage(), 'called' => [ 'file' => $exception->getTrace()[0]['file'], 'line' => $exception->getTrace()[0]['line'], ], 'occurred' => [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), ], ]; if ($exception->getPrevious() instanceof \Exception) { $log['previous'] = [ 'message' => $exception->getPrevious()->getMessage(), 'exception' => get_class($exception->getPrevious()), 'file' => $exception->getPrevious()->getFile(), 'line' => $exception->getPrevious()->getLine(), ]; } $this->logger->error($this->serializer->serialize($log, 'json')); } }
步骤2:配置服务(Symfony Flex会自动注册,可手动指定Logger)
在services.yaml中明确指定使用你创建的exception频道日志:
# config/services.yaml App\Service\ExceptionLoggerService: arguments: $logger: '@monolog.logger.exception'
步骤3:在消费者和服务中使用
修改消费者的catch块:
public function execute(AMQPMessage $msg): int { try { return $this->documentService->createDocument($this->serializer->deserialize($msg->body, 'array', 'json')); } catch (\Exception $e) { $this->exceptionLogger->logException($e); // 调用自定义日志服务 return ConsumerInterface::MSG_REJECT; } }
服务中的catch块同理:
try { // ...业务逻辑 $template = $this->templateRepository->findOneByName($messageData['template']); if ($template === null) throw new \Exception('Template does not exist'); // ...业务逻辑 return ConsumerInterface::MSG_ACK; } catch (\Exception $e) { $this->exceptionLogger->logException($e); // 调用自定义日志服务 return ConsumerInterface::MSG_REJECT; }
方案2:监听CLI专属的console.error事件
如果你坚持想用事件驱动的方式,可以使用Symfony CLI命令专属的console.error事件,这个事件会在CLI命令抛出未捕获异常时触发。
步骤1:修改Listener监听console.error
// src/EventListener/DocumentExceptionListener.php namespace App\EventListener; use Psr\Log\LoggerInterface; use Symfony\Component\Console\Event\ConsoleErrorEvent; use Symfony\Component\Serializer\SerializerInterface; class DocumentExceptionListener { private $logger; private $serializer; public function __construct(LoggerInterface $logger, SerializerInterface $serializer) { $this->logger = $logger; $this->serializer = $serializer; $this->logger->info('Starting to listen...'); } public function onConsoleError(ConsoleErrorEvent $event) { $this->logger->info('currently listening...'); $this->log($event->getError()); } private function log(\Exception $exception) { // 保留你原来的日志格式化逻辑 $log = [ 'code' => $exception->getCode(), 'message' => $exception->getMessage(), 'called' => [ 'file' => $exception->getTrace()[0]['file'], 'line' => $exception->getTrace()[0]['line'], ], 'occurred' => [ 'file' => $exception->getFile(), 'line' => $exception->getLine(), ], ]; if ($exception->getPrevious() instanceof \Exception) { $log['previous'] = [ 'message' => $exception->getPrevious()->getMessage(), 'exception' => get_class($exception->getPrevious()), 'file' => $exception->getPrevious()->getFile(), 'line' => $exception->getPrevious()->getLine(), ]; } $this->logger->error($this->serializer->serialize($log, 'json')); } }
步骤2:更新Listener的服务标签
# config/services.yaml App\EventListener\DocumentExceptionListener: tags: - { name: kernel.event_listener, event: console.error, method: onConsoleError } arguments: $logger: '@monolog.logger.exception'
注意事项
console.error事件默认只触发未被捕获的异常,所以如果你在代码中已经catch了异常,需要在catch块中手动触发事件:
// 在消费者中注入EventDispatcherInterface public function __construct( private DocumentService $documentService, private SerializerInterface $serializer, private EventDispatcherInterface $eventDispatcher, private LoggerInterface $logger ) {} public function execute(AMQPMessage $msg): int { try { return $this->documentService->createDocument($this->serializer->deserialize($msg->body, 'array', 'json')); } catch (\Exception $e) { // 手动创建并分发ConsoleErrorEvent $event = new ConsoleErrorEvent( $e, $this->input, // 如果你能获取到InputInterface $this->output, // 如果你能获取到OutputInterface $this->command // 当前消费者命令实例 ); $this->eventDispatcher->dispatch($event, ConsoleErrorEvent::NAME); return ConsumerInterface::MSG_REJECT; } }
总结
- 优先选择方案1,它更简单直接,符合你当前主动捕获异常的代码结构,不需要依赖事件机制。
- 方案2适合需要统一处理所有CLI命令异常的场景,但需要额外处理已捕获异常的事件触发逻辑。
内容的提问来源于stack exchange,提问作者Murat Aktasli
相关产品推荐
相关产品推荐

