Skip to content

Commit aff42b5

Browse files
stayallivecleptric
andauthored
Add Monolog Sentry Logs handler (#1867)
Co-authored-by: Michi Hoffmann <[email protected]>
1 parent db21532 commit aff42b5

File tree

5 files changed

+433
-46
lines changed

5 files changed

+433
-46
lines changed

psalm-baseline.xml

Lines changed: 22 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,6 @@
88
<code>$parsedDsn['user']</code>
99
</PossiblyUndefinedArrayOffset>
1010
</file>
11-
<file src="src/HttpClient/HttpClientFactory.php">
12-
<UndefinedClass occurrences="5">
13-
<code>Guzzle6HttpClient</code>
14-
<code>GuzzleHttpClientOptions</code>
15-
<code>GuzzleHttpClientOptions</code>
16-
<code>GuzzleHttpClientOptions</code>
17-
<code>SymfonyHttpClient</code>
18-
</UndefinedClass>
19-
</file>
2011
<file src="src/Integration/IntegrationRegistry.php">
2112
<PossiblyInvalidArgument occurrences="2">
2213
<code>$userIntegration</code>
@@ -38,6 +29,14 @@
3829
<code>int|string|Level|LogLevel::*</code>
3930
</UndefinedDocblockClass>
4031
</file>
32+
<file src="src/Monolog/CompatibilityLogLevelTrait.php">
33+
<DuplicateClass occurrences="1">
34+
<code>CompatibilityLogLevelTrait</code>
35+
</DuplicateClass>
36+
<UndefinedClass occurrences="1">
37+
<code>Level</code>
38+
</UndefinedClass>
39+
</file>
4140
<file src="src/Monolog/CompatibilityProcessingHandlerTrait.php">
4241
<DuplicateClass occurrences="1">
4342
<code>CompatibilityProcessingHandlerTrait</code>
@@ -60,6 +59,20 @@
6059
<code>$record['context']</code>
6160
</PossiblyUndefinedMethod>
6261
</file>
62+
<file src="src/Monolog/LogsHandler.php">
63+
<PossiblyInvalidArgument occurrences="3">
64+
<code>$record['level']</code>
65+
<code>$record['level']</code>
66+
<code>$record['message']</code>
67+
</PossiblyInvalidArgument>
68+
<PossiblyInvalidArrayOffset occurrences="1">
69+
<code>$record['context']['exception']</code>
70+
</PossiblyInvalidArrayOffset>
71+
<PossiblyUndefinedMethod occurrences="2">
72+
<code>$record['context']</code>
73+
<code>$record['context']</code>
74+
</PossiblyUndefinedMethod>
75+
</file>
6376
<file src="src/Profiling/Profile.php">
6477
<LessSpecificReturnStatement occurrences="1"/>
6578
<MoreSpecificReturnType occurrences="1">
@@ -79,37 +92,9 @@
7992
<code>representationSerialize</code>
8093
</InvalidReturnType>
8194
</file>
82-
<file src="src/State/Hub.php">
83-
<TooManyArguments occurrences="3">
84-
<code>captureException</code>
85-
<code>captureLastError</code>
86-
<code>captureMessage</code>
87-
</TooManyArguments>
88-
</file>
89-
<file src="src/State/HubAdapter.php">
90-
<TooManyArguments occurrences="4">
91-
<code>captureException</code>
92-
<code>captureLastError</code>
93-
<code>captureMessage</code>
94-
<code>startTransaction</code>
95-
</TooManyArguments>
96-
</file>
97-
<file src="src/Tracing/SpanContext.php">
98-
<UnsafeInstantiation occurrences="1">
99-
<code>new static()</code>
100-
</UnsafeInstantiation>
101-
</file>
10295
<file src="src/Tracing/Transaction.php">
10396
<NonInvariantDocblockPropertyType occurrences="1">
10497
<code>$transaction</code>
10598
</NonInvariantDocblockPropertyType>
10699
</file>
107-
<file src="src/functions.php">
108-
<TooManyArguments occurrences="4">
109-
<code>captureException</code>
110-
<code>captureLastError</code>
111-
<code>captureMessage</code>
112-
<code>startTransaction</code>
113-
</TooManyArguments>
114-
</file>
115100
</files>

src/Logs/LogLevel.php

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,66 @@ class LogLevel
1414
*/
1515
private $value;
1616

17+
/**
18+
* @var int The priority of the log level, used for sorting
19+
*/
20+
private $priority;
21+
1722
/**
1823
* @var array<string, self> A list of cached enum instances
1924
*/
2025
private static $instances = [];
2126

22-
private function __construct(string $value)
27+
private function __construct(string $value, int $priority)
2328
{
2429
$this->value = $value;
30+
$this->priority = $priority;
2531
}
2632

2733
public static function trace(): self
2834
{
29-
return self::getInstance('trace');
35+
return self::getInstance('trace', 10);
3036
}
3137

3238
public static function debug(): self
3339
{
34-
return self::getInstance('debug');
40+
return self::getInstance('debug', 20);
3541
}
3642

3743
public static function info(): self
3844
{
39-
return self::getInstance('info');
45+
return self::getInstance('info', 30);
4046
}
4147

4248
public static function warn(): self
4349
{
44-
return self::getInstance('warn');
50+
return self::getInstance('warn', 40);
4551
}
4652

4753
public static function error(): self
4854
{
49-
return self::getInstance('error');
55+
return self::getInstance('error', 50);
5056
}
5157

5258
public static function fatal(): self
5359
{
54-
return self::getInstance('fatal');
60+
return self::getInstance('fatal', 60);
5561
}
5662

5763
public function __toString(): string
5864
{
5965
return $this->value;
6066
}
6167

62-
private static function getInstance(string $value): self
68+
public function getPriority(): int
69+
{
70+
return $this->priority;
71+
}
72+
73+
private static function getInstance(string $value, int $priority): self
6374
{
6475
if (!isset(self::$instances[$value])) {
65-
self::$instances[$value] = new self($value);
76+
self::$instances[$value] = new self($value, $priority);
6677
}
6778

6879
return self::$instances[$value];
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Monolog;
6+
7+
use Monolog\Level;
8+
use Monolog\Logger;
9+
use Sentry\Logs\LogLevel;
10+
11+
if (Logger::API >= 3) {
12+
/**
13+
* Logic which is used if monolog >= 3 is installed.
14+
*
15+
* @internal
16+
*/
17+
trait CompatibilityLogLevelTrait
18+
{
19+
/**
20+
* Translates the Monolog level into the Sentry LogLevel.
21+
*/
22+
private static function getSentryLogLevelFromMonologLevel(int $level): LogLevel
23+
{
24+
$level = Level::from($level);
25+
26+
switch ($level) {
27+
case Level::Debug:
28+
return LogLevel::debug();
29+
case Level::Warning:
30+
return LogLevel::warn();
31+
case Level::Error:
32+
return LogLevel::error();
33+
case Level::Critical:
34+
case Level::Alert:
35+
case Level::Emergency:
36+
return LogLevel::fatal();
37+
case Level::Info:
38+
case Level::Notice:
39+
default:
40+
return LogLevel::info();
41+
}
42+
}
43+
}
44+
} else {
45+
/**
46+
* Logic which is used if monolog < 3 is installed.
47+
*
48+
* @internal
49+
*/
50+
trait CompatibilityLogLevelTrait
51+
{
52+
/**
53+
* Translates the Monolog level into the Sentry LogLevel.
54+
*
55+
* @param Logger::DEBUG|Logger::INFO|Logger::NOTICE|Logger::WARNING|Logger::ERROR|Logger::CRITICAL|Logger::ALERT|Logger::EMERGENCY $level The Monolog log level
56+
*/
57+
private static function getSentryLogLevelFromMonologLevel(int $level): LogLevel
58+
{
59+
switch ($level) {
60+
case Logger::DEBUG:
61+
return LogLevel::debug();
62+
case Logger::WARNING:
63+
return LogLevel::warn();
64+
case Logger::ERROR:
65+
return LogLevel::error();
66+
case Logger::CRITICAL:
67+
case Logger::ALERT:
68+
case Logger::EMERGENCY:
69+
return LogLevel::fatal();
70+
case Logger::INFO:
71+
case Logger::NOTICE:
72+
default:
73+
return LogLevel::info();
74+
}
75+
}
76+
}
77+
}

src/Monolog/LogsHandler.php

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\Monolog;
6+
7+
use Monolog\Formatter\FormatterInterface;
8+
use Monolog\Formatter\LineFormatter;
9+
use Monolog\Handler\HandlerInterface;
10+
use Monolog\LogRecord;
11+
use Sentry\Logs\LogLevel;
12+
use Sentry\Logs\Logs;
13+
14+
class LogsHandler implements HandlerInterface
15+
{
16+
use CompatibilityLogLevelTrait;
17+
18+
/**
19+
* The minimum logging level at which this handler will be triggered.
20+
*
21+
* @var LogLevel
22+
*/
23+
private $logLevel;
24+
25+
/**
26+
* Whether the messages that are handled can bubble up the stack or not.
27+
*
28+
* @var bool
29+
*/
30+
private $bubble;
31+
32+
/**
33+
* Creates a new Monolog handler that converts Monolog logs to Sentry logs.
34+
*
35+
* @param LogLevel|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs
36+
* @param bool $bubble whether the messages that are handled can bubble up the stack or not
37+
*/
38+
public function __construct(?LogLevel $logLevel = null, bool $bubble = true)
39+
{
40+
$this->logLevel = $logLevel ?? LogLevel::debug();
41+
$this->bubble = $bubble;
42+
}
43+
44+
/**
45+
* @param array<string, mixed>|LogRecord $record
46+
*/
47+
public function isHandling($record): bool
48+
{
49+
return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority();
50+
}
51+
52+
/**
53+
* @param array<string, mixed>|LogRecord $record
54+
*/
55+
public function handle($record): bool
56+
{
57+
// Do not collect logs for exceptions, they should be handled seperately by the `Handler` or `captureException`
58+
if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Throwable) {
59+
return false;
60+
}
61+
62+
Logs::getInstance()->aggregator()->add(
63+
self::getSentryLogLevelFromMonologLevel($record['level']),
64+
$record['message'],
65+
[],
66+
array_merge($record['context'], $record['extra'])
67+
);
68+
69+
return $this->bubble === false;
70+
}
71+
72+
/**
73+
* @param array<array<string, mixed>|LogRecord> $records
74+
*/
75+
public function handleBatch(array $records): void
76+
{
77+
foreach ($records as $record) {
78+
$this->handle($record);
79+
}
80+
}
81+
82+
public function close(): void
83+
{
84+
Logs::getInstance()->flush();
85+
}
86+
87+
/**
88+
* @param callable $callback
89+
*/
90+
public function pushProcessor($callback): void
91+
{
92+
// noop, this handler does not support processors
93+
}
94+
95+
/**
96+
* @return callable
97+
*/
98+
public function popProcessor()
99+
{
100+
// Since we do not support processors, we throw an exception if this method is called
101+
throw new \LogicException('You tried to pop from an empty processor stack.');
102+
}
103+
104+
public function setFormatter(FormatterInterface $formatter): void
105+
{
106+
// noop, this handler does not support formatters
107+
}
108+
109+
public function getFormatter(): FormatterInterface
110+
{
111+
// To adhere to the interface we need to return a formatter so we return a default one
112+
return new LineFormatter();
113+
}
114+
}

0 commit comments

Comments
 (0)