Skip to content

Commit 6695102

Browse files
authored
Add breadcrumb monolog handler (#1199)
1 parent 39fa647 commit 6695102

6 files changed

+212
-29
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Unreleased
44

5+
- Add `Sentry\Monolog\BreadcrumbHandler`, a Monolog handler to allow registration of logs as breadcrumbs (#1199)
56
- Do not setup any error handlers if the DSN is null (#1349)
67
- Add setter for type on the `ExceptionDataBag` (#1347)
78
- Drop symfony/polyfill-uuid in favour of a standalone implementation (#1346)

phpstan-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,11 @@ parameters:
9595
count: 1
9696
path: src/Integration/RequestIntegration.php
9797

98+
-
99+
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\\.$#"
100+
count: 1
101+
path: src/Monolog/BreadcrumbHandler.php
102+
98103
-
99104
message: "#^Method Sentry\\\\Options\\:\\:getBeforeBreadcrumbCallback\\(\\) should return callable\\(Sentry\\\\Breadcrumb\\)\\: Sentry\\\\Breadcrumb\\|null but returns mixed\\.$#"
100105
count: 1

psalm-baseline.xml

+16-29
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<files psalm-version="4.23.0@f1fe6ff483bf325c803df9f510d09a03fd796f88">
2+
<files psalm-version="4.26.0@6998fabb2bf528b65777bf9941920888d23c03ac">
33
<file src="src/Dsn.php">
44
<PossiblyUndefinedArrayOffset occurrences="4">
55
<code>$parsedDsn['host']</code>
@@ -22,6 +22,21 @@
2222
<code>$userIntegrations</code>
2323
</PossiblyInvalidArgument>
2424
</file>
25+
<file src="src/Monolog/BreadcrumbHandler.php">
26+
<PossiblyInvalidArgument occurrences="4">
27+
<code>$record['channel']</code>
28+
<code>$record['level']</code>
29+
<code>$record['level']</code>
30+
<code>$record['message']</code>
31+
</PossiblyInvalidArgument>
32+
<PossiblyInvalidMethodCall occurrences="1">
33+
<code>getTimestamp</code>
34+
</PossiblyInvalidMethodCall>
35+
<UndefinedDocblockClass occurrences="2">
36+
<code>Level|int</code>
37+
<code>int|string|Level|LogLevel::*</code>
38+
</UndefinedDocblockClass>
39+
</file>
2540
<file src="src/Monolog/CompatibilityProcessingHandlerTrait.php">
2641
<DuplicateClass occurrences="1">
2742
<code>CompatibilityProcessingHandlerTrait</code>
@@ -80,32 +95,4 @@
8095
<code>startTransaction</code>
8196
</TooManyArguments>
8297
</file>
83-
<file src="vendor/monolog/monolog/src/Monolog/Level.php">
84-
<ParseError occurrences="12">
85-
<code>$name</code>
86-
<code>,</code>
87-
<code>,</code>
88-
<code>,</code>
89-
<code>,</code>
90-
<code>,</code>
91-
<code>,</code>
92-
<code>,</code>
93-
<code>,</code>
94-
<code>,</code>
95-
<code>Level</code>
96-
<code>case</code>
97-
</ParseError>
98-
</file>
99-
<file src="vendor/monolog/monolog/src/Monolog/LogRecord.php">
100-
<ParseError occurrences="1">
101-
<code>\DateTimeImmutable</code>
102-
</ParseError>
103-
</file>
104-
<file src="vendor/monolog/monolog/src/Monolog/Utils.php">
105-
<ParseError occurrences="3">
106-
<code>:</code>
107-
<code>=&gt;</code>
108-
<code>}</code>
109-
</ParseError>
110-
</file>
11198
</files>

src/Monolog/BreadcrumbHandler.php

+101
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Monolog;
6+
7+
use Monolog\Handler\AbstractProcessingHandler;
8+
use Monolog\Level;
9+
use Monolog\Logger;
10+
use Monolog\LogRecord;
11+
use Psr\Log\LogLevel;
12+
use Sentry\Breadcrumb;
13+
use Sentry\Event;
14+
use Sentry\State\HubInterface;
15+
use Sentry\State\Scope;
16+
17+
/**
18+
* This Monolog handler logs every message as a {@see Breadcrumb} into the current {@see Scope},
19+
* to enrich any event sent to Sentry.
20+
*/
21+
final class BreadcrumbHandler extends AbstractProcessingHandler
22+
{
23+
/**
24+
* @var HubInterface
25+
*/
26+
private $hub;
27+
28+
/**
29+
* @phpstan-param int|string|Level|LogLevel::* $level
30+
*
31+
* @param HubInterface $hub The hub to which errors are reported
32+
* @param int|string $level The minimum logging level at which this
33+
* handler will be triggered
34+
* @param bool $bubble Whether the messages that are handled can
35+
* bubble up the stack or not
36+
*/
37+
public function __construct(HubInterface $hub, $level = Logger::DEBUG, bool $bubble = true)
38+
{
39+
$this->hub = $hub;
40+
41+
parent::__construct($level, $bubble);
42+
}
43+
44+
/**
45+
* @psalm-suppress MoreSpecificImplementedParamType
46+
*
47+
* @param LogRecord|array{
48+
* level: int,
49+
* channel: string,
50+
* datetime: \DateTimeImmutable,
51+
* message: string,
52+
* extra?: array<string, mixed>
53+
* } $record {@see https://github.com/Seldaek/monolog/blob/main/doc/message-structure.md}
54+
*/
55+
protected function write($record): void
56+
{
57+
$breadcrumb = new Breadcrumb(
58+
$this->getBreadcrumbLevel($record['level']),
59+
$this->getBreadcrumbType($record['level']),
60+
$record['channel'],
61+
$record['message'],
62+
($record['context'] ?? []) + ($record['extra'] ?? []),
63+
$record['datetime']->getTimestamp()
64+
);
65+
66+
$this->hub->addBreadcrumb($breadcrumb);
67+
}
68+
69+
/**
70+
* @param Level|int $level
71+
*/
72+
private function getBreadcrumbLevel($level): string
73+
{
74+
if ($level instanceof Level) {
75+
$level = $level->value;
76+
}
77+
78+
switch ($level) {
79+
case Logger::DEBUG:
80+
return Breadcrumb::LEVEL_DEBUG;
81+
case Logger::INFO:
82+
case Logger::NOTICE:
83+
return Breadcrumb::LEVEL_INFO;
84+
case Logger::WARNING:
85+
return Breadcrumb::LEVEL_WARNING;
86+
case Logger::ERROR:
87+
return Breadcrumb::LEVEL_ERROR;
88+
default:
89+
return Breadcrumb::LEVEL_FATAL;
90+
}
91+
}
92+
93+
private function getBreadcrumbType(int $level): string
94+
{
95+
if ($level >= Logger::ERROR) {
96+
return Breadcrumb::TYPE_ERROR;
97+
}
98+
99+
return Breadcrumb::TYPE_DEFAULT;
100+
}
101+
}
+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Tests\Monolog;
6+
7+
use Monolog\Logger;
8+
use Monolog\LogRecord;
9+
use PHPUnit\Framework\TestCase;
10+
use Sentry\Breadcrumb;
11+
use Sentry\Monolog\BreadcrumbHandler;
12+
use Sentry\State\HubInterface;
13+
14+
final class BreadcrumbHandlerTest extends TestCase
15+
{
16+
/**
17+
* @dataProvider handleDataProvider
18+
*/
19+
public function testHandle($record, Breadcrumb $expectedBreadcrumb): void
20+
{
21+
$hub = $this->createMock(HubInterface::class);
22+
$hub->expects($this->once())
23+
->method('addBreadcrumb')
24+
->with($this->callback(function (Breadcrumb $breadcrumb) use ($expectedBreadcrumb, $record): bool {
25+
$this->assertSame($expectedBreadcrumb->getMessage(), $breadcrumb->getMessage());
26+
$this->assertSame($expectedBreadcrumb->getLevel(), $breadcrumb->getLevel());
27+
$this->assertSame($expectedBreadcrumb->getType(), $breadcrumb->getType());
28+
$this->assertEquals($record['datetime']->getTimestamp(), $breadcrumb->getTimestamp());
29+
$this->assertSame($expectedBreadcrumb->getCategory(), $breadcrumb->getCategory());
30+
$this->assertEquals($expectedBreadcrumb->getMetadata(), $breadcrumb->getMetadata());
31+
32+
return true;
33+
}));
34+
35+
$handler = new BreadcrumbHandler($hub);
36+
$handler->handle($record);
37+
}
38+
39+
/**
40+
* @return iterable<LogRecord|array{array<string, mixed>, Breadcrumb}>
41+
*/
42+
public function handleDataProvider(): iterable
43+
{
44+
$defaultBreadcrumb = new Breadcrumb(
45+
Breadcrumb::LEVEL_DEBUG,
46+
Breadcrumb::TYPE_DEFAULT,
47+
'channel.foo',
48+
'foo bar',
49+
[]
50+
);
51+
52+
$levelsToBeTested = [
53+
Logger::DEBUG => Breadcrumb::LEVEL_DEBUG,
54+
Logger::INFO => Breadcrumb::LEVEL_INFO,
55+
Logger::NOTICE => Breadcrumb::LEVEL_INFO,
56+
Logger::WARNING => Breadcrumb::LEVEL_WARNING,
57+
];
58+
59+
foreach ($levelsToBeTested as $loggerLevel => $breadcrumbLevel) {
60+
yield 'with level ' . Logger::getLevelName($loggerLevel) => [
61+
RecordFactory::create('foo bar', $loggerLevel, 'channel.foo', [], []),
62+
$defaultBreadcrumb->withLevel($breadcrumbLevel),
63+
];
64+
}
65+
66+
yield 'with level ERROR' => [
67+
RecordFactory::create('foo bar', Logger::ERROR, 'channel.foo', [], []),
68+
$defaultBreadcrumb->withLevel(Breadcrumb::LEVEL_ERROR)
69+
->withType(Breadcrumb::TYPE_ERROR),
70+
];
71+
72+
yield 'with level ALERT' => [
73+
RecordFactory::create('foo bar', Logger::ALERT, 'channel.foo', [], []),
74+
$defaultBreadcrumb->withLevel(Breadcrumb::LEVEL_FATAL)
75+
->withType(Breadcrumb::TYPE_ERROR),
76+
];
77+
78+
yield 'with context' => [
79+
RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', ['context' => ['foo' => 'bar']], []),
80+
$defaultBreadcrumb->withMetadata('context', ['foo' => 'bar']),
81+
];
82+
83+
yield 'with extra' => [
84+
RecordFactory::create('foo bar', Logger::DEBUG, 'channel.foo', [], ['extra' => ['foo' => 'bar']]),
85+
$defaultBreadcrumb->withMetadata('extra', ['foo' => 'bar']),
86+
];
87+
}
88+
}

tests/Monolog/RecordFactory.php

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public static function create(string $message, int $level, string $channel, arra
4040
'level_name' => Logger::getLevelName($level),
4141
'channel' => $channel,
4242
'extra' => $extra,
43+
'datetime' => new \DateTimeImmutable(),
4344
];
4445
}
4546
}

0 commit comments

Comments
 (0)