Skip to content

Commit

Permalink
feature #42244 [HttpKernel] Add support for configuring log level, an…
Browse files Browse the repository at this point in the history
…d status code by exception class (lyrixx)

This PR was merged into the 5.4 branch.

Discussion
----------

[HttpKernel] Add support for configuring log level, and status code by exception class

| Q             | A
| ------------- | ---
| Branch?       | 5.4
| Bug fix?      | no
| New feature?  | yes
| Deprecations? | no
| Tickets       | Fix #38901
| License       | MIT
| Doc PR        |

Usage:

```yaml
# config/packages/framework.yaml
framework:
    exceptions:
        Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
            log_level: debug
            status_code: 422
```

Commits
-------

94c4de3 [HttpKernel] Add support for configuring log level, and status code by exception class
  • Loading branch information
fabpot committed Sep 29, 2021
2 parents 0e843d9 + 94c4de3 commit e026dc5
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Expand Up @@ -11,6 +11,7 @@ CHANGELOG
* Deprecate the `cache.adapter.doctrine` service
* Add support for resetting container services after each messenger message
* Add `configureContainer()`, `configureRoutes()`, `getConfigDir()` and `getBundlesPath()` to `MicroKernelTrait`
* Add support for configuring log level, and status code by exception class

5.3
---
Expand Down
Expand Up @@ -15,6 +15,7 @@
use Doctrine\Common\Annotations\PsrCachedReader;
use Doctrine\Common\Cache\Cache;
use Doctrine\DBAL\Connection;
use Psr\Log\LogLevel;
use Symfony\Bundle\FullStack;
use Symfony\Component\Asset\Package;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
Expand Down Expand Up @@ -139,6 +140,7 @@ public function getConfigTreeBuilder()
$this->addPropertyInfoSection($rootNode, $enableIfStandalone);
$this->addCacheSection($rootNode, $willBeAvailable);
$this->addPhpErrorsSection($rootNode);
$this->addExceptionsSection($rootNode);
$this->addWebLinkSection($rootNode, $enableIfStandalone);
$this->addLockSection($rootNode, $enableIfStandalone);
$this->addMessengerSection($rootNode, $enableIfStandalone);
Expand Down Expand Up @@ -1163,6 +1165,64 @@ private function addPhpErrorsSection(ArrayNodeDefinition $rootNode)
;
}

private function addExceptionsSection(ArrayNodeDefinition $rootNode)
{
$logLevels = (new \ReflectionClass(LogLevel::class))->getConstants();

$rootNode
->children()
->arrayNode('exceptions')
->info('Exception handling configuration')
->beforeNormalization()
->ifArray()
->then(function (array $v): array {
if (!\array_key_exists('exception', $v)) {
return $v;
}

// Fix XML normalization
$data = isset($v['exception'][0]) ? $v['exception'] : [$v['exception']];
$exceptions = [];
foreach ($data as $exception) {
$config = [];
if (\array_key_exists('log-level', $exception)) {
$config['log_level'] = $exception['log-level'];
}
if (\array_key_exists('status-code', $exception)) {
$config['status_code'] = $exception['status-code'];
}
$exceptions[$exception['name']] = $config;
}

return $exceptions;
})
->end()
->prototype('array')
->fixXmlConfig('exception')
->children()
->scalarNode('log_level')
->info('The level of log message. Null to let Symfony decide.')
->validate()
->ifTrue(function ($v) use ($logLevels) { return !\in_array($v, $logLevels); })
->thenInvalid(sprintf('The log level is not valid. Pick one among "%s".', implode('", "', $logLevels)))
->end()
->defaultNull()
->end()
->scalarNode('status_code')
->info('The status code of the response. Null to let Symfony decide.')
->validate()
->ifTrue(function ($v) { return !\in_array($v, range(100, 499)); })
->thenInvalid('The log level is not valid. Pick one among between 100 et 599.')
->end()
->defaultNull()
->end()
->end()
->end()
->end()
->end()
;
}

private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
Expand Down
Expand Up @@ -427,6 +427,8 @@ public function load(array $configs, ContainerBuilder $container)
$this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader);
$this->registerSecretsConfiguration($config['secrets'], $container, $loader);

$container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']);

if ($this->isConfigEnabled($container, $config['serializer'])) {
if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) {
throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".');
Expand Down
Expand Up @@ -29,6 +29,7 @@
<xsd:element name="cache" type="cache" minOccurs="0" maxOccurs="1" />
<xsd:element name="workflow" type="workflow" minOccurs="0" maxOccurs="unbounded" />
<xsd:element name="php-errors" type="php-errors" minOccurs="0" maxOccurs="1" />
<xsd:element name="exceptions" type="exceptions" minOccurs="0" maxOccurs="1" />
<xsd:element name="lock" type="lock" minOccurs="0" maxOccurs="1" />
<xsd:element name="messenger" type="messenger" minOccurs="0" maxOccurs="1" />
<xsd:element name="http-client" type="http_client" minOccurs="0" maxOccurs="1" />
Expand Down Expand Up @@ -346,6 +347,18 @@
<xsd:attribute name="enabled" type="xsd:boolean" />
</xsd:complexType>

<xsd:complexType name="exceptions">
<xsd:sequence>
<xsd:element name="exception" type="exception" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="exception">
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="log-level" type="xsd:string" />
<xsd:attribute name="status-code" type="xsd:int" />
</xsd:complexType>

<xsd:complexType name="marking_store">
<xsd:sequence>
<xsd:element name="argument" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />
Expand Down
Expand Up @@ -102,6 +102,7 @@
param('kernel.error_controller'),
service('logger')->nullOnInvalid(),
param('kernel.debug'),
abstract_arg('an exceptions to log & status code mapping'),
])
->tag('kernel.event_subscriber')
->tag('monolog.logger', ['channel' => 'request'])
Expand Down
Expand Up @@ -579,6 +579,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'name_based_uuid_version' => 5,
'time_based_uuid_version' => 6,
],
'exceptions' => [],
];
}
}
@@ -0,0 +1,12 @@
<?php

use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

$container->loadFromExtension('framework', [
'exceptions' => [
BadRequestHttpException::class => [
'log_level' => 'info',
'status_code' => 422,
],
],
]);
@@ -0,0 +1,13 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:framework="http://symfony.com/schema/dic/symfony"
xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd
http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd">

<framework:config>
<framework:exceptions>
<framework:exception name="Symfony\Component\HttpKernel\Exception\BadRequestHttpException" log-level="info" status-code="422" />
</framework:exceptions>
</framework:config>
</container>
@@ -0,0 +1,5 @@
framework:
exceptions:
Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
log_level: info
status_code: 422
Expand Up @@ -521,6 +521,18 @@ public function testPhpErrorsWithLogLevels()
], $definition->getArgument(2));
}

public function testExceptionsConfig()
{
$container = $this->createContainerFromFile('exceptions');

$this->assertSame([
\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class => [
'log_level' => 'info',
'status_code' => 422,
],
], $container->getDefinition('exception_listener')->getArgument(3));
}

public function testRouter()
{
$container = $this->createContainerFromFile('full');
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpKernel/CHANGELOG.md
Expand Up @@ -6,6 +6,7 @@ CHANGELOG

* Deprecate `AbstractTestSessionListener::getSession` inject a session in the request instead
* Deprecate the `fileLinkFormat` parameter of `DebugHandlersListener`
* Add support for configuring log level, and status code by exception class

5.3
---
Expand Down
38 changes: 29 additions & 9 deletions src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php
Expand Up @@ -32,19 +32,30 @@ class ErrorListener implements EventSubscriberInterface
protected $controller;
protected $logger;
protected $debug;
protected $exceptionsMapping;

public function __construct($controller, LoggerInterface $logger = null, bool $debug = false)
public function __construct($controller, LoggerInterface $logger = null, bool $debug = false, array $exceptionsMapping = [])
{
$this->controller = $controller;
$this->logger = $logger;
$this->debug = $debug;
$this->exceptionsMapping = $exceptionsMapping;
}

public function logKernelException(ExceptionEvent $event)
{
$e = FlattenException::createFromThrowable($event->getThrowable());
$throwable = $event->getThrowable();
$logLevel = null;
foreach ($this->exceptionsMapping as $class => $config) {
if ($throwable instanceof $class && $config['log_level']) {
$logLevel = $config['log_level'];
break;
}
}

$e = FlattenException::createFromThrowable($throwable);

$this->logException($event->getThrowable(), sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()));
$this->logException($throwable, sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), $e->getFile(), $e->getLine()), $logLevel);
}

public function onKernelException(ExceptionEvent $event)
Expand All @@ -53,8 +64,8 @@ public function onKernelException(ExceptionEvent $event)
return;
}

$exception = $event->getThrowable();
$request = $this->duplicateRequest($exception, $event->getRequest());
$throwable = $event->getThrowable();
$request = $this->duplicateRequest($throwable, $event->getRequest());

try {
$response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false);
Expand All @@ -65,18 +76,25 @@ public function onKernelException(ExceptionEvent $event)

$prev = $e;
do {
if ($exception === $wrapper = $prev) {
if ($throwable === $wrapper = $prev) {
throw $e;
}
} while ($prev = $wrapper->getPrevious());

$prev = new \ReflectionProperty($wrapper instanceof \Exception ? \Exception::class : \Error::class, 'previous');
$prev->setAccessible(true);
$prev->setValue($wrapper, $exception);
$prev->setValue($wrapper, $throwable);

throw $e;
}

foreach ($this->exceptionsMapping as $exception => $config) {
if ($throwable instanceof $exception && $config['status_code']) {
$response->setStatusCode($config['status_code']);
break;
}
}

$event->setResponse($response);

if ($this->debug) {
Expand Down Expand Up @@ -124,10 +142,12 @@ public static function getSubscribedEvents(): array
/**
* Logs an exception.
*/
protected function logException(\Throwable $exception, string $message): void
protected function logException(\Throwable $exception, string $message, string $logLevel = null): void
{
if (null !== $this->logger) {
if (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
if (null !== $logLevel) {
$this->logger->log($logLevel, $message, ['exception' => $exception]);
} elseif (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
$this->logger->critical($message, ['exception' => $exception]);
} else {
$this->logger->error($message, ['exception' => $exception]);
Expand Down
Expand Up @@ -97,6 +97,27 @@ public function testHandleWithLogger($event, $event2)
$this->assertCount(3, $logger->getLogs('critical'));
}

public function testHandleWithLoggerAndCustomConfiguration()
{
$request = new Request();
$event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar'));
$logger = new TestLogger();
$l = new ErrorListener('not used', $logger, false, [
\RuntimeException::class => [
'log_level' => 'warning',
'status_code' => 401,
],
]);
$l->logKernelException($event);
$l->onKernelException($event);

$this->assertEquals(new Response('foo', 401), $event->getResponse());

$this->assertEquals(0, $logger->countErrors());
$this->assertCount(0, $logger->getLogs('critical'));
$this->assertCount(1, $logger->getLogs('warning'));
}

public function provider()
{
if (!class_exists(Request::class)) {
Expand Down

0 comments on commit e026dc5

Please sign in to comment.