Skip to content

Commit bbbda4c

Browse files
authored
Add sql.origin to SQL query spans (#398)
1 parent 4113eaa commit bbbda4c

File tree

6 files changed

+181
-6
lines changed

6 files changed

+181
-6
lines changed

config/sentry.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
// Capture SQL queries as spans
3838
'sql_queries' => true,
3939

40+
// Try to find out where the SQL query originated from and add it to the query spans
41+
'sql_origin' => true,
42+
4043
// Capture views as spans
4144
'views' => true,
4245
],
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Tracing;
4+
5+
use Sentry\Frame;
6+
use Sentry\Options;
7+
use Sentry\FrameBuilder;
8+
use Illuminate\Support\Str;
9+
use Sentry\Serializer\RepresentationSerializerInterface;
10+
11+
class BacktraceHelper
12+
{
13+
/**
14+
* @var Options The SDK client options
15+
*/
16+
private $options;
17+
18+
/**
19+
* @var FrameBuilder An instance of the builder of {@see Frame} objects
20+
*/
21+
private $frameBuilder;
22+
23+
/**
24+
* Constructor.
25+
*
26+
* @param Options $options The SDK client options
27+
* @param RepresentationSerializerInterface $representationSerializer The representation serializer
28+
*/
29+
public function __construct(Options $options, RepresentationSerializerInterface $representationSerializer)
30+
{
31+
$this->options = $options;
32+
$this->frameBuilder = new FrameBuilder($options, $representationSerializer);
33+
}
34+
35+
/**
36+
* Find the first in app frame for a given backtrace.
37+
*
38+
* @param array<int, array<string, mixed>> $backtrace The backtrace
39+
*
40+
* @phpstan-param list<array{
41+
* line?: integer,
42+
* file?: string,
43+
* }> $backtrace
44+
*/
45+
public function findFirstInAppFrameForBacktrace(array $backtrace): ?Frame
46+
{
47+
$file = Frame::INTERNAL_FRAME_FILENAME;
48+
$line = 0;
49+
50+
foreach ($backtrace as $backtraceFrame) {
51+
$frame = $this->frameBuilder->buildFromBacktraceFrame($file, $line, $backtraceFrame);
52+
53+
if ($frame->isInApp()) {
54+
return $frame;
55+
}
56+
57+
$file = $backtraceFrame['file'] ?? Frame::INTERNAL_FRAME_FILENAME;
58+
$line = $backtraceFrame['line'] ?? 0;
59+
}
60+
61+
return null;
62+
}
63+
64+
/**
65+
* Takes a frame and if it's a compiled view path returns the original view path.
66+
*
67+
* @param \Sentry\Frame $frame
68+
*
69+
* @return string|null
70+
*/
71+
public function getOriginalViewPathForFrameOfCompiledViewPath(Frame $frame): ?string
72+
{
73+
// Check if we are dealing with a frame for a cached view path
74+
if (!Str::startsWith($frame->getFile(), '/storage/framework/views/')) {
75+
return null;
76+
}
77+
78+
// If for some reason the file does not exists, skip resolving
79+
if (!file_exists($frame->getAbsoluteFilePath())) {
80+
return null;
81+
}
82+
83+
$viewFileContents = file_get_contents($frame->getAbsoluteFilePath());
84+
85+
preg_match('/PATH (?<originalPath>.*?) ENDPATH/', $viewFileContents, $matches);
86+
87+
// No path comment found in the file, must be a very old Laravel version
88+
if (empty($matches['originalPath'])) {
89+
return null;
90+
}
91+
92+
return $this->stripPrefixFromFilePath($matches['originalPath']);
93+
}
94+
95+
/**
96+
* Removes from the given file path the specified prefixes.
97+
*
98+
* @param string $filePath The path to the file
99+
*/
100+
private function stripPrefixFromFilePath(string $filePath): string
101+
{
102+
foreach ($this->options->getPrefixes() as $prefix) {
103+
if (Str::startsWith($filePath, $prefix)) {
104+
return mb_substr($filePath, mb_strlen($prefix));
105+
}
106+
}
107+
108+
return $filePath;
109+
}
110+
}

src/Sentry/Laravel/Tracing/EventHandler.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class EventHandler
5353
*/
5454
private $traceSqlQueries;
5555

56+
/**
57+
* Indicates if we should we add SQL query origin data to query spans.
58+
*
59+
* @var bool
60+
*/
61+
private $traceSqlQueryOrigins;
62+
5663
/**
5764
* Indicates if we should trace queue job spans.
5865
*
@@ -81,17 +88,28 @@ class EventHandler
8188
*/
8289
private $currentQueueJobSpan;
8390

91+
/**
92+
* The backtrace helper.
93+
*
94+
* @var \Sentry\Laravel\Tracing\BacktraceHelper
95+
*/
96+
private $backtraceHelper;
97+
8498
/**
8599
* EventHandler constructor.
86100
*
87101
* @param \Illuminate\Contracts\Container\Container $container
102+
* @param \Sentry\Laravel\Tracing\BacktraceHelper $backtraceHelper
88103
* @param array $config
89104
*/
90-
public function __construct(Container $container, array $config)
105+
public function __construct(Container $container, BacktraceHelper $backtraceHelper, array $config)
91106
{
92107
$this->container = $container;
108+
$this->backtraceHelper = $backtraceHelper;
93109

94110
$this->traceSqlQueries = ($config['sql_queries'] ?? true) === true;
111+
$this->traceSqlQueryOrigins = ($config['sql_origin'] ?? true) === true;
112+
95113
$this->traceQueueJobs = ($config['queue_jobs'] ?? false) === true;
96114
$this->traceQueueJobsAsTransactions = ($config['queue_job_transactions'] ?? false) === true;
97115
}
@@ -210,10 +228,36 @@ private function recordQuerySpan($query, $time): void
210228
$context->setStartTimestamp(microtime(true) - $time / 1000);
211229
$context->setEndTimestamp($context->getStartTimestamp() + $time / 1000);
212230

231+
if ($this->traceSqlQueryOrigins) {
232+
$queryOrigin = $this->resolveQueryOriginFromBacktrace($context);
233+
234+
if ($queryOrigin !== null) {
235+
$context->setData(['sql.origin' => $queryOrigin]);
236+
}
237+
}
238+
213239
$parentSpan->startChild($context);
214240
}
215241

216242
/**
243+
* Try to find the origin of the SQL query that was just executed.
244+
*
245+
* @return string|null
246+
*/
247+
private function resolveQueryOriginFromBacktrace(): ?string
248+
{
249+
$firstAppFrame = $this->backtraceHelper->findFirstInAppFrameForBacktrace(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS));
250+
251+
if ($firstAppFrame === null) {
252+
return null;
253+
}
254+
255+
$filePath = $this->backtraceHelper->getOriginalViewPathForFrameOfCompiledViewPath($firstAppFrame) ?? $firstAppFrame->getFile();
256+
257+
return "{$filePath}:{$firstAppFrame->getLine()}";
258+
}
259+
260+
/*
217261
* Since Laravel 5.2
218262
*
219263
* @param \Illuminate\Queue\Events\JobProcessing $event

src/Sentry/Laravel/Tracing/ServiceProvider.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
use Illuminate\Foundation\Http\Kernel as HttpKernel;
99
use Illuminate\View\Engines\EngineResolver;
1010
use Illuminate\View\Factory as ViewFactory;
11+
use InvalidArgumentException;
1112
use Laravel\Lumen\Application as Lumen;
1213
use Sentry\Laravel\BaseServiceProvider;
14+
use Sentry\Serializer\RepresentationSerializer;
1315

1416
class ServiceProvider extends BaseServiceProvider
1517
{
@@ -39,6 +41,15 @@ public function register(): void
3941
{
4042
$this->app->singleton(Middleware::class);
4143

44+
$this->app->singleton(BacktraceHelper::class, function () {
45+
/** @var \Sentry\State\Hub $sentry */
46+
$sentry = $this->app->make(self::$abstract);
47+
48+
$options = $sentry->getClient()->getOptions();
49+
50+
return new BacktraceHelper($options, new RepresentationSerializer($options));
51+
});
52+
4253
if (!$this->app instanceof Lumen) {
4354
$this->app->booted(function () {
4455
$this->app->make(Middleware::class)->setBootedTimestamp();
@@ -48,7 +59,11 @@ public function register(): void
4859

4960
private function bindEvents(array $tracingConfig): void
5061
{
51-
$handler = new EventHandler($this->app, $tracingConfig);
62+
$handler = new EventHandler(
63+
$this->app,
64+
$this->app->make(BacktraceHelper::class),
65+
$tracingConfig
66+
);
5267

5368
$handler->subscribe();
5469

test/Sentry/SentryLaravelTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Sentry\Breadcrumb;
77
use Sentry\State\Scope;
88
use ReflectionProperty;
9+
use Sentry\Laravel\Tracing;
910
use Sentry\State\HubInterface;
1011
use Sentry\Laravel\ServiceProvider;
1112
use Orchestra\Testbench\TestCase as LaravelTestCase;
@@ -30,6 +31,7 @@ protected function getPackageProviders($app)
3031
{
3132
return [
3233
ServiceProvider::class,
34+
Tracing\ServiceProvider::class,
3335
];
3436
}
3537

test/Sentry/Tracing/EventHandlerTest.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,21 @@
33
namespace Sentry\Laravel\Tests\Tracing;
44

55
use ReflectionClass;
6-
use Orchestra\Testbench\TestCase;
6+
use Sentry\Laravel\Tests\SentryLaravelTestCase;
7+
use Sentry\Laravel\Tracing\BacktraceHelper;
78
use RuntimeException;
89
use Sentry\Laravel\Tests\ExpectsException;
910
use Sentry\Laravel\Tracing\EventHandler;
1011

11-
class EventHandlerTest extends TestCase
12+
class EventHandlerTest extends SentryLaravelTestCase
1213
{
1314
use ExpectsException;
1415

1516
public function test_missing_event_handler_throws_exception()
1617
{
1718
$this->safeExpectException(RuntimeException::class);
1819

19-
$handler = new EventHandler($this->app, []);
20+
$handler = new EventHandler($this->app, $this->app->make(BacktraceHelper::class), []);
2021

2122
$handler->thisIsNotAHandlerAndShouldThrowAnException();
2223
}
@@ -30,7 +31,7 @@ public function test_all_mapped_event_handlers_exist()
3031

3132
private function tryAllEventHandlerMethods(array $methods): void
3233
{
33-
$handler = new EventHandler($this->app, []);
34+
$handler = new EventHandler($this->app, $this->app->make(BacktraceHelper::class), []);
3435

3536
$methods = array_map(static function ($method) {
3637
return "{$method}Handler";

0 commit comments

Comments
 (0)