Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 16 additions & 29 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="4.23.0@f1fe6ff483bf325c803df9f510d09a03fd796f88">
<files psalm-version="4.26.0@6998fabb2bf528b65777bf9941920888d23c03ac">
<file src="src/Dsn.php">
<PossiblyUndefinedArrayOffset occurrences="4">
<code>$parsedDsn['host']</code>
Expand All @@ -22,6 +22,21 @@
<code>$userIntegrations</code>
</PossiblyInvalidArgument>
</file>
<file src="src/Monolog/BreadcrumbHandler.php">
<PossiblyInvalidArgument occurrences="4">
<code>$record['channel']</code>
<code>$record['level']</code>
<code>$record['level']</code>
<code>$record['message']</code>
</PossiblyInvalidArgument>
<PossiblyInvalidMethodCall occurrences="1">
<code>getTimestamp</code>
</PossiblyInvalidMethodCall>
<UndefinedDocblockClass occurrences="2">
<code>Level|int</code>
<code>int|string|Level|LogLevel::*</code>
</UndefinedDocblockClass>
</file>
<file src="src/Monolog/CompatibilityProcessingHandlerTrait.php">
<DuplicateClass occurrences="1">
<code>CompatibilityProcessingHandlerTrait</code>
Expand Down Expand Up @@ -80,32 +95,4 @@
<code>startTransaction</code>
</TooManyArguments>
</file>
<file src="vendor/monolog/monolog/src/Monolog/Level.php">
<ParseError occurrences="12">
<code>$name</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>,</code>
<code>Level</code>
<code>case</code>
</ParseError>
</file>
<file src="vendor/monolog/monolog/src/Monolog/LogRecord.php">
<ParseError occurrences="1">
<code>\DateTimeImmutable</code>
</ParseError>
</file>
<file src="vendor/monolog/monolog/src/Monolog/Utils.php">
<ParseError occurrences="3">
<code>:</code>
<code>=&gt;</code>
<code>}</code>
</ParseError>
</file>
</files>
101 changes: 101 additions & 0 deletions src/Monolog/BreadcrumbHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<?php

declare(strict_types=1);

namespace Sentry\Monolog;

use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Level;
use Monolog\Logger;
use Monolog\LogRecord;
use Psr\Log\LogLevel;
use Sentry\Breadcrumb;
use Sentry\Event;
use Sentry\State\HubInterface;
use Sentry\State\Scope;

/**
* This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see Scope},
* to enrich any event sent to Sentry.
*/
final class BreadcrumbHandler extends AbstractProcessingHandler
{
/**
* @var HubInterface
*/
private $hub;

/**
* @phpstan-param int|string|Level|LogLevel::* $level
*
* @param HubInterface $hub The hub to which errors are reported
* @param int|string $level The minimum logging level at which this
* handler will be triggered
* @param bool $bubble Whether the messages that are handled can
* bubble up the stack or not
*/
public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true)
{
$this->hub = $hub;

parent::__construct($level, $bubble);
}

/**
* @psalm-suppress MoreSpecificImplementedParamType
*
* @param LogRecord|array{
* level: int,
* channel: string,
* datetime: \DateTimeImmutable,
* message: string,
* extra?: array<string, mixed>
* } $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;
}
}
88 changes: 88 additions & 0 deletions tests/Monolog/BreadcrumbHandlerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php

declare(strict_types=1);

namespace Sentry\Tests\Monolog;

use Monolog\Logger;
use Monolog\LogRecord;
use PHPUnit\Framework\TestCase;
use Sentry\Breadcrumb;
use Sentry\Monolog\BreadcrumbHandler;
use Sentry\State\HubInterface;

final class BreadcrumbHandlerTest extends TestCase
{
/**
* @dataProvider handleDataProvider
*/
public function testHandle($record, Breadcrumb $expectedBreadcrumb): void
{
$hub = $this->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<LogRecord|array{array<string, mixed>, 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']),
];
}
}
1 change: 1 addition & 0 deletions tests/Monolog/RecordFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
];
}
}