8000 [HttpKernel][ErrorHandler] Allow setting log level for exceptions wit… · symfony/symfony@9be54ba · GitHub
[go: up one dir, main page]

Skip to content

Commit 9be54ba

Browse files
committed
[HttpKernel][ErrorHandler] Allow setting log level for exceptions with attribute
1 parent 65e65ac commit 9be54ba

File tree

4 files changed

+130
-16
lines changed

4 files changed

+130
-16
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ErrorHandler\Attribute;
13+
14+
/**
15+
* @author Dejan Angelov <angelovdejan@protonmail.com>
16+
*/
17+
#[\Attribute(\Attribute::TARGET_CLASS)]
18+
final class LogLevel
19+
{
20+
public function __construct(public readonly string $level)
21+
{
22+
if (!defined(sprintf("\Psr\Log\LogLevel::%s", strtoupper($this->level)))) {
23+
throw new \InvalidArgumentException(sprintf("Invalid log level - \"%s\".", $this->level));
24+
}
25+
}
26+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Symfony\Component\ErrorHandler\Tests\Attribute;
5+
6+
use PHPUnit\Framework\TestCase;
7+
use Psr\Log\LogLevel as PsrLogLevel;
8+
use Symfony\Component\ErrorHandler\Attribute\LogLevel;
9+
10+
/**
11+
* @author Dejan Angelov <angelovdejan@protonmail.com>
12+
*/
13+
class LogLevelTest extends TestCase
14+
{
15+
public function testWithValidLogLevel()
16+
{
17+
$logLevel = PsrLogLevel::NOTICE;
18+
19+
$attribute = new LogLevel($logLevel);
20+
21+
$this->assertSame($logLevel, $attribute->level);
22+
}
23+
24+
public function testWithInvalidLogLevel()
25+
{
26+
$this->expectException(\InvalidArgumentException::class);
27+
$this->expectExceptionMessage('Invalid log level - "invalid".');
28+
29+
new LogLevel("invalid");
30+
}
31+
}

src/Symfony/Component/HttpKernel/EventListener/ErrorListener.php

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespace Symfony\Component\HttpKernel\EventListener;
1313

1414
use Psr\Log\LoggerInterface;
15+
use Psr\Log\LogLevel as PsrLogLevel;
16+
use Symfony\Component\ErrorHandler\Attribute\LogLevel;
1517
use Symfony\Component\ErrorHandler\Exception\FlattenException;
1618
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1719
use Symfony\Component\HttpFoundation\Request;
@@ -52,14 +54,7 @@ public function __construct(string|object|array|null $controller, LoggerInterfac
5254
public function logKernelException(ExceptionEvent $event)
5355
{
5456
$throwable = $event->getThrowable();
55-
$logLevel = null;
56-
57-
foreach ($this->exceptionsMapping as $class => $config) {
58-
if ($throwable instanceof $class && $config['log_level']) {
59-
$logLevel = $config['log_level'];
60-
break;
61-
}
62-
}
57+
$logLevel = $this->resolveLogLevel($throwable);
6358

6459
foreach ($this->exceptionsMapping as $class => $config) {
6560
if (!$throwable instanceof $class || !$config['status_code']) {
@@ -104,9 +99,10 @@ public function onKernelException(ExceptionEvent $event)
10499
try {
105100
$response = $event->getKernel()->handle($request, HttpKernelInterface::SUB_REQUEST, false);
106101
} catch (\Exception $e) {
102+
$logLevel = $this->resolveLogLevel($e);
107103
$f = FlattenException::createFromThrowable($e);
108104

109-
$this->logException($e, sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), $e->getFile(), $e->getLine()));
105+
$this->logException($e, sprintf('Exception thrown when handling an exception (%s: %s at %s line %s)', $f->getClass(), $f->getMessage(), $e->getFile(), $e->getLine()), $logLevel);
110106

111107
$prev = $e;
112108
do {
@@ -168,17 +164,38 @@ public static function getSubscribedEvents(): array
168164
/**
169165
* Logs an exception.
170166
*/
171-
protected function logException(\Throwable $exception, string $message, string $logLevel = null): void
167+
protected function logException(\Throwable $exception, string $message, string $logLevel): void
172168
{
173169
if (null !== $this->logger) {
174-
if (null !== $logLevel) {
175-
$this->logger->log($logLevel, $message, ['exception' => $exception]);
176-
} elseif (!$exception instanceof HttpExceptionInterface || $exception->getStatusCode() >= 500) {
177-
$this->logger->critical($message, ['exception' => $exception]);
178-
} else {
179-
$this->logger->error($message, ['exception' => $exception]);
170+
$this->logger->log($logLevel, $message, ['exception' => $exception]);
171+
}
172+
}
173+
174+
/**
175+
* Resolves the level to be used when logging the exception.
176+
*/
177+
private function resolveLogLevel(\Throwable $throwable): string
178+
{
179+
foreach ($this->exceptionsMapping as $class => $config) {
180+
if ($throwable instanceof $class && $config['log_level']) {
181+
return $config['log_level'];
180182
}
181183
}
184+
185+
$attributes = (new \ReflectionClass($throwable))->getAttributes(LogLevel::class);
186+
187+
if ($attributes) {
188+
/** @var LogLevel $instance */
189+
$instance = $attributes[0]->newInstance();
190+
191+
return $instance->level;
192+
}
193+
194+
if (!$throwable instanceof HttpExceptionInterface || $throwable->getStatusCode() >= 500) {
195+
return PsrLogLevel::CRITICAL;
196+
}
197+
198+
return PsrLogLevel::ERROR;
182199
}
183200

184201
/**

src/Symfony/Component/HttpKernel/Tests/EventListener/ErrorListenerTest.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Log\LoggerInterface;
16+
use Symfony\Component\ErrorHandler\Attribute\LogLevel;
1617
use Symfony\Component\ErrorHandler\Exception\FlattenException;
1718
use Symfony\Component\EventDispatcher\EventDispatcher;
1819
use Symfony\Component\HttpFoundation\Request;
@@ -118,6 +119,40 @@ public function testHandleWithLoggerAndCustomConfiguration()
118119
$this->assertCount(1, $logger->getLogs('warning'));
119120
}
120121

122+
public function testHandleWithLogLevelAttribute()
123+
{
124+
$request = new Request();
125+
$event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new WarningWithLogLevelAttribute());
126+
$logger = new TestLogger();
127+
$l = new ErrorListener('not used', $logger);
128+
129+
$l->logKernelException($event);
130+
$l->onKernelException($event);
131+
132+
$this->assertEquals(0, $logger->countErrors());
133+
$this->assertCount(0, $logger->getLogs('critical'));
134+
$this->assertCount(1, $logger->getLogs('warning'));
135+
}
136+
137+
public function testHandleWithLogLevelAttributeAndCustomConfiguration()
138+
{
139+
$request = new Request();
140+
$event = new ExceptionEvent(new TestKernel(), $request, HttpKernelInterface::MAIN_REQUEST, new WarningWithLogLevelAttribute());
141+
$logger = new TestLogger();
142+
$l = new ErrorListener('not used', $logger, false, [
143+
WarningWithLogLevelAttribute::class => [
144+
'log_level' => 'info',
145+
'status_code' => 401
146+
],
147+
]);
148+
$l->logKernelException($event);
149+
$l->onKernelException($event);
150+
151+
$this->assertEquals(0, $logger->countErrors());
152+
$this->assertCount(0, $logger->getLogs('warning'));
153+
$this->assertCount(1, $logger->getLogs('info'));
154+
}
155+
121156
/**
122157
* @dataProvider exceptionWithAttributeProvider
123158
*/
@@ -312,3 +347,8 @@ class WithCustomUserProvidedAttribute extends \Exception
312347
class WithGeneralAttribute extends \Exception
313348
{
314349
}
350+
351+
#[LogLevel(\Psr\Log\LogLevel::WARNING)]
352+
class WarningWithLogLevelAttribute extends \Exception
353+
{
354+
}

0 commit comments

Comments
 (0)
0