Skip to content

Commit 2e1317c

Browse files
committed
fix non conventions based SentryLog engine
1 parent 3579562 commit 2e1317c

3 files changed

Lines changed: 229 additions & 53 deletions

File tree

src/Log/Engine/SentryLog.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeSentry\Log\Engine;
5+
6+
use Cake\Event\EventManager;
7+
use Cake\Log\Engine\BaseLog;
8+
use Psr\Log\LogLevel;
9+
use Sentry\Logs\Logs;
10+
use Stringable;
11+
12+
class SentryLog extends BaseLog
13+
{
14+
public bool $logsWillBeFlushed = false;
15+
16+
/**
17+
* @param array $config
18+
*/
19+
public function __construct(array $config = [])
20+
{
21+
parent::__construct($config);
22+
23+
// Send the logs to sentry after the client has received the response
24+
if (PHP_SAPI !== 'cli' && function_exists('fastcgi_finish_request')) {
25+
$this->logsWillBeFlushed = true;
26+
EventManager::instance()->on('Server.terminate', function (): void {
27+
Logs::getInstance()->flush();
28+
});
29+
}
30+
}
31+
32+
/**
33+
* @param string $level
34+
* @param \Stringable|string $message
35+
* @param array $context
36+
* @return void
37+
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
38+
* @psalm-suppress MoreSpecificImplementedParamType
39+
*/
40+
public function log($level, string|Stringable $message, array $context = []): void
41+
{
42+
$message = $this->interpolate($message, $context);
43+
$message = $this->formatter->format($level, $message, $context);
44+
45+
$sentryLogger = Logs::getInstance();
46+
47+
match ($level) {
48+
LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL => $sentryLogger->fatal($message, [], $context),
49+
LogLevel::ERROR => $sentryLogger->error($message),
50+
LogLevel::WARNING => $sentryLogger->warn($message, [], $context),
51+
LogLevel::NOTICE, LogLevel::INFO => $sentryLogger->info($message, [], $context),
52+
LogLevel::DEBUG => $sentryLogger->debug($message, [], $context),
53+
default => $sentryLogger->trace($message, [], $context),
54+
};
55+
56+
if (!$this->logsWillBeFlushed) {
57+
$sentryLogger->flush();
58+
}
59+
}
60+
}
61+
62+
// phpcs:disable
63+
class_alias('CakeSentry\Log\Engine\SentryLog', 'CakeSentry\Log\Engines\SentryLog');
64+
// phpcs:enable

src/Log/Engines/SentryLog.php

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,10 @@
33

44
namespace CakeSentry\Log\Engines;
55

6-
use Cake\Event\EventManager;
7-
use Cake\Log\Engine\BaseLog;
8-
use Psr\Log\LogLevel;
9-
use Sentry\Logs\Logs;
10-
use Stringable;
6+
use CakeSentry\Log\Engine\SentryLog;
7+
use function Cake\Core\deprecationWarning;
118

12-
class SentryLog extends BaseLog
13-
{
14-
public bool $logsWillBeFlushed = false;
9+
$msg = 'Use `CakeSentry\Log\Engine\SentryLog` instead of `CakeSentry\Log\Engines\SentryLog`.';
10+
deprecationWarning('3.5.3', $msg);
1511

16-
/**
17-
* @param array $config
18-
*/
19-
public function __construct(array $config = [])
20-
{
21-
parent::__construct($config);
22-
23-
// Send the logs to sentry after the client has received the response
24-
if (PHP_SAPI !== 'cli' && function_exists('fastcgi_finish_request')) {
25-
$this->logsWillBeFlushed = true;
26-
EventManager::instance()->on('Server.terminate', function (): void {
27-
Logs::getInstance()->flush();
28-
});
29-
}
30-
}
31-
32-
/**
33-
* @param string $level
34-
* @param \Stringable|string $message
35-
* @param array $context
36-
* @return void
37-
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
38-
* @psalm-suppress MoreSpecificImplementedParamType
39-
*/
40-
public function log($level, string|Stringable $message, array $context = []): void
41-
{
42-
$message = $this->interpolate($message, $context);
43-
$message = $this->formatter->format($level, $message, $context);
44-
45-
$sentryLogger = Logs::getInstance();
46-
47-
match ($level) {
48-
LogLevel::EMERGENCY, LogLevel::ALERT, LogLevel::CRITICAL => $sentryLogger->fatal($message, [], $context),
49-
LogLevel::ERROR => $sentryLogger->error($message),
50-
LogLevel::WARNING => $sentryLogger->warn($message, [], $context),
51-
LogLevel::NOTICE, LogLevel::INFO => $sentryLogger->info($message, [], $context),
52-
LogLevel::DEBUG => $sentryLogger->debug($message, [], $context),
53-
default => $sentryLogger->trace($message, [], $context),
54-
};
55-
56-
if (!$this->logsWillBeFlushed) {
57-
$sentryLogger->flush();
58-
}
59-
}
60-
}
12+
class_exists(SentryLog::class);
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace CakeSentry\Test\TestCase\Log\Engine;
5+
6+
use Cake\Log\Formatter\DefaultFormatter;
7+
use Cake\TestSuite\TestCase;
8+
use CakeSentry\Log\Engine\SentryLog;
9+
use Mockery;
10+
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
11+
use PHPUnit\Framework\Attributes\DataProvider;
12+
use Psr\Log\LogLevel;
13+
use Sentry\ClientInterface;
14+
use Sentry\Event;
15+
use Sentry\Options;
16+
use Sentry\SentrySdk;
17+
use Sentry\State\Hub;
18+
19+
class SentryLogTest extends TestCase
20+
{
21+
use MockeryPHPUnitIntegration;
22+
23+
protected Hub $originalHub;
24+
25+
public function setUp(): void
26+
{
27+
parent::setUp();
28+
$this->skipIf(!method_exists('Sentry\Logs\Log', 'getPsrLevel'), 'Sentry SDK too low');
29+
30+
$this->originalHub = SentrySdk::getCurrentHub();
31+
}
32+
33+
public function tearDown(): void
34+
{
35+
SentrySdk::setCurrentHub($this->originalHub);
36+
37+
parent::tearDown();
38+
}
39+
40+
#[DataProvider('logLevelProvider')]
41+
public function testLogSendsFormattedLogsToSentry(
42+
string $level,
43+
string $expectedPsrLevel,
44+
bool $expectsContextAttributes,
45+
): void {
46+
$client = $this->createClientMock(function (Event $event) use ($level, $expectedPsrLevel, $expectsContextAttributes): void {
47+
$logs = $event->getLogs();
48+
49+
$this->assertCount(1, $logs);
50+
$this->assertSame(sprintf('%s: Message 42', $level), $logs[0]->getBody());
51+
$this->assertSame($expectedPsrLevel, $logs[0]->getPsrLevel());
52+
53+
$attributes = $logs[0]->attributes()->toSimpleArray();
54+
if ($expectsContextAttributes) {
55+
$this->assertSame(42, $attributes['userId']);
56+
$this->assertSame('test', $attributes['scope']);
57+
} else {
58+
$this->assertArrayNotHasKey('userId', $attributes);
59+
$this->assertArrayNotHasKey('scope', $attributes);
60+
}
61+
});
62+
63+
SentrySdk::setCurrentHub(new Hub($client));
64+
65+
$logger = new SentryLog([
66+
'formatter' => [
67+
'className' => DefaultFormatter::class,
68+
'includeDate' => false,
69+
],
70+
]);
71+
72+
$logger->log($level, 'Message {userId}', ['userId' => 42, 'scope' => 'test']);
73+
}
74+
75+
public static function logLevelProvider(): array
76+
{
77+
return [
78+
'warning' => [LogLevel::WARNING, LogLevel::WARNING, true],
79+
'error' => [LogLevel::ERROR, LogLevel::ERROR, false],
80+
'notice' => [LogLevel::NOTICE, LogLevel::INFO, true],
81+
'debug' => [LogLevel::DEBUG, LogLevel::DEBUG, true],
82+
'emergency' => [LogLevel::EMERGENCY, LogLevel::CRITICAL, true],
83+
'custom defaults to trace' => ['custom', LogLevel::DEBUG, true],
84+
];
85+
}
86+
87+
public function testLogSkipsImmediateFlushWhenDeferred(): void
88+
{
89+
$client = Mockery::mock(ClientInterface::class);
90+
$client->shouldReceive('getOptions')->andReturn(new Options([
91+
'dsn' => 'https://public@example.com/1',
92+
'enable_logs' => true,
93+
]));
94+
$client->shouldReceive('captureEvent')
95+
->once()
96+
->withArgs(function (Event $event): bool {
97+
$logs = $event->getLogs();
98+
99+
$this->assertCount(1, $logs);
100+
$this->assertSame('info: Deferred message', $logs[0]->getBody());
101+
$this->assertSame('test', $logs[0]->attributes()->toSimpleArray()['scope']);
102+
103+
return true;
104+
})
105+
->andReturnNull();
106+
107+
SentrySdk::setCurrentHub(new Hub($client));
108+
109+
$logger = new SentryLog([
110+
'formatter' => [
111+
'className' => DefaultFormatter::class,
112+
'includeDate' => false,
113+
],
114+
]);
115+
$logger->logsWillBeFlushed = true;
116+
117+
$logger->log(LogLevel::INFO, 'Deferred message', ['scope' => 'test']);
118+
119+
$logs = SentrySdk::getCurrentRuntimeContext()->getLogsAggregator()->all();
120+
$this->assertCount(1, $logs);
121+
$this->assertSame('info: Deferred message', $logs[0]->getBody());
122+
$this->assertSame('test', $logs[0]->attributes()->toSimpleArray()['scope']);
123+
124+
SentrySdk::getCurrentRuntimeContext()->getLogsAggregator()->flush();
125+
}
126+
127+
public function testLegacyNamespaceAliasesToNewClass(): void
128+
{
129+
$this->expectDeprecationMessageMatches(
130+
'/Use `CakeSentry\\\\Log\\\\Engine\\\\SentryLog` instead of `CakeSentry\\\\Log\\\\Engines\\\\SentryLog`\./',
131+
function (): void {
132+
require dirname(__DIR__, 4) . '/src/Log/Engines/SentryLog.php';
133+
},
134+
);
135+
136+
$legacyClass = 'CakeSentry\\Log\\Engines\\SentryLog';
137+
138+
$this->assertTrue(class_exists($legacyClass, false));
139+
$this->assertSame(SentryLog::class, get_class(new $legacyClass([])));
140+
}
141+
142+
protected function createClientMock(callable $captureEventAssertion): ClientInterface
143+
{
144+
$client = Mockery::mock(ClientInterface::class);
145+
$client->shouldReceive('getOptions')->andReturn(new Options([
146+
'dsn' => 'https://public@example.com/1',
147+
'enable_logs' => true,
148+
]));
149+
$client->shouldReceive('captureEvent')
150+
->once()
151+
->withArgs(function (Event $event) use ($captureEventAssertion): bool {
152+
$captureEventAssertion($event);
153+
154+
return true;
155+
})
156+
->andReturnNull();
157+
158+
return $client;
159+
}
160+
}

0 commit comments

Comments
 (0)