如何避免记录Api平台正确转换为状态代码的预期异常



在我们Api平台项目的一些路由中,对于一些常见的错误条件,例外情况是thrown。

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

  • NotEnoughStock
  • NotEnoughCredit

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

通过使用exception_to_status配置,这些异常在响应中正确转换为状态代码400,并且响应中包含适当的错误消息。到目前为止还不错。

exception_to_status:
AppOrderNotEnoughStock: !php/const SymfonyComponentHttpFoundationResponse::HTTP_BAD_REQUEST
AppOrderNotEnoughCredit: !php/const SymfonyComponentHttpFoundationResponse::HTTP_BAD_REQUEST

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

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

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

这些交易不应该被视为已处理吗?是否有必要创建另一个异常侦听器来处理此问题?如果创建一个异常侦听器,如何做到这一点,以免干扰Api平台的错误规范化?

有一个简单的答案:处理异常不是捕获异常。

即使您将异常转换为400错误,您的异常仍然是未捕获的。。。这就是为什么symfony会记录它,并在这里完成。

如果您不想记录任何DomainException,只需覆盖logException()方法,以便在它是DomainException实例的情况下跳过日志记录。

这里有一个例子:

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

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

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

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

或者,只需使用自己的exception_listener服务进行装饰,不需要编译器通行证:

AppEventListenerExceptionListener:
decorates: 'exception_listener' 

我在一个伪应用程序上测试了它,得到了:

Apr 11 21:36:11|CRITI |请求未捕获的PHP异常App\Exception\DomainException:"不再记录此异常",位于D:\www\campagne\src\Dataersister\StationDataPersister.PHP第53行4月11日23:36:12 |警告|服务器POST(400(/api/工作站

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

此代码基于HttpCode激活策略

namespace AppLog
use AppExceptionDomainException;
use MonologHandlerFingersCrossedErrorLevelActivationStrategy;
use SymfonyComponentHttpKernelExceptionHttpException;
/**
* 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使用我们的激活策略

monolog:
handlers:
main:
type: fingers_crossed
action_level: info
handler: nested
activation_strategy: AppLogLogActivationStrategy 
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|警告|服务器POST(400(/api/工作站

就像@yivi一样,我不喜欢我的解决方案,因为每次应用程序都会尝试记录一些东西,你在这个函数中浪费时间。。。这种方法不会更改日志,而是将其删除

在Monolog中,当使用fingers_crossed日志处理程序时,将允许您从日志记录中排除以特定状态响应的请求,只有当异常是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问题上得到了这个答案。它有效,但我不喜欢这个解决方案。希望其他一些答案能在这方面有所改进。

最新更新