You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.13 06:38:32