如何避免记录由 Api-Platform 正确转换为状态代码的预期异常?

How to avoid logging expected exceptions that are correctly converted to status codes by Api-Platform?

在我们的 Api-Platform 项目的一些路由中,一些常见错误情况的例外情况是 thrown。

例如在调用 POST /orders 时,NewOrderHandler 可以在适当的情况下抛出这两个中的任何一个:

所有这些异常都属于 DomainException 层次结构。

这些异常是使用 exception_to_status 配置在响应中 correctly converted 到状态代码 400 的,并且响应包含适当的错误消息。到目前为止一切顺利。

exception_to_status:
    App\Order\NotEnoughStock: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST
    App\Order\NotEnoughCredit: !php/const Symfony\Component\HttpFoundation\Response::HTTP_BAD_REQUEST

唯一的问题是异常仍被记录为 CRITICAL 错误,被视为 "uncaught exception"。这甚至在生产中也会被记录下来。

我本来希望通过转换为正确的状态代码(例如 !== 500),这些异常将被视为 "handled",因此不会污染日志。

从处理程序中抛出异常很方便,因为它有助于处理事务性并自动生成适当的错误响应消息。它适用于网络和控制台。

这些交易不应该被视为已处理吗?是否有必要创建另一个异常监听器来处理这个问题?如果创建异常侦听器,该怎么做才不会干扰 Api-平台错误规范化?

而在 Monolog 中,当使用 fingers_crossed 日志处理程序时,将 let you exclude from logging requests that respond with certain statuses,只有当异常是 HttpException:

的实例时才会这样做

我通过实现订阅者将异常转换为 BadRequestHttpException.

来解决这个问题
final class DomainToHttpExceptionSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): iterable
    {
        return [ KernelEvents::EXCEPTION => 'convertException'];
    }

    public function convertException(ExceptionEvent $event): void
    {
        $exception = $event->getThrowable();

        if ($exception instanceof DomainException) {
            $event->setThrowable(
                new BadRequestHttpException(
                    $exception->getMessage(),
                    $exception
                )
            );

        }
    }
}

加上这个独白配置就可以了:

monolog:
    handlers:
        fingers:
              type: fingers_crossed
              action_level: warning
              excluded_http_codes:
                - 404
                - 400

我在 GitHub 问题上从 this answer 那里得到了这个。它有效,但我不喜欢这个解决方案。希望其他一些答案会对此有所改进。

我在虚拟应用程序上对其进行了测试,得到:

Apr 11 21:36:11 |CRITI| REQUES Uncaught PHP Exception App\Exception\DomainException: "This is no more logged" at D:\www\campagne\src\DataPersister\StationDataPersister.php line 53 Apr 11 23:36:12 |WARN | SERVER POST (400) /api/stations

您可以实施自己的日志激活策略:

此代码基于 HttpCode activation strategy

namespace App\Log

use App\Exception\DomainException;
use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy;
use Symfony\Component\HttpKernel\Exception\HttpException;

/**
 * Activation strategy for logs
 */
class LogActivationStrategy extends ErrorLevelActivationStrategy
{

    public function __construct()
    {
        parent::__construct('error');
    }

    public function isHandlerActivated(array $record): bool
    {
        $isActivated = parent::isHandlerActivated($record);
        if ($isActivated && isset($record['context']['exception'])) {
            $exception = $record['context']['exception'];

            // This is a domain exception, I don't log it
            return !$exception instanceof DomainException;

            // OR if code could be different from 400
            if ($exception instanceof DomainException) {
                // This is a domain exception 
                // You log it when status code is different from 400.
                return 400 !== $exception->getStatusCode();
            }
        }

        return $isActivated;
    }
}

我们还需要告诉 Monolog 使用我们的 ActivationStrategy

monolog:
    handlers:
        main:
            type: fingers_crossed
            action_level: info
            handler: nested
            activation_strategy: App\Log\LogActivationStrategy 
        nested:
            type: stream
            path: "%kernel.logs_dir%/%kernel.environment%.log"
            level: info
       console:
            type: console
            process_psr_3_messages: false
            channels: ["!event", "!doctrine", "!console"]

现在我的日志只包含:

Apr 11 23:41:07 |WARN | SERVER POST (400) /api/stations

就像@yivi,我不喜欢我的解决方案,因为每次应用程序都会尝试记录一些东西,你会在这个函数上浪费时间......而且这个方法不会改变日志,它会删除它.

答案很简单:处理异常不是捕获异常。

即使您将异常转换为 400 错误,您的异常仍未被捕获...这就是为什么 symfony 记录它并完成 here

如果您不想记录任何 DomainException,只需重写 logException() 方法,以便在它是 DomainException 实例时跳过记录。

这里有一个例子:

namespace App\EventListener;

use Symfony\Component\HttpKernel\EventListener\ErrorListener;

class ExceptionListener extends ErrorListener
{
    protected function logException(\Exception $exception, string $message): void
    {
        if ($exception instanceof DomainException) {
            return;
        }

        parent::logException($exception, $message);
    }
}

最后你需要告诉 Symfony 使用这个 class 而不是 Symfony 那个。由于 exception_listener 服务定义没有 class 参数,我建议使用编译器传递来替换 class.

namespace App;

use App\EventListener\ExceptionListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class OverrideServiceCompilerPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        $definition = $container->getDefinition('exception_listener');
        $definition->setClass(ExceptionListener::class);
    }
}

有关详细信息,请参阅 Bundle override

或者,只需 decorate 使用您自己的 exception_listener 服务,不需要编译器传递:

App\EventListener\ExceptionListener:
        decorates: 'exception_listener'