如何避免记录由 Api-Platform 正确转换为状态代码的预期异常?
How to avoid logging expected exceptions that are correctly converted to status codes by Api-Platform?
在我们的 Api-Platform 项目的一些路由中,一些常见错误情况的例外情况是 throw
n。
例如在调用 POST /orders
时,NewOrderHandler
可以在适当的情况下抛出这两个中的任何一个:
NotEnoughStock
NotEnoughCredit
所有这些异常都属于 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'
在我们的 Api-Platform 项目的一些路由中,一些常见错误情况的例外情况是 throw
n。
例如在调用 POST /orders
时,NewOrderHandler
可以在适当的情况下抛出这两个中的任何一个:
NotEnoughStock
NotEnoughCredit
所有这些异常都属于 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'