Skip to content

Commit 2d45a88

Browse files
spawniastayallive
andauthored
Add tracing for storage access (#726)
Co-authored-by: Alex Bouma <[email protected]>
1 parent 7760e70 commit 2d45a88

14 files changed

+499
-12
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
"minimum-stability": "dev",
7575
"config": {
7676
"allow-plugins": {
77-
"kylekatarnls/update-helper": false
77+
"kylekatarnls/update-helper": false,
78+
"php-http/discovery": false
7879
}
7980
}
8081
}

config/sentry.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
// Capture Laravel cache events in breadcrumbs
2020
'cache' => true,
2121

22+
// Capture storage access as breadcrumbs
23+
'storage' => true,
24+
2225
// Capture Livewire components in breadcrumbs
2326
'livewire' => true,
2427

@@ -54,6 +57,9 @@
5457
// Capture views as spans
5558
'views' => true,
5659

60+
// Capture storage access as spans
61+
'storage' => true,
62+
5763
// Capture Livewire components as spans
5864
'livewire' => true,
5965

src/Sentry/Laravel/EventHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public function __call(string $method, array $arguments)
200200
}
201201

202202
try {
203-
call_user_func_array([$this, $handlerMethod], $arguments);
203+
$this->{$handlerMethod}(...$arguments);
204204
} catch (Exception $exception) {
205205
// Ignore
206206
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Features\Storage;
4+
5+
use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystem;
6+
use Illuminate\Contracts\Filesystem\Filesystem;
7+
use Illuminate\Contracts\Foundation\Application;
8+
use Illuminate\Filesystem\FilesystemManager;
9+
use RuntimeException;
10+
use Sentry\Laravel\Features\Feature;
11+
12+
class Integration extends Feature
13+
{
14+
private const FEATURE_KEY = 'storage';
15+
16+
private const STORAGE_DRIVER_NAME = 'sentry';
17+
18+
public function isApplicable(): bool
19+
{
20+
return $this->isTracingFeatureEnabled(self::FEATURE_KEY)
21+
|| $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY);
22+
}
23+
24+
public function setup(): void
25+
{
26+
foreach (config('filesystems.disks') as $disk => $config) {
27+
$currentDriver = $config['driver'];
28+
29+
if ($currentDriver === self::STORAGE_DRIVER_NAME) {
30+
continue;
31+
}
32+
33+
config([
34+
"filesystems.disks.{$disk}.driver" => self::STORAGE_DRIVER_NAME,
35+
"filesystems.disks.{$disk}.sentry_disk_name" => $disk,
36+
"filesystems.disks.{$disk}.sentry_original_driver" => $config['driver'],
37+
]);
38+
}
39+
40+
$this->container()->afterResolving(FilesystemManager::class, function (FilesystemManager $filesystemManager): void {
41+
$filesystemManager->extend(
42+
self::STORAGE_DRIVER_NAME,
43+
function (Application $application, array $config) use ($filesystemManager): Filesystem {
44+
if (empty($config['sentry_disk_name'])) {
45+
throw new RuntimeException(sprintf('Missing `sentry_disk_name` config key for `%s` filesystem driver.', self::STORAGE_DRIVER_NAME));
46+
}
47+
48+
if (empty($config['sentry_original_driver'])) {
49+
throw new RuntimeException(sprintf('Missing `sentry_original_driver` config key for `%s` filesystem driver.', self::STORAGE_DRIVER_NAME));
50+
}
51+
52+
if ($config['sentry_original_driver'] === self::STORAGE_DRIVER_NAME) {
53+
throw new RuntimeException(sprintf('`sentry_original_driver` for Sentry storage integration cannot be the `%s` driver.', self::STORAGE_DRIVER_NAME));
54+
}
55+
56+
$disk = $config['sentry_disk_name'];
57+
58+
$config['driver'] = $config['sentry_original_driver'];
59+
unset($config['sentry_original_driver']);
60+
61+
$diskResolver = (function (string $disk, array $config) {
62+
// This is a "hack" to make sure that the original driver is resolved by the FilesystemManager
63+
config(["filesystems.disks.{$disk}" => $config]);
64+
65+
return $this->resolve($disk);
66+
})->bindTo($filesystemManager, FilesystemManager::class);
67+
68+
$originalFilesystem = $diskResolver($disk, $config);
69+
70+
$defaultData = ['disk' => $disk, 'driver' => $config['driver']];
71+
72+
$recordSpans = $this->isTracingFeatureEnabled(self::FEATURE_KEY);
73+
$recordBreadcrumbs = $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY);
74+
75+
return $originalFilesystem instanceof CloudFilesystem
76+
? new SentryCloudFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs)
77+
: new SentryFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
78+
}
79+
);
80+
});
81+
}
82+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Features\Storage;
4+
5+
use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystem;
6+
7+
class SentryCloudFilesystem extends SentryFilesystem implements CloudFilesystem
8+
{
9+
/** @var CloudFilesystem */
10+
protected $filesystem;
11+
12+
public function __construct(CloudFilesystem $filesystem, array $defaultData, bool $recordSpans, bool $recordBreadcrumbs)
13+
{
14+
parent::__construct($filesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
15+
}
16+
17+
public function url($path)
18+
{
19+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
20+
}
21+
}
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php
2+
3+
namespace Sentry\Laravel\Features\Storage;
4+
5+
use Illuminate\Contracts\Filesystem\Filesystem;
6+
use Sentry\Breadcrumb;
7+
use Sentry\Laravel\Integration;
8+
use Sentry\Laravel\Util\Filesize;
9+
use Sentry\Tracing\SpanContext;
10+
use function Sentry\trace;
11+
12+
/**
13+
* Decorates the underlying filesystem by wrapping all calls to it with tracing.
14+
*
15+
* Parameters such as paths, directories or options are attached to the span as data,
16+
* parameters that contain file contents are omitted due to potential problems with
17+
* payload size or sensitive data.
18+
*/
19+
class SentryFilesystem implements Filesystem
20+
{
21+
/** @var Filesystem */
22+
protected $filesystem;
23+
24+
/** @var array */
25+
protected $defaultData;
26+
27+
/** @var bool */
28+
protected $recordSpans;
29+
30+
/** @var bool */
31+
protected $recordBreadcrumbs;
32+
33+
public function __construct(Filesystem $filesystem, array $defaultData, bool $recordSpans, bool $recordBreadcrumbs)
34+
{
35+
$this->filesystem = $filesystem;
36+
$this->defaultData = $defaultData;
37+
$this->recordSpans = $recordSpans;
38+
$this->recordBreadcrumbs = $recordBreadcrumbs;
39+
}
40+
41+
/**
42+
* Execute the method on the underlying filesystem and wrap it with tracing and log a breadcrumb.
43+
*
44+
* @param list<mixed> $args
45+
* @param array<string, mixed> $data
46+
*
47+
* @return mixed
48+
*/
49+
protected function withSentry(string $method, array $args, string $description, array $data)
50+
{
51+
$op = "file.{$method}"; // See https://develop.sentry.dev/sdk/performance/span-operations/#web-server
52+
$data = array_merge($data, $this->defaultData);
53+
54+
if ($this->recordBreadcrumbs) {
55+
Integration::addBreadcrumb(new Breadcrumb(
56+
Breadcrumb::LEVEL_INFO,
57+
Breadcrumb::TYPE_DEFAULT,
58+
$op,
59+
$description,
60+
$data
61+
));
62+
}
63+
64+
if ($this->recordSpans) {
65+
$spanContext = new SpanContext;
66+
$spanContext->setOp($op);
67+
$spanContext->setData($data);
68+
$spanContext->setDescription($description);
69+
70+
return trace(function () use ($method, $args) {
71+
return $this->filesystem->{$method}(...$args);
72+
}, $spanContext);
73+
}
74+
75+
return $this->filesystem->{$method}(...$args);
76+
}
77+
78+
/** @see \Illuminate\Filesystem\FilesystemAdapter::assertExists() */
79+
public function assertExists($path, $content = null)
80+
{
81+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
82+
}
83+
84+
/** @see \Illuminate\Filesystem\FilesystemAdapter::assertMissing() */
85+
public function assertMissing($path)
86+
{
87+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
88+
}
89+
90+
/** @see \Illuminate\Filesystem\FilesystemAdapter::assertDirectoryEmpty() */
91+
public function assertDirectoryEmpty($path)
92+
{
93+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
94+
}
95+
96+
public function exists($path)
97+
{
98+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
99+
}
100+
101+
public function get($path)
102+
{
103+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
104+
}
105+
106+
public function readStream($path)
107+
{
108+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
109+
}
110+
111+
public function put($path, $contents, $options = [])
112+
{
113+
$description = is_string($contents) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($contents))) : $path;
114+
115+
return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path', 'options'));
116+
}
117+
118+
public function writeStream($path, $resource, array $options = [])
119+
{
120+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'options'));
121+
}
122+
123+
public function getVisibility($path)
124+
{
125+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
126+
}
127+
128+
public function setVisibility($path, $visibility)
129+
{
130+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'visibility'));
131+
}
132+
133+
public function prepend($path, $data)
134+
{
135+
$description = is_string($data) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($data))) : $path;
136+
137+
return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path'));
138+
}
139+
140+
public function append($path, $data)
141+
{
142+
$description = is_string($data) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($data))) : $path;
143+
144+
return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path'));
145+
}
146+
147+
public function delete($paths)
148+
{
149+
if (is_array($paths)) {
150+
$data = compact('paths');
151+
$description = sprintf('%s paths', count($paths));
152+
} else {
153+
$data = ['path' => $paths];
154+
$description = $paths;
155+
}
156+
157+
return $this->withSentry(__FUNCTION__, func_get_args(), $description, $data);
158+
}
159+
160+
public function copy($from, $to)
161+
{
162+
return $this->withSentry(__FUNCTION__, func_get_args(), sprintf('from "%s" to "%s"', $from, $to), compact('from', 'to'));
163+
}
164+
165+
public function move($from, $to)
166+
{
167+
return $this->withSentry(__FUNCTION__, func_get_args(), sprintf('from "%s" to "%s"', $from, $to), compact('from', 'to'));
168+
}
169+
170+
public function size($path)
171+
{
172+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
173+
}
174+
175+
public function lastModified($path)
176+
{
177+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
178+
}
179+
180+
public function files($directory = null, $recursive = false)
181+
{
182+
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory', 'recursive'));
183+
}
184+
185+
public function allFiles($directory = null)
186+
{
187+
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory'));
188+
}
189+
190+
public function directories($directory = null, $recursive = false)
191+
{
192+
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory', 'recursive'));
193+
}
194+
195+
public function allDirectories($directory = null)
196+
{
197+
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory'));
198+
}
199+
200+
public function makeDirectory($path)
201+
{
202+
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
203+
}
204+
205+
public function deleteDirectory($directory)
206+
{
207+
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory'));
208+
}
209+
210+
public function __call($name, $arguments)
211+
{
212+
return $this->filesystem->{$name}(...$arguments);
213+
}
214+
}

src/Sentry/Laravel/ServiceProvider.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class ServiceProvider extends BaseServiceProvider
5454
Features\CacheIntegration::class,
5555
Features\QueueIntegration::class,
5656
Features\ConsoleIntegration::class,
57+
Features\Storage\Integration::class,
5758
Features\LivewirePackageIntegration::class,
5859
];
5960

src/Sentry/Laravel/Tracing/EventHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ public function __call(string $method, array $arguments)
146146
}
147147

148148
try {
149-
call_user_func_array([$this, $handlerMethod], $arguments);
149+
$this->{$handlerMethod}(...$arguments);
150150
} catch (Exception $e) {
151151
// Ignore to prevent bubbling up errors in the SDK
152152
}

src/Sentry/Laravel/Tracing/ViewEngineDecorator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,6 @@ public function get($path, array $data = []): string
5353

5454
public function __call($name, $arguments)
5555
{
56-
return call_user_func_array([$this->engine, $name], $arguments);
56+
return $this->engine->{$name}(...$arguments);
5757
}
5858
}

0 commit comments

Comments
 (0)