Skip to content

Commit a90dc6a

Browse files
authored
Cache performance spans & breadcrumbs (#656)
1 parent a36d9da commit a90dc6a

File tree

8 files changed

+236
-17
lines changed

8 files changed

+236
-17
lines changed

config/sentry.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
// Capture Laravel logs in breadcrumbs
1616
'logs' => true,
1717

18+
// Capture Laravel cache events in breadcrumbs
19+
'cache' => true,
20+
1821
// Capture Livewire components in breadcrumbs
1922
'livewire' => true,
2023

@@ -56,6 +59,12 @@
5659
// Capture HTTP client requests as spans
5760
'http_client_requests' => true,
5861

62+
// Capture Redis operations as spans (this enables Redis events in Laravel)
63+
'redis_commands' => env('SENTRY_TRACE_REDIS_COMMANDS', false),
64+
65+
// Try to find out where the Redis command originated from and add it to the command spans
66+
'redis_origin' => true,
67+
5968
// Indicates if the tracing integrations supplied by Sentry should be loaded
6069
'default_integrations' => true,
6170

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Features;
4+
5+
use Illuminate\Cache\Events;
6+
use Illuminate\Contracts\Events\Dispatcher;
7+
use Illuminate\Redis\Events as RedisEvents;
8+
use Illuminate\Redis\RedisManager;
9+
use Sentry\Breadcrumb;
10+
use Sentry\Laravel\Features\Concerns\ResolvesEventOrigin;
11+
use Sentry\Laravel\Integration;
12+
use Sentry\SentrySdk;
13+
use Sentry\Tracing\SpanContext;
14+
15+
class CacheIntegration extends Feature
16+
{
17+
use ResolvesEventOrigin;
18+
19+
public function isApplicable(): bool
20+
{
21+
return $this->isTracingFeatureEnabled('redis_commands') || $this->isBreadcrumbFeatureEnabled('cache');
22+
}
23+
24+
public function setup(Dispatcher $events): void
25+
{
26+
if ($this->isBreadcrumbFeatureEnabled('cache')) {
27+
$events->listen([
28+
Events\CacheHit::class,
29+
Events\CacheMissed::class,
30+
Events\KeyWritten::class,
31+
Events\KeyForgotten::class,
32+
], [$this, 'handleCacheEvent']);
33+
}
34+
35+
if ($this->isTracingFeatureEnabled('redis_commands', false)) {
36+
$events->listen(RedisEvents\CommandExecuted::class, [$this, 'handleRedisCommand']);
37+
38+
$this->container()->afterResolving(RedisManager::class, static function (RedisManager $redis): void {
39+
$redis->enableEvents();
40+
});
41+
}
42+
}
43+
44+
public function handleCacheEvent(Events\CacheEvent $event): void
45+
{
46+
switch (true) {
47+
case $event instanceof Events\KeyWritten:
48+
$message = 'Written';
49+
break;
50+
case $event instanceof Events\KeyForgotten:
51+
$message = 'Forgotten';
52+
break;
53+
case $event instanceof Events\CacheMissed:
54+
$message = 'Missed';
55+
break;
56+
case $event instanceof Events\CacheHit:
57+
$message = 'Read';
58+
break;
59+
default:
60+
// In case events are added in the future we do nothing when an unknown event is encountered
61+
return;
62+
}
63+
64+
Integration::addBreadcrumb(new Breadcrumb(
65+
Breadcrumb::LEVEL_INFO,
66+
Breadcrumb::TYPE_DEFAULT,
67+
'cache',
68+
"{$message}: {$event->key}",
69+
$event->tags ? ['tags' => $event->tags] : []
70+
));
71+
}
72+
73+
public function handleRedisCommand(RedisEvents\CommandExecuted $event): void
74+
{
75+
$parentSpan = SentrySdk::getCurrentHub()->getSpan();
76+
77+
// If there is no tracing span active there is no need to handle the event
78+
if ($parentSpan === null) {
79+
return;
80+
}
81+
82+
$context = new SpanContext();
83+
$context->setOp('db.redis');
84+
$context->setDescription(strtoupper($event->command) . ' ' . ($event->parameters[0] ?? null));
85+
$context->setStartTimestamp(microtime(true) - $event->time / 1000);
86+
$context->setEndTimestamp($context->getStartTimestamp() + $event->time / 1000);
87+
88+
$data = [
89+
'db.redis.connection' => $event->connectionName,
90+
];
91+
92+
if ($this->shouldSendDefaultPii()) {
93+
$data['db.redis.parameters'] = $event->parameters;
94+
}
95+
96+
if ($this->isTracingFeatureEnabled('redis_origin')) {
97+
$commandOrigin = $this->resolveEventOrigin();
98+
99+
if ($commandOrigin !== null) {
100+
$data['db.redis.origin'] = $commandOrigin;
101+
}
102+
}
103+
104+
$context->setData($data);
105+
106+
$parentSpan->startChild($context);
107+
}
108+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Features\Concerns;
4+
5+
use Sentry\Laravel\Tracing\BacktraceHelper;
6+
7+
trait ResolvesEventOrigin
8+
{
9+
protected function resolveEventOrigin(): ?string
10+
{
11+
$backtraceHelper = $this->makeBacktraceHelper();
12+
13+
$firstAppFrame = $backtraceHelper->findFirstInAppFrameForBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
14+
15+
if ($firstAppFrame === null) {
16+
return null;
17+
}
18+
19+
$filePath = $backtraceHelper->getOriginalViewPathForFrameOfCompiledViewPath($firstAppFrame) ?? $firstAppFrame->getFile();
20+
21+
return "{$filePath}:{$firstAppFrame->getLine()}";
22+
}
23+
24+
private function makeBacktraceHelper(): BacktraceHelper
25+
{
26+
return $this->container()->make(BacktraceHelper::class);
27+
}
28+
}

src/Sentry/Laravel/Features/Feature.php

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@
55
use Illuminate\Contracts\Container\Container;
66
use Sentry\Integration\IntegrationInterface;
77
use Sentry\Laravel\BaseServiceProvider;
8+
use Sentry\SentrySdk;
89

910
/**
1011
* @method void setup() Setup the feature in the environment.
1112
*
1213
* @internal
1314
*/
14-
abstract class Feature implements IntegrationInterface
15+
abstract class Feature
1516
{
1617
/**
1718
* @var Container The Laravel application container.
@@ -48,9 +49,9 @@ public function __construct(Container $container)
4849
abstract public function isApplicable(): bool;
4950

5051
/**
51-
* Initializes the current integration by registering it once.
52+
* Initializes the feature.
5253
*/
53-
public function setupOnce(): void
54+
public function boot(): void
5455
{
5556
if (method_exists($this, 'setup') && $this->isApplicable()) {
5657
try {
@@ -83,6 +84,20 @@ protected function getUserConfig(): array
8384
return empty($config) ? [] : $config;
8485
}
8586

87+
/**
88+
* Should default PII be sent by default.
89+
*/
90+
protected function shouldSendDefaultPii(): bool
91+
{
92+
$client = SentrySdk::getCurrentHub()->getClient();
93+
94+
if ($client === null) {
95+
return false;
96+
}
97+
98+
return $client->getOptions()->shouldSendDefaultPii();
99+
}
100+
86101
/**
87102
* Indicates if the given feature is enabled for tracing.
88103
*/

src/Sentry/Laravel/ServiceProvider.php

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,10 @@ class ServiceProvider extends BaseServiceProvider
4141
];
4242

4343
/**
44-
* List of default feature integrations that are enabled by default.
44+
* List of features that are provided by the SDK.
4545
*/
46-
protected const DEFAULT_FEATURES = [
46+
protected const FEATURES = [
47+
Features\CacheIntegration::class,
4748
Features\LivewirePackageIntegration::class,
4849
];
4950

@@ -57,6 +58,8 @@ public function boot(): void
5758
if ($this->hasDsnSet()) {
5859
$this->bindEvents();
5960

61+
$this->setupFeatures();
62+
6063
if ($this->app->bound(HttpKernelInterface::class)) {
6164
/** @var \Illuminate\Foundation\Http\Kernel $httpKernel */
6265
$httpKernel = $this->app->make(HttpKernelInterface::class);
@@ -126,6 +129,20 @@ protected function bindEvents(): void
126129
}
127130
}
128131

132+
/**
133+
* Setup the default SDK features.
134+
*/
135+
protected function setupFeatures(): void
136+
{
137+
foreach (self::FEATURES as $feature) {
138+
try {
139+
$this->app->make($feature)->boot();
140+
} catch (\Throwable $e) {
141+
// Ensure that features do not break the whole application
142+
}
143+
}
144+
}
145+
129146
/**
130147
* Register the artisan commands.
131148
*/
@@ -241,19 +258,12 @@ private function resolveIntegrationsFromUserConfig(): array
241258

242259
$userConfig = $this->getUserConfig();
243260

244-
$integrationsToResolve = array_merge(
245-
$userConfig['integrations'] ?? [],
246-
// These features are enabled by default and can be configured using the `tracing` and `breadcrumbs` config
247-
self::DEFAULT_FEATURES
248-
);
261+
$integrationsToResolve = array_merge($userConfig['integrations'] ?? []);
249262

250263
$enableDefaultTracingIntegrations = $userConfig['tracing']['default_integrations'] ?? true;
251264

252265
if ($enableDefaultTracingIntegrations) {
253-
$integrationsToResolve = array_merge(
254-
$integrationsToResolve,
255-
TracingServiceProvider::DEFAULT_INTEGRATIONS
256-
);
266+
$integrationsToResolve = array_merge($integrationsToResolve, TracingServiceProvider::DEFAULT_INTEGRATIONS);
257267
}
258268

259269
foreach ($integrationsToResolve as $userIntegration) {

src/Sentry/Laravel/Tracing/EventHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ protected function queryExecutedHandler(DatabaseEvents\QueryExecuted $query): vo
233233
$queryOrigin = $this->resolveQueryOriginFromBacktrace();
234234

235235
if ($queryOrigin !== null) {
236-
$context->setData(['sql.origin' => $queryOrigin]);
236+
$context->setData(['db.sql.origin' => $queryOrigin]);
237237
}
238238
}
239239

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace Sentry\Features;
4+
5+
use Illuminate\Support\Facades\Cache;
6+
use Sentry\Laravel\Tests\TestCase;
7+
8+
class CacheIntegrationTest extends TestCase
9+
{
10+
public function testCacheBreadcrumbForWriteAndHitIsRecorded(): void
11+
{
12+
Cache::put($key = 'foo', 'bar');
13+
14+
$this->assertEquals("Written: {$key}", $this->getLastBreadcrumb()->getMessage());
15+
16+
Cache::get('foo');
17+
18+
$this->assertEquals("Read: {$key}", $this->getLastBreadcrumb()->getMessage());
19+
}
20+
21+
public function testCacheBreadcrumbForWriteAndForgetIsRecorded(): void
22+
{
23+
Cache::put($key = 'foo', 'bar');
24+
25+
$this->assertEquals("Written: {$key}", $this->getLastBreadcrumb()->getMessage());
26+
27+
Cache::forget($key);
28+
29+
$this->assertEquals("Forgotten: {$key}", $this->getLastBreadcrumb()->getMessage());
30+
}
31+
32+
public function testCacheBreadcrumbForMissIsRecorded(): void
33+
{
34+
Cache::get($key = 'foo');
35+
36+
$this->assertEquals("Missed: {$key}", $this->getLastBreadcrumb()->getMessage());
37+
}
38+
39+
public function testCacheBreadcrumIsNotRecordedWhenDisabled(): void
40+
{
41+
$this->resetApplicationWithConfig([
42+
'sentry.breadcrumbs.cache' => false,
43+
]);
44+
45+
$this->assertFalse($this->app['config']->get('sentry.breadcrumbs.cache'));
46+
47+
Cache::get('foo');
48+
49+
$this->assertEmpty($this->getCurrentBreadcrumbs());
50+
}
51+
}

test/Sentry/TestCase.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,12 @@
88
use Sentry\ClientInterface;
99
use Sentry\Event;
1010
use Sentry\EventHint;
11-
use Sentry\Laravel\Integration;
1211
use Sentry\State\Scope;
1312
use ReflectionProperty;
1413
use Sentry\Laravel\Tracing;
1514
use Sentry\State\HubInterface;
1615
use Sentry\Laravel\ServiceProvider;
1716
use Orchestra\Testbench\TestCase as LaravelTestCase;
18-
use Throwable;
1917

2018
abstract class TestCase extends LaravelTestCase
2119
{

0 commit comments

Comments
 (0)