diff --git a/src/Monolog/LogsHandler.php b/src/Monolog/LogsHandler.php index 9ee342a4f..b92640b14 100644 --- a/src/Monolog/LogsHandler.php +++ b/src/Monolog/LogsHandler.php @@ -18,7 +18,9 @@ class LogsHandler implements HandlerInterface /** * The minimum logging level at which this handler will be triggered. * - * @var LogLevel + * @psalm-suppress UndefinedDocblockClass + * + * @var LogLevel|\Monolog\Level|int */ private $logLevel; @@ -32,21 +34,32 @@ class LogsHandler implements HandlerInterface /** * Creates a new Monolog handler that converts Monolog logs to Sentry logs. * - * @param LogLevel|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs - * @param bool $bubble whether the messages that are handled can bubble up the stack or not + * @psalm-suppress UndefinedDocblockClass + * + * @param LogLevel|\Monolog\Level|int|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs + * @param bool $bubble whether the messages that are handled can bubble up the stack or not */ - public function __construct(?LogLevel $logLevel = null, bool $bubble = true) + public function __construct($logLevel = null, bool $bubble = true) { $this->logLevel = $logLevel ?? LogLevel::debug(); $this->bubble = $bubble; } /** + * @psalm-suppress UndefinedDocblockClass + * @psalm-suppress UndefinedClass + * * @param array|LogRecord $record */ public function isHandling($record): bool { - return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority(); + if ($this->logLevel instanceof LogLevel) { + return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority(); + } elseif ($this->logLevel instanceof \Monolog\Level) { + return $record['level'] >= $this->logLevel->value; + } else { + return $record['level'] >= $this->logLevel; + } } /** diff --git a/tests/Monolog/LogsHandlerTest.php b/tests/Monolog/LogsHandlerTest.php index 2a6af3298..004a80c38 100644 --- a/tests/Monolog/LogsHandlerTest.php +++ b/tests/Monolog/LogsHandlerTest.php @@ -4,19 +4,17 @@ namespace Sentry\Tests\Monolog; +use Monolog\Level; use Monolog\Logger; use PHPUnit\Framework\TestCase; use Sentry\ClientBuilder; -use Sentry\Event; use Sentry\Logs\Log; use Sentry\Logs\LogLevel; use Sentry\Logs\Logs; use Sentry\Monolog\LogsHandler; use Sentry\SentrySdk; use Sentry\State\Hub; -use Sentry\Transport\Result; -use Sentry\Transport\ResultStatus; -use Sentry\Transport\TransportInterface; +use Sentry\Tests\StubTransport; final class LogsHandlerTest extends TestCase { @@ -64,42 +62,47 @@ static function (string $key) { } /** - * @dataProvider logLevelDataProvider + * @dataProvider monologLegacyLevelDataProvider */ - public function testLogLevels($record, int $countLogs): void + public function testFiltersAndMapsUsingLegacyMonologThreshold(int $threshold, int $recordLevel, int $expectedCount, ?LogLevel $expectedMappedLevel): void { - $handler = new LogsHandler(LogLevel::warn()); - $handler->handle($record); + $handler = new LogsHandler($threshold); + $handler->handle(RecordFactory::create('foo bar', $recordLevel, 'channel.foo', [], [])); $logs = Logs::getInstance()->aggregator()->all(); - $this->assertCount($countLogs, $logs); + $this->assertCount($expectedCount, $logs); + + if ($expectedMappedLevel !== null) { + $this->assertEquals($expectedMappedLevel, $logs[0]->getLevel()); + } + } + + /** + * @dataProvider monologLevelDataProvider + */ + public function testFiltersAndMapsUsingMonologEnumThreshold($threshold, $recordLevel, int $expectedCount, ?LogLevel $expectedMappedLevel): void + { + if (!class_exists(Level::class)) { + $this->markTestSkipped('Test only works for Monolog >= 3'); + } + + $this->assertInstanceOf(Level::class, $threshold); + $this->assertInstanceOf(Level::class, $recordLevel); + + $handler = new LogsHandler($threshold); + $handler->handle(RecordFactory::create('foo bar', $recordLevel->value, 'channel.foo', [], [])); + + $logs = Logs::getInstance()->aggregator()->all(); + $this->assertCount($expectedCount, $logs); + + if ($expectedMappedLevel !== null) { + $this->assertEquals($expectedMappedLevel, $logs[0]->getLevel()); + } } public function testLogsHandlerDestructor() { - $transport = new class implements TransportInterface { - private $events = []; - - public function send(Event $event): Result - { - $this->events[] = $event; - - return new Result(ResultStatus::success()); - } - - public function close(?int $timeout = null): Result - { - return new Result(ResultStatus::success()); - } - - /** - * @return Event[] - */ - public function getEvents(): array - { - return $this->events; - } - }; + $transport = new StubTransport(); $client = ClientBuilder::create([ 'enable_logs' => true, ])->setTransport($transport) @@ -110,8 +113,8 @@ public function getEvents(): array $this->handleLogAndDrop(); - $this->assertCount(1, $transport->getEvents()); - $this->assertSame('I was dropped :(', $transport->getEvents()[0]->getLogs()[0]->getBody()); + $this->assertCount(1, StubTransport::$events); + $this->assertSame('I was dropped :(', StubTransport::$events[0]->getLogs()[0]->getBody()); } private function handleLogAndDrop(): void @@ -283,83 +286,127 @@ public static function handleDataProvider(): iterable ]; } - public static function logLevelDataProvider(): iterable + public static function monologLegacyLevelDataProvider(): iterable { - yield [ - RecordFactory::create( - 'foo bar', - Logger::DEBUG, - 'channel.foo', - [], - [] - ), + yield 'NOTICE threshold drops INFO (both map to sentry info)' => [ + Logger::NOTICE, + Logger::INFO, 0, + null, ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::NOTICE, - 'channel.foo', - [], - [] - ), + yield 'NOTICE threshold keeps NOTICE (mapped to sentry info)' => [ + Logger::NOTICE, + Logger::NOTICE, + 1, + LogLevel::info(), + ]; + + yield 'NOTICE threshold keeps WARNING (mapped to sentry warn)' => [ + Logger::NOTICE, + Logger::WARNING, + 1, + LogLevel::warn(), + ]; + + yield 'ALERT threshold drops CRITICAL (both map to sentry fatal)' => [ + Logger::ALERT, + Logger::CRITICAL, 0, + null, ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::INFO, - 'channel.foo', - [], - [] - ), + yield 'ALERT threshold keeps ALERT (mapped to sentry fatal)' => [ + Logger::ALERT, + Logger::ALERT, + 1, + LogLevel::fatal(), + ]; + + yield 'ALERT threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Logger::ALERT, + Logger::EMERGENCY, + 1, + LogLevel::fatal(), + ]; + + yield 'EMERGENCY threshold drops ALERT (both map to sentry fatal)' => [ + Logger::EMERGENCY, + Logger::ALERT, 0, + null, ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::WARNING, - 'channel.foo', - [], - [] - ), + yield 'EMERGENCY threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Logger::EMERGENCY, + Logger::EMERGENCY, 1, + LogLevel::fatal(), ]; + } - yield [ - RecordFactory::create( - 'foo bar', - Logger::CRITICAL, - 'channel.foo', - [], - [] - ), + public static function monologLevelDataProvider(): iterable + { + if (!class_exists(Level::class)) { + yield 'Monolog < 3 (skipped)' => [null, null, 0, null]; + + return; + } + + yield 'NOTICE threshold drops INFO (both map to sentry info)' => [ + Level::Notice, + Level::Info, + 0, + null, + ]; + + yield 'NOTICE threshold keeps NOTICE (mapped to sentry info)' => [ + Level::Notice, + Level::Notice, 1, + LogLevel::info(), ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::ALERT, - 'channel.foo', - [], - [] - ), + yield 'NOTICE threshold keeps WARNING (mapped to sentry warn)' => [ + Level::Notice, + Level::Warning, 1, + LogLevel::warn(), ]; - yield [ - RecordFactory::create( - 'foo bar', - Logger::EMERGENCY, - 'channel.foo', - [], - [] - ), + yield 'ALERT threshold drops CRITICAL (both map to sentry fatal)' => [ + Level::Alert, + Level::Critical, + 0, + null, + ]; + + yield 'ALERT threshold keeps ALERT (mapped to sentry fatal)' => [ + Level::Alert, + Level::Alert, + 1, + LogLevel::fatal(), + ]; + + yield 'ALERT threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Level::Alert, + Level::Emergency, + 1, + LogLevel::fatal(), + ]; + + yield 'EMERGENCY threshold drops ALERT (both map to sentry fatal)' => [ + Level::Emergency, + Level::Alert, + 0, + null, + ]; + + yield 'EMERGENCY threshold keeps EMERGENCY (mapped to sentry fatal)' => [ + Level::Emergency, + Level::Emergency, 1, + LogLevel::fatal(), ]; } } diff --git a/tests/Monolog/RecordFactory.php b/tests/Monolog/RecordFactory.php index 39e2b67d6..be2213059 100644 --- a/tests/Monolog/RecordFactory.php +++ b/tests/Monolog/RecordFactory.php @@ -19,7 +19,7 @@ final class RecordFactory * * @return array|LogRecord */ - public static function create(string $message, int $level, string $channel, array $context, array $extra) + public static function create(string $message, int $level, string $channel, array $context = [], array $extra = []) { if (Logger::API >= 3) { return new LogRecord(