diff --git a/CHANGELOG.md b/CHANGELOG.md index 637113d77..c4b5030d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add `Sentry\Monolog\BreadcrumbHandler`, a Monolog handler to allow registration of logs as breadcrumbs (#1199) - Do not setup any error handlers if the DSN is null (#1349) - Add setter for type on the `ExceptionDataBag` (#1347) - Drop symfony/polyfill-uuid in favour of a standalone implementation (#1346) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 62ea23ef5..34c2f4654 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -95,6 +95,11 @@ parameters: count: 1 path: src/Integration/RequestIntegration.php + - + message: "#^Parameter \\#1 \\$level of method Monolog\\\\Handler\\\\AbstractHandler\\:\\:__construct\\(\\) expects 100\\|200\\|250\\|300\\|400\\|500\\|550\\|600\\|'ALERT'\\|'alert'\\|'CRITICAL'\\|'critical'\\|'DEBUG'\\|'debug'\\|'EMERGENCY'\\|'emergency'\\|'ERROR'\\|'error'\\|'INFO'\\|'info'\\|'NOTICE'\\|'notice'\\|'WARNING'\\|'warning'\\|Monolog\\\\Level, int\\|Monolog\\\\Level\\|string given\\.$#" + count: 1 + path: src/Monolog/BreadcrumbHandler.php + - message: "#^Method Sentry\\\\Options\\:\\:getBeforeBreadcrumbCallback\\(\\) should return callable\\(Sentry\\\\Breadcrumb\\)\\: Sentry\\\\Breadcrumb\\|null but returns mixed\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 814952ffa..93bf65e2c 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $parsedDsn['host'] @@ -22,6 +22,21 @@ $userIntegrations + + + $record['channel'] + $record['level'] + $record['level'] + $record['message'] + + + getTimestamp + + + Level|int + int|string|Level|LogLevel::* + + CompatibilityProcessingHandlerTrait @@ -80,32 +95,4 @@ startTransaction - - - $name - , - , - , - , - , - , - , - , - , - Level - case - - - - - \DateTimeImmutable - - - - - : - => - } - - diff --git a/src/Monolog/BreadcrumbHandler.php b/src/Monolog/BreadcrumbHandler.php new file mode 100644 index 000000000..8c5e03b0a --- /dev/null +++ b/src/Monolog/BreadcrumbHandler.php @@ -0,0 +1,101 @@ +hub = $hub; + + parent::__construct($level, $bubble); + } + + /** + * @psalm-suppress MoreSpecificImplementedParamType + * + * @param LogRecord|array{ + * level: int, + * channel: string, + * datetime: \DateTimeImmutable, + * message: string, + * extra?: array + * } $record {@see https://github.com/Seldaek/monolog/blob/main/doc/message-structure.md} + */ + protected function write($record): void + { + $breadcrumb = new Breadcrumb( + $this->getBreadcrumbLevel($record['level']), + $this->getBreadcrumbType($record['level']), + $record['channel'], + $record['message'], + ($record['context'] ?? []) + ($record['extra'] ?? []), + $record['datetime']->getTimestamp() + ); + + $this->hub->addBreadcrumb($breadcrumb); + } + + /** + * @param Level|int $level + */ + private function getBreadcrumbLevel($level): string + { + if ($level instanceof Level) { + $level = $level->value; + } + + switch ($level) { + case Logger::DEBUG: + return Breadcrumb::LEVEL_DEBUG; + case Logger::INFO: + case Logger::NOTICE: + return Breadcrumb::LEVEL_INFO; + case Logger::WARNING: + return Breadcrumb::LEVEL_WARNING; + case Logger::ERROR: + return Breadcrumb::LEVEL_ERROR; + default: + return Breadcrumb::LEVEL_FATAL; + } + } + + private function getBreadcrumbType(int $level): string + { + if ($level >= Logger::ERROR) { + return Breadcrumb::TYPE_ERROR; + } + + return Breadcrumb::TYPE_DEFAULT; + } +} diff --git a/tests/Monolog/BreadcrumbHandlerTest.php b/tests/Monolog/BreadcrumbHandlerTest.php new file mode 100644 index 000000000..14802d951 --- /dev/null +++ b/tests/Monolog/BreadcrumbHandlerTest.php @@ -0,0 +1,88 @@ +createMock(HubInterface::class); + $hub->expects($this->once()) + ->method('addBreadcrumb') + ->with($this->callback(function (Breadcrumb $breadcrumb) use ($expectedBreadcrumb, $record): bool { + $this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage()); + $this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel()); + $this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType()); + $this->assertEquals($record['datetime']->getTimestamp(), $breadcrumb->getTimestamp()); + $this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory()); + $this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata()); + + return true; + })); + + $handler = new BreadcrumbHandler($hub); + $handler->handle($record); + } + + /** + * @return iterable, Breadcrumb}> + */ + public function handleDataProvider(): iterable + { + $defaultBreadcrumb = new Breadcrumb( + Breadcrumb::LEVEL_DEBUG, + Breadcrumb::TYPE_DEFAULT, + 'channel.foo', + 'foo bar', + [] + ); + + $levelsToBeTested = [ + Logger::DEBUG => Breadcrumb::LEVEL_DEBUG, + Logger::INFO => Breadcrumb::LEVEL_INFO, + Logger::NOTICE => Breadcrumb::LEVEL_INFO, + Logger::WARNING => Breadcrumb::LEVEL_WARNING, + ]; + + foreach ($levelsToBeTested as $loggerLevel => $breadcrumbLevel) { + yield 'with level ' . Logger::getLevelName($loggerLevel) => [ + RecordFactory::create('foo bar', $loggerLevel, 'channel.foo', [], []), + $defaultBreadcrumb->withLevel($breadcrumbLevel), + ]; + } + + yield 'with level ERROR' => [ + RecordFactory::create('foo bar', Logger::ERROR, 'channel.foo', [], []), + $defaultBreadcrumb->withLevel(Breadcrumb::LEVEL_ERROR) + ->withType(Breadcrumb::TYPE_ERROR), + ]; + + yield 'with level ALERT' => [ + RecordFactory::create('foo bar', Logger::ALERT, 'channel.foo', [], []), + $defaultBreadcrumb->withLevel(Breadcrumb::LEVEL_FATAL) + ->withType(Breadcrumb::TYPE_ERROR), + ]; + + yield 'with context' => [ + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', ['context' => ['foo' => 'bar']], []), + $defaultBreadcrumb->withMetadata('context', ['foo' => 'bar']), + ]; + + yield 'with extra' => [ + RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], ['extra' => ['foo' => 'bar']]), + $defaultBreadcrumb->withMetadata('extra', ['foo' => 'bar']), + ]; + } +} diff --git a/tests/Monolog/RecordFactory.php b/tests/Monolog/RecordFactory.php index 9f65473db..b302c509b 100644 --- a/tests/Monolog/RecordFactory.php +++ b/tests/Monolog/RecordFactory.php @@ -40,6 +40,7 @@ public static function create(string $message, int $level, string $channel, arra 'level_name' => Logger::getLevelName($level), 'channel' => $channel, 'extra' => $extra, + 'datetime' => new \DateTimeImmutable(), ]; } }