Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the invalid message is inconsistent with the check range(100, 499) while invalid message contains text like 100 et 599

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please send a PR? Let's also replace this range() by two comparisons.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check see: #43399

->end()
->defaultNull()
->end()
->end()
->end()
->end()
->end()
;
}

private function addLockSection(ArrayNodeDefinition $rootNode, callable $enableIfStandalone)
{
$rootNode
Expand Down
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor
'name_based_uuid_version' => 5,
'time_based_uuid_version' => 6,
],
'exceptions' => [],
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

$container->loadFromExtension('framework', [
'exceptions' => [
BadRequestHttpException::class => [
'log_level' => 'info',
'status_code' => 422,
],
],
]);
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
framework:
exceptions:
Symfony\Component\HttpKernel\Exception\BadRequestHttpException:
log_level: info
status_code: 422
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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']);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this work? The response is rendered L71 before this happens so whilst the response will have the correct status code the page rendered may not be matching. For example the ErrorController will render a page for a 500 although the status code will be altered here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't undestant. The status code you write in the configuration is the one you want.
It is not used to "match" something. It overrides the current value.

Copy link
Contributor

@theofidry theofidry Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It overrides the current value

Of the created response.

In the case you are dealing with an API, you probably do not care. However, if you are dealing with an HTML response, then you probably have a different template if it's a 400 or a 500. From what I see here although you override the status code from a 500 to a 400 for example, the rendered page would still look like you had a 500 (and with the default template it shows the status code, i.e. 500).

Copy link
Member

@yceruto yceruto Nov 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to update HtmlErrorRenderer to take this new exceptions mapping into account.

break;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that the order you define the exceptions in your config is important. I don't mind, but this should be documented.

}
}

$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
Original file line number Diff line number Diff line change
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