Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/Monolog/LogsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class LogsHandler implements HandlerInterface
/**
* The minimum logging level at which this handler will be triggered.
*
* @var LogLevel
* @psalm-suppress UndefinedDocblockClass
*
* @var LogLevel|\Monolog\Level|int
*/
private $logLevel;

Expand All @@ -32,21 +34,32 @@ class LogsHandler implements HandlerInterface
/**
* Creates a new Monolog handler that converts Monolog logs to Sentry logs.
*
* @param LogLevel|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs
* @param bool $bubble whether the messages that are handled can bubble up the stack or not
* @psalm-suppress UndefinedDocblockClass
*
* @param LogLevel|\Monolog\Level|int|null $logLevel the minimum logging level at which this handler will be triggered and collects the logs
* @param bool $bubble whether the messages that are handled can bubble up the stack or not
*/
public function __construct(?LogLevel $logLevel = null, bool $bubble = true)
public function __construct($logLevel = null, bool $bubble = true)
{
$this->logLevel = $logLevel ?? LogLevel::debug();
$this->bubble = $bubble;
}

/**
* @psalm-suppress UndefinedDocblockClass
* @psalm-suppress UndefinedClass
*
* @param array<string, mixed>|LogRecord $record
*/
public function isHandling($record): bool
{
return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority();
if ($this->logLevel instanceof LogLevel) {
return self::getSentryLogLevelFromMonologLevel($record['level'])->getPriority() >= $this->logLevel->getPriority();
} elseif ($this->logLevel instanceof \Monolog\Level) {
return $record['level'] >= $this->logLevel->value;
} else {
return $record['level'] >= $this->logLevel;
}
}

/**
Expand Down
229 changes: 138 additions & 91 deletions tests/Monolog/LogsHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@

namespace Sentry\Tests\Monolog;

use Monolog\Level;
use Monolog\Logger;
use PHPUnit\Framework\TestCase;
use Sentry\ClientBuilder;
use Sentry\Event;
use Sentry\Logs\Log;
use Sentry\Logs\LogLevel;
use Sentry\Logs\Logs;
use Sentry\Monolog\LogsHandler;
use Sentry\SentrySdk;
use Sentry\State\Hub;
use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;
use Sentry\Transport\TransportInterface;
use Sentry\Tests\StubTransport;

final class LogsHandlerTest extends TestCase
{
Expand Down Expand Up @@ -64,42 +62,47 @@ static function (string $key) {
}

/**
* @dataProvider logLevelDataProvider
* @dataProvider monologLegacyLevelDataProvider
*/
public function testLogLevels($record, int $countLogs): void
public function testFiltersAndMapsUsingLegacyMonologThreshold(int $threshold, int $recordLevel, int $expectedCount, ?LogLevel $expectedMappedLevel): void
{
$handler = new LogsHandler(LogLevel::warn());
$handler->handle($record);
$handler = new LogsHandler($threshold);
$handler->handle(RecordFactory::create('foo bar', $recordLevel, 'channel.foo', [], []));

$logs = Logs::getInstance()->aggregator()->all();
$this->assertCount($countLogs, $logs);
$this->assertCount($expectedCount, $logs);

if ($expectedMappedLevel !== null) {
$this->assertEquals($expectedMappedLevel, $logs[0]->getLevel());
}
}

/**
* @dataProvider monologLevelDataProvider
*/
public function testFiltersAndMapsUsingMonologEnumThreshold($threshold, $recordLevel, int $expectedCount, ?LogLevel $expectedMappedLevel): void
{
if (!class_exists(Level::class)) {
$this->markTestSkipped('Test only works for Monolog >= 3');
}

$this->assertInstanceOf(Level::class, $threshold);
$this->assertInstanceOf(Level::class, $recordLevel);

$handler = new LogsHandler($threshold);
$handler->handle(RecordFactory::create('foo bar', $recordLevel->value, 'channel.foo', [], []));

$logs = Logs::getInstance()->aggregator()->all();
$this->assertCount($expectedCount, $logs);

if ($expectedMappedLevel !== null) {
$this->assertEquals($expectedMappedLevel, $logs[0]->getLevel());
}
}

public function testLogsHandlerDestructor()
{
$transport = new class implements TransportInterface {
private $events = [];

public function send(Event $event): Result
{
$this->events[] = $event;

return new Result(ResultStatus::success());
}

public function close(?int $timeout = null): Result
{
return new Result(ResultStatus::success());
}

/**
* @return Event[]
*/
public function getEvents(): array
{
return $this->events;
}
};
$transport = new StubTransport();
$client = ClientBuilder::create([
'enable_logs' => true,
])->setTransport($transport)
Expand All @@ -110,8 +113,8 @@ public function getEvents(): array

$this->handleLogAndDrop();

$this->assertCount(1, $transport->getEvents());
$this->assertSame('I was dropped :(', $transport->getEvents()[0]->getLogs()[0]->getBody());
$this->assertCount(1, StubTransport::$events);
$this->assertSame('I was dropped :(', StubTransport::$events[0]->getLogs()[0]->getBody());
}

private function handleLogAndDrop(): void
Expand Down Expand Up @@ -283,83 +286,127 @@ public static function handleDataProvider(): iterable
];
}

public static function logLevelDataProvider(): iterable
public static function monologLegacyLevelDataProvider(): iterable
{
yield [
RecordFactory::create(
'foo bar',
Logger::DEBUG,
'channel.foo',
[],
[]
),
yield 'NOTICE threshold drops INFO (both map to sentry info)' => [
Logger::NOTICE,
Logger::INFO,
0,
null,
];

yield [
RecordFactory::create(
'foo bar',
Logger::NOTICE,
'channel.foo',
[],
[]
),
yield 'NOTICE threshold keeps NOTICE (mapped to sentry info)' => [
Logger::NOTICE,
Logger::NOTICE,
1,
LogLevel::info(),
];

yield 'NOTICE threshold keeps WARNING (mapped to sentry warn)' => [
Logger::NOTICE,
Logger::WARNING,
1,
LogLevel::warn(),
];

yield 'ALERT threshold drops CRITICAL (both map to sentry fatal)' => [
Logger::ALERT,
Logger::CRITICAL,
0,
null,
];

yield [
RecordFactory::create(
'foo bar',
Logger::INFO,
'channel.foo',
[],
[]
),
yield 'ALERT threshold keeps ALERT (mapped to sentry fatal)' => [
Logger::ALERT,
Logger::ALERT,
1,
LogLevel::fatal(),
];

yield 'ALERT threshold keeps EMERGENCY (mapped to sentry fatal)' => [
Logger::ALERT,
Logger::EMERGENCY,
1,
LogLevel::fatal(),
];

yield 'EMERGENCY threshold drops ALERT (both map to sentry fatal)' => [
Logger::EMERGENCY,
Logger::ALERT,
0,
null,
];

yield [
RecordFactory::create(
'foo bar',
Logger::WARNING,
'channel.foo',
[],
[]
),
yield 'EMERGENCY threshold keeps EMERGENCY (mapped to sentry fatal)' => [
Logger::EMERGENCY,
Logger::EMERGENCY,
1,
LogLevel::fatal(),
];
}

yield [
RecordFactory::create(
'foo bar',
Logger::CRITICAL,
'channel.foo',
[],
[]
),
public static function monologLevelDataProvider(): iterable
{
if (!class_exists(Level::class)) {
yield 'Monolog < 3 (skipped)' => [null, null, 0, null];

return;
}

yield 'NOTICE threshold drops INFO (both map to sentry info)' => [
Level::Notice,
Level::Info,
0,
null,
];

yield 'NOTICE threshold keeps NOTICE (mapped to sentry info)' => [
Level::Notice,
Level::Notice,
1,
LogLevel::info(),
];

yield [
RecordFactory::create(
'foo bar',
Logger::ALERT,
'channel.foo',
[],
[]
),
yield 'NOTICE threshold keeps WARNING (mapped to sentry warn)' => [
Level::Notice,
Level::Warning,
1,
LogLevel::warn(),
];

yield [
RecordFactory::create(
'foo bar',
Logger::EMERGENCY,
'channel.foo',
[],
[]
),
yield 'ALERT threshold drops CRITICAL (both map to sentry fatal)' => [
Level::Alert,
Level::Critical,
0,
null,
];

yield 'ALERT threshold keeps ALERT (mapped to sentry fatal)' => [
Level::Alert,
Level::Alert,
1,
LogLevel::fatal(),
];

yield 'ALERT threshold keeps EMERGENCY (mapped to sentry fatal)' => [
Level::Alert,
Level::Emergency,
1,
LogLevel::fatal(),
];

yield 'EMERGENCY threshold drops ALERT (both map to sentry fatal)' => [
Level::Emergency,
Level::Alert,
0,
null,
];

yield 'EMERGENCY threshold keeps EMERGENCY (mapped to sentry fatal)' => [
Level::Emergency,
Level::Emergency,
1,
LogLevel::fatal(),
];
}
}
2 changes: 1 addition & 1 deletion tests/Monolog/RecordFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ final class RecordFactory
*
* @return array<string, mixed>|LogRecord
*/
public static function create(string $message, int $level, string $channel, array $context, array $extra)
public static function create(string $message, int $level, string $channel, array $context = [], array $extra = [])
{
if (Logger::API >= 3) {
return new LogRecord(
Expand Down