Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add breadcrumb monolog handler #1199

Merged
merged 12 commits into from
Sep 1, 2022
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(),
];
}
}