From 742a86344d698f0f6c4ce2cc9c7e00ba4fc0cb54 Mon Sep 17 00:00:00 2001 From: Arkalo2 <24898676+Arkalo2@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:57:41 +0100 Subject: [PATCH] [FrameworkBundle][HttpKernel] Allow configuring the logging channel per type of exceptions --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../DependencyInjection/Configuration.php | 4 ++ .../FrameworkExtension.php | 15 ++++- .../FrameworkBundle/Resources/config/web.php | 1 + .../FrameworkExtensionTestCase.php | 4 ++ src/Symfony/Component/HttpKernel/CHANGELOG.md | 3 +- .../EventListener/ErrorListener.php | 38 ++++++++++--- .../Tests/EventListener/ErrorListenerTest.php | 57 +++++++++++++++++++ 8 files changed, 113 insertions(+), 10 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 9dd35a826a06c..63eca480b4824 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -17,6 +17,7 @@ CHANGELOG * Auto-exclude DI extensions, test cases, entities and messenger messages * Add DI alias from `ServicesResetterInterface` to `services_resetter` * Add `methods` argument in `#[IsCsrfTokenValid]` attribute + * Allow configuring the logging channel per type of exceptions 7.2 --- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 17b4a438b2aec..bb6a3347b4c12 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -1458,6 +1458,10 @@ private function addExceptionsSection(ArrayNodeDefinition $rootNode): void ->end() ->defaultNull() ->end() + ->scalarNode('log_channel') + ->info('The channel of log message. Null to let Symfony decide.') + ->defaultNull() + ->end() ->end() ->end() ->end() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index b8b723d1d221b..fed06a6c74814 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -417,7 +417,20 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); $this->registerSecretsConfiguration($config['secrets'], $container, $loader, $config['secret'] ?? null); - $container->getDefinition('exception_listener')->replaceArgument(3, $config['exceptions']); + $exceptionListener = $container->getDefinition('exception_listener'); + + $loggers = []; + foreach ($config['exceptions'] as $exception) { + if (!isset($exception['log_channel'])) { + continue; + } + $loggers[$exception['log_channel']] = new Reference('monolog.logger.'.$exception['log_channel'], ContainerInterface::NULL_ON_INVALID_REFERENCE); + } + + $exceptionListener + ->replaceArgument(3, $config['exceptions']) + ->setArgument(4, $loggers) + ; if ($this->readConfigEnabled('serializer', $container, $config['serializer'])) { if (!class_exists(Serializer::class)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php index 6f8358fb0c7b8..a4e975dac8749 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.php @@ -138,6 +138,7 @@ service('logger')->nullOnInvalid(), param('kernel.debug'), abstract_arg('an exceptions to log & status code mapping'), + abstract_arg('list of loggers by log_channel'), ]) ->tag('kernel.event_subscriber') ->tag('monolog.logger', ['channel' => 'request']) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php index fdc586cc922ba..9edbf111ebe7b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php @@ -615,21 +615,25 @@ public function testExceptionsConfig() ], array_keys($configuration)); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => 'info', 'status_code' => 422, ], $configuration[\Symfony\Component\HttpKernel\Exception\BadRequestHttpException::class]); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => 'info', 'status_code' => null, ], $configuration[\Symfony\Component\HttpKernel\Exception\NotFoundHttpException::class]); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => 'info', 'status_code' => null, ], $configuration[\Symfony\Component\HttpKernel\Exception\ConflictHttpException::class]); $this->assertEqualsCanonicalizing([ + 'log_channel' => null, 'log_level' => null, 'status_code' => 500, ], $configuration[\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class]); diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 1d533c29f51c8..6bf1a60ebc6e2 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -7,7 +7,8 @@ CHANGELOG * Add `$key` argument to `#[MapQueryString]` that allows using a specific key for argument resolving * Support `Uid` in `#[MapQueryParameter]` * Add `ServicesResetterInterface`, implemented by `ServicesResetter` - + * Allow configuring the logging channel per type of exceptions in ErrorListener + 7.2 --- diff --git a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php index c677958cde377..18e8bff4413d8 100644 --- a/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php +++ b/src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php @@ -34,13 +34,14 @@ class ErrorListener implements EventSubscriberInterface { /** - * @param array|null}> $exceptionsMapping + * @param array|null, log_channel: string|null}> $exceptionsMapping */ public function __construct( protected string|object|array|null $controller, protected ?LoggerInterface $logger = null, protected bool $debug = false, protected array $exceptionsMapping = [], + protected array $loggers = [], ) { } @@ -48,6 +49,7 @@ public function logKernelException(ExceptionEvent $event): void { $throwable = $event->getThrowable(); $logLevel = $this->resolveLogLevel($throwable); + $logChannel = $this->resolveLogChannel($throwable); foreach ($this->exceptionsMapping as $class => $config) { if (!$throwable instanceof $class || !$config['status_code']) { @@ -69,7 +71,7 @@ public function logKernelException(ExceptionEvent $event): void $e = FlattenException::createFromThrowable($throwable); - $this->logException($throwable, \sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), basename($e->getFile()), $e->getLine()), $logLevel); + $this->logException($throwable, \sprintf('Uncaught PHP Exception %s: "%s" at %s line %s', $e->getClass(), $e->getMessage(), basename($e->getFile()), $e->getLine()), $logLevel, $logChannel); } public function onKernelException(ExceptionEvent $event): void @@ -159,16 +161,20 @@ public static function getSubscribedEvents(): array /** * Logs an exception. + * + * @param ?string $logChannel */ - protected function logException(\Throwable $exception, string $message, ?string $logLevel = null): void + protected function logException(\Throwable $exception, string $message, ?string $logLevel = null, /* ?string $logChannel = null */): void { - if (null === $this->logger) { + $logChannel = (3 < \func_num_args() ? \func_get_arg(3) : null) ?? $this->resolveLogChannel($exception); + + $logLevel ??= $this->resolveLogLevel($exception); + + if(!$logger = $this->getLogger($logChannel)) { return; } - $logLevel ??= $this->resolveLogLevel($exception); - - $this->logger->log($logLevel, $message, ['exception' => $exception]); + $logger->log($logLevel, $message, ['exception' => $exception]); } /** @@ -193,6 +199,17 @@ private function resolveLogLevel(\Throwable $throwable): string return LogLevel::ERROR; } + private function resolveLogChannel(\Throwable $throwable): ?string + { + foreach ($this->exceptionsMapping as $class => $config) { + if ($throwable instanceof $class && isset($config['log_channel'])) { + return $config['log_channel']; + } + } + + return null; + } + /** * Clones the request for the exception. */ @@ -201,7 +218,7 @@ protected function duplicateRequest(\Throwable $exception, Request $request): Re $attributes = [ '_controller' => $this->controller, 'exception' => $exception, - 'logger' => DebugLoggerConfigurator::getDebugLogger($this->logger), + 'logger' => DebugLoggerConfigurator::getDebugLogger($this->getLogger($exception)), ]; $request = $request->duplicate(null, null, $attributes); $request->setMethod('GET'); @@ -249,4 +266,9 @@ private function getInheritedAttribute(string $class, string $attribute): ?objec return $attributeReflector?->newInstance(); } + + private function getLogger(?string $logChannel): ?LoggerInterface + { + return $logChannel ? $this->loggers[$logChannel] ?? $this->logger : $this->logger; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php index 2e1f7d58b7258..7fdda59635935 100644 --- a/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php @@ -143,6 +143,63 @@ public function testHandleWithLogLevelAttribute() $this->assertCount(1, $logger->getLogsForLevel('warning')); } + public function testHandleWithLogChannel() + { + $request = new Request(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); + + $defaultLogger = new TestLogger(); + $channelLoger = new TestLogger(); + + $l = new ErrorListener('not used', $defaultLogger, false, [ + \RuntimeException::class => [ + 'log_level' => 'warning', + 'status_code' => 401, + 'log_channel' => 'channel', + ], + \Exception::class => [ + 'log_level' => 'error', + 'status_code' => 402, + ], + ], ['channel' => $channelLoger]); + + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertCount(0, $defaultLogger->getLogsForLevel('error')); + $this->assertCount(0, $defaultLogger->getLogsForLevel('warning')); + $this->assertCount(0, $channelLoger->getLogsForLevel('error')); + $this->assertCount(1, $channelLoger->getLogsForLevel('warning')); + } + + public function testHandleWithLoggerChannelNotUsed() + { + $request = new Request(); + $event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new \RuntimeException('bar')); + $defaultLogger = new TestLogger(); + $channelLoger = new TestLogger(); + $l = new ErrorListener('not used', $defaultLogger, false, [ + \RuntimeException::class => [ + 'log_level' => 'warning', + 'status_code' => 401, + ], + \ErrorException::class => [ + 'log_level' => 'error', + 'status_code' => 402, + 'log_channel' => 'channel', + ], + ], ['channel' => $channelLoger]); + $l->logKernelException($event); + $l->onKernelException($event); + + $this->assertSame(0, $defaultLogger->countErrors()); + $this->assertCount(0, $defaultLogger->getLogsForLevel('critical')); + $this->assertCount(1, $defaultLogger->getLogsForLevel('warning')); + $this->assertCount(0, $channelLoger->getLogsForLevel('warning')); + $this->assertCount(0, $channelLoger->getLogsForLevel('error')); + $this->assertCount(0, $channelLoger->getLogsForLevel('critical')); + } + public function testHandleClassImplementingInterfaceWithLogLevelAttribute() { $request = new Request();