Skip to content

Commit f33d9ff

Browse files
cleptricstayallive
andauthored
Add Sentry logs (#1000)
Co-authored-by: Alex Bouma <[email protected]>
1 parent 7dda008 commit f33d9ff

File tree

8 files changed

+290
-20
lines changed

8 files changed

+290
-20
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"require": {
2626
"php": "^7.2 | ^8.0",
2727
"illuminate/support": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0",
28-
"sentry/sentry": "^4.10",
28+
"sentry/sentry": "^4.13",
2929
"symfony/psr-http-message-bridge": "^1.0 | ^2.0 | ^6.0 | ^7.0",
3030
"nyholm/psr7": "^1.0"
3131
},

src/Sentry/Laravel/Features/LogIntegration.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Support\Facades\Log;
66
use Sentry\Laravel\LogChannel;
7+
use Sentry\Laravel\Logs\LogChannel as LogsLogChannel;
78

89
class LogIntegration extends Feature
910
{
@@ -17,5 +18,9 @@ public function register(): void
1718
Log::extend('sentry', function ($app, array $config) {
1819
return (new LogChannel($app))($config);
1920
});
21+
22+
Log::extend('sentry_logs', function ($app, array $config) {
23+
return (new LogsLogChannel($app))($config);
24+
});
2025
}
2126
}

src/Sentry/Laravel/Integration.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Sentry\EventId;
99
use Sentry\ExceptionMechanism;
1010
use Sentry\Laravel\Integration\ModelViolations as ModelViolationReports;
11+
use Sentry\Logs\Logs;
1112
use Sentry\SentrySdk;
1213
use Sentry\Tracing\TransactionSource;
1314
use Throwable;
@@ -120,6 +121,8 @@ public static function flushEvents(): void
120121

121122
if ($client !== null) {
122123
$client->flush();
124+
125+
Logs::getInstance()->flush();
123126
}
124127
}
125128

src/Sentry/Laravel/LogChannel.php

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,6 @@
99

1010
class LogChannel extends LogManager
1111
{
12-
/**
13-
* @param array $config
14-
*
15-
* @return Logger
16-
*/
1712
public function __invoke(array $config = []): Logger
1813
{
1914
$handler = new SentryHandler(
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Logs;
4+
5+
use Monolog\Handler\FingersCrossedHandler;
6+
use Monolog\Logger;
7+
use Illuminate\Log\LogManager;
8+
9+
class LogChannel extends LogManager
10+
{
11+
public function __invoke(array $config = []): Logger
12+
{
13+
$handler = new LogsHandler(
14+
$config['level'] ?? Logger::DEBUG,
15+
$config['bubble'] ?? true
16+
);
17+
18+
if (isset($config['action_level'])) {
19+
$handler = new FingersCrossedHandler($handler, $config['action_level']);
20+
21+
// Consume the `action_level` config option since newer Laravel versions also support this option
22+
// and will wrap the handler again in another `FingersCrossedHandler` if we leave the option set
23+
// See: https://github.com/laravel/framework/pull/40305 (release v8.79.0)
24+
unset($config['action_level']);
25+
}
26+
27+
return new Logger(
28+
$this->parseChannel($config),
29+
[
30+
$this->prepareHandler($handler, $config),
31+
]
32+
);
33+
}
34+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Logs;
4+
5+
use Sentry\Logs\LogLevel;
6+
use Monolog\Formatter\LineFormatter;
7+
use Monolog\Formatter\FormatterInterface;
8+
use Monolog\Handler\AbstractProcessingHandler;
9+
use Sentry\Monolog\CompatibilityProcessingHandlerTrait;
10+
use Sentry\Severity;
11+
use Throwable;
12+
13+
class LogsHandler extends AbstractProcessingHandler
14+
{
15+
use CompatibilityProcessingHandlerTrait;
16+
17+
/**
18+
* @var FormatterInterface The formatter to use for the logs generated via handleBatch()
19+
*/
20+
protected $batchFormatter;
21+
22+
/**
23+
* {@inheritdoc}
24+
*/
25+
public function handleBatch(array $records): void
26+
{
27+
$level = $this->level;
28+
29+
// filter records based on their level
30+
$records = array_filter(
31+
$records,
32+
function ($record) use ($level) {
33+
return $record['level'] >= $level;
34+
}
35+
);
36+
37+
if (!$records) {
38+
return;
39+
}
40+
41+
// the record with the highest severity is the "main" one
42+
$record = array_reduce(
43+
$records,
44+
function ($highest, $record) {
45+
if ($highest === null || $record['level'] > $highest['level']) {
46+
return $record;
47+
}
48+
49+
return $highest;
50+
}
51+
);
52+
53+
// the other ones are added as a context item
54+
$logs = [];
55+
foreach ($records as $r) {
56+
$logs[] = $this->processRecord($r);
57+
}
58+
59+
if ($logs) {
60+
$record['context']['logs'] = (string)$this->getBatchFormatter()->formatBatch($logs);
61+
}
62+
63+
$this->handle($record);
64+
}
65+
66+
/**
67+
* Sets the formatter for the logs generated by handleBatch().
68+
*
69+
* @param FormatterInterface $formatter
70+
*
71+
* @return \Sentry\Laravel\SentryHandler
72+
*/
73+
public function setBatchFormatter(FormatterInterface $formatter): self
74+
{
75+
$this->batchFormatter = $formatter;
76+
77+
return $this;
78+
}
79+
80+
/**
81+
* Gets the formatter for the logs generated by handleBatch().
82+
*/
83+
public function getBatchFormatter(): FormatterInterface
84+
{
85+
if (!$this->batchFormatter) {
86+
$this->batchFormatter = new LineFormatter();
87+
}
88+
89+
return $this->batchFormatter;
90+
}
91+
92+
/**
93+
* {@inheritdoc}
94+
* @suppress PhanTypeMismatchArgument
95+
*/
96+
protected function doWrite($record): void
97+
{
98+
$exception = $record['context']['exception'] ?? null;
99+
100+
if ($exception instanceof Throwable) {
101+
return;
102+
}
103+
104+
\Sentry\logger()->aggregator()->add(
105+
// This seems a little bit of a roundabout way to get the log level, but this is done for compatibility
106+
self::getLogLevelFromSeverity(
107+
self::getSeverityFromLevel($record['level'])
108+
),
109+
$record['message'],
110+
[],
111+
array_merge($record['context'], $record['extra'])
112+
);
113+
}
114+
115+
private static function getLogLevelFromSeverity(Severity $severity): LogLevel
116+
{
117+
switch ($severity) {
118+
case Severity::debug():
119+
return LogLevel::debug();
120+
case Severity::warning():
121+
return LogLevel::warn();
122+
case Severity::error():
123+
return LogLevel::error();
124+
case Severity::fatal():
125+
return LogLevel::fatal();
126+
default:
127+
return LogLevel::info();
128+
}
129+
}
130+
}

test/Sentry/Features/ConsoleSchedulingIntegrationTest.php

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,6 @@ public function testScheduleMacroIsRegisteredWithoutDsnSet(): void
142142
$this->assertTrue(Event::hasMacro('sentryMonitor'));
143143
}
144144

145-
/** @define-env envSamplingAllTransactions */
146-
public function testScheduledCommandCreatesTransaction(): void
147-
{
148-
$this->getScheduler()->command('inspire')->everyMinute();
149-
150-
$this->artisan('schedule:run');
151-
152-
$this->assertSentryTransactionCount(1);
153-
154-
$transaction = $this->getLastSentryEvent();
155-
156-
$this->assertEquals('inspire', $transaction->getTransaction());
157-
}
158-
159145
/** @define-env envSamplingAllTransactions */
160146
public function testScheduledClosureCreatesTransaction(): void
161147
{
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
namespace Sentry\Features;
4+
5+
use Illuminate\Config\Repository;
6+
use Illuminate\Support\Facades\Log;
7+
use Sentry\Laravel\Tests\TestCase;
8+
use Sentry\Logs\LogLevel;
9+
use function Sentry\logger;
10+
11+
class LogLogsIntegrationTest extends TestCase
12+
{
13+
protected function defineEnvironment($app): void
14+
{
15+
parent::defineEnvironment($app);
16+
17+
tap($app['config'], static function (Repository $config) {
18+
$config->set('sentry.enable_logs', true);
19+
20+
$config->set('logging.channels.sentry_logs', [
21+
'driver' => 'sentry_logs',
22+
]);
23+
24+
$config->set('logging.channels.sentry_logs_error_level', [
25+
'driver' => 'sentry_logs',
26+
'level' => 'error',
27+
]);
28+
});
29+
}
30+
31+
public function testLogChannelIsRegistered(): void
32+
{
33+
$this->expectNotToPerformAssertions();
34+
35+
Log::channel('sentry_logs');
36+
}
37+
38+
/** @define-env envWithoutDsnSet */
39+
public function testLogChannelIsRegisteredWithoutDsn(): void
40+
{
41+
$this->expectNotToPerformAssertions();
42+
43+
Log::channel('sentry_logs');
44+
}
45+
46+
public function testLogChannelGeneratesLogs(): void
47+
{
48+
$logger = Log::channel('sentry_logs');
49+
50+
$logger->info('Sentry Laravel info log message');
51+
52+
$logs = $this->getAndFlushCapturedLogs();
53+
54+
$this->assertCount(1, $logs);
55+
56+
$log = $logs[0];
57+
58+
$this->assertEquals(LogLevel::info(), $log->getLevel());
59+
$this->assertEquals('Sentry Laravel info log message', $log->getBody());
60+
}
61+
62+
public function testLogChannelGeneratesLogsOnlyForConfiguredLevel(): void
63+
{
64+
$logger = Log::channel('sentry_logs_error_level');
65+
66+
$logger->info('Sentry Laravel info log message');
67+
$logger->warning('Sentry Laravel warning log message');
68+
$logger->error('Sentry Laravel error log message');
69+
70+
$logs = $this->getAndFlushCapturedLogs();
71+
72+
$this->assertCount(1, $logs);
73+
74+
$log = $logs[0];
75+
76+
$this->assertEquals(LogLevel::error(), $log->getLevel());
77+
$this->assertEquals('Sentry Laravel error log message', $log->getBody());
78+
}
79+
80+
public function testLogChannelDoesntCaptureExceptions(): void
81+
{
82+
$logger = Log::channel('sentry_logs');
83+
84+
$logger->error('Sentry Laravel error log message', ['exception' => new \Exception('Test exception')]);
85+
86+
$logs = $this->getAndFlushCapturedLogs();
87+
88+
$this->assertCount(0, $logs);
89+
}
90+
91+
public function testLogChannelAddsContextAsAttributes(): void
92+
{
93+
$logger = Log::channel('sentry_logs');
94+
95+
$logger->info('Sentry Laravel info log message', [
96+
'foo' => 'bar',
97+
]);
98+
99+
$logs = $this->getAndFlushCapturedLogs();
100+
101+
$this->assertCount(1, $logs);
102+
103+
$log = $logs[0];
104+
105+
$this->assertEquals('bar', $log->attributes()->get('foo')->getValue());
106+
}
107+
108+
/** @return \Sentry\Logs\Log[] */
109+
private function getAndFlushCapturedLogs(): array
110+
{
111+
$logs = logger()->aggregator()->all();
112+
113+
logger()->aggregator()->flush();
114+
115+
return $logs;
116+
}
117+
}

0 commit comments

Comments
 (0)