-
-
Notifications
You must be signed in to change notification settings - Fork 189
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tracing for storage access (#726)
Co-authored-by: Alex Bouma <alex@bouma.me>
- Loading branch information
1 parent
7760e70
commit 2d45a88
Showing
14 changed files
with
499 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
<?php | ||
|
||
namespace Sentry\Laravel\Features\Storage; | ||
|
||
use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystem; | ||
use Illuminate\Contracts\Filesystem\Filesystem; | ||
use Illuminate\Contracts\Foundation\Application; | ||
use Illuminate\Filesystem\FilesystemManager; | ||
use RuntimeException; | ||
use Sentry\Laravel\Features\Feature; | ||
|
||
class Integration extends Feature | ||
{ | ||
private const FEATURE_KEY = 'storage'; | ||
|
||
private const STORAGE_DRIVER_NAME = 'sentry'; | ||
|
||
public function isApplicable(): bool | ||
{ | ||
return $this->isTracingFeatureEnabled(self::FEATURE_KEY) | ||
|| $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); | ||
} | ||
|
||
public function setup(): void | ||
{ | ||
foreach (config('filesystems.disks') as $disk => $config) { | ||
$currentDriver = $config['driver']; | ||
|
||
if ($currentDriver === self::STORAGE_DRIVER_NAME) { | ||
continue; | ||
} | ||
|
||
config([ | ||
"filesystems.disks.{$disk}.driver" => self::STORAGE_DRIVER_NAME, | ||
"filesystems.disks.{$disk}.sentry_disk_name" => $disk, | ||
"filesystems.disks.{$disk}.sentry_original_driver" => $config['driver'], | ||
]); | ||
} | ||
|
||
$this->container()->afterResolving(FilesystemManager::class, function (FilesystemManager $filesystemManager): void { | ||
$filesystemManager->extend( | ||
self::STORAGE_DRIVER_NAME, | ||
function (Application $application, array $config) use ($filesystemManager): Filesystem { | ||
if (empty($config['sentry_disk_name'])) { | ||
throw new RuntimeException(sprintf('Missing `sentry_disk_name` config key for `%s` filesystem driver.', self::STORAGE_DRIVER_NAME)); | ||
} | ||
|
||
if (empty($config['sentry_original_driver'])) { | ||
throw new RuntimeException(sprintf('Missing `sentry_original_driver` config key for `%s` filesystem driver.', self::STORAGE_DRIVER_NAME)); | ||
} | ||
|
||
if ($config['sentry_original_driver'] === self::STORAGE_DRIVER_NAME) { | ||
throw new RuntimeException(sprintf('`sentry_original_driver` for Sentry storage integration cannot be the `%s` driver.', self::STORAGE_DRIVER_NAME)); | ||
} | ||
|
||
$disk = $config['sentry_disk_name']; | ||
|
||
$config['driver'] = $config['sentry_original_driver']; | ||
unset($config['sentry_original_driver']); | ||
|
||
$diskResolver = (function (string $disk, array $config) { | ||
// This is a "hack" to make sure that the original driver is resolved by the FilesystemManager | ||
config(["filesystems.disks.{$disk}" => $config]); | ||
|
||
return $this->resolve($disk); | ||
})->bindTo($filesystemManager, FilesystemManager::class); | ||
|
||
$originalFilesystem = $diskResolver($disk, $config); | ||
|
||
$defaultData = ['disk' => $disk, 'driver' => $config['driver']]; | ||
|
||
$recordSpans = $this->isTracingFeatureEnabled(self::FEATURE_KEY); | ||
$recordBreadcrumbs = $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY); | ||
|
||
return $originalFilesystem instanceof CloudFilesystem | ||
? new SentryCloudFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs) | ||
: new SentryFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs); | ||
} | ||
); | ||
}); | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/Sentry/Laravel/Features/Storage/SentryCloudFilesystem.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<?php | ||
|
||
namespace Sentry\Laravel\Features\Storage; | ||
|
||
use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystem; | ||
|
||
class SentryCloudFilesystem extends SentryFilesystem implements CloudFilesystem | ||
{ | ||
/** @var CloudFilesystem */ | ||
protected $filesystem; | ||
|
||
public function __construct(CloudFilesystem $filesystem, array $defaultData, bool $recordSpans, bool $recordBreadcrumbs) | ||
{ | ||
parent::__construct($filesystem, $defaultData, $recordSpans, $recordBreadcrumbs); | ||
} | ||
|
||
public function url($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
} |
214 changes: 214 additions & 0 deletions
214
src/Sentry/Laravel/Features/Storage/SentryFilesystem.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,214 @@ | ||
<?php | ||
|
||
namespace Sentry\Laravel\Features\Storage; | ||
|
||
use Illuminate\Contracts\Filesystem\Filesystem; | ||
use Sentry\Breadcrumb; | ||
use Sentry\Laravel\Integration; | ||
use Sentry\Laravel\Util\Filesize; | ||
use Sentry\Tracing\SpanContext; | ||
use function Sentry\trace; | ||
|
||
/** | ||
* Decorates the underlying filesystem by wrapping all calls to it with tracing. | ||
* | ||
* Parameters such as paths, directories or options are attached to the span as data, | ||
* parameters that contain file contents are omitted due to potential problems with | ||
* payload size or sensitive data. | ||
*/ | ||
class SentryFilesystem implements Filesystem | ||
{ | ||
/** @var Filesystem */ | ||
protected $filesystem; | ||
|
||
/** @var array */ | ||
protected $defaultData; | ||
|
||
/** @var bool */ | ||
protected $recordSpans; | ||
|
||
/** @var bool */ | ||
protected $recordBreadcrumbs; | ||
|
||
public function __construct(Filesystem $filesystem, array $defaultData, bool $recordSpans, bool $recordBreadcrumbs) | ||
{ | ||
$this->filesystem = $filesystem; | ||
$this->defaultData = $defaultData; | ||
$this->recordSpans = $recordSpans; | ||
$this->recordBreadcrumbs = $recordBreadcrumbs; | ||
} | ||
|
||
/** | ||
* Execute the method on the underlying filesystem and wrap it with tracing and log a breadcrumb. | ||
* | ||
* @param list<mixed> $args | ||
* @param array<string, mixed> $data | ||
* | ||
* @return mixed | ||
*/ | ||
protected function withSentry(string $method, array $args, string $description, array $data) | ||
{ | ||
$op = "file.{$method}"; // See https://develop.sentry.dev/sdk/performance/span-operations/#web-server | ||
$data = array_merge($data, $this->defaultData); | ||
|
||
if ($this->recordBreadcrumbs) { | ||
Integration::addBreadcrumb(new Breadcrumb( | ||
Breadcrumb::LEVEL_INFO, | ||
Breadcrumb::TYPE_DEFAULT, | ||
$op, | ||
$description, | ||
$data | ||
)); | ||
} | ||
|
||
if ($this->recordSpans) { | ||
$spanContext = new SpanContext; | ||
$spanContext->setOp($op); | ||
$spanContext->setData($data); | ||
$spanContext->setDescription($description); | ||
|
||
return trace(function () use ($method, $args) { | ||
return $this->filesystem->{$method}(...$args); | ||
}, $spanContext); | ||
} | ||
|
||
return $this->filesystem->{$method}(...$args); | ||
} | ||
|
||
/** @see \Illuminate\Filesystem\FilesystemAdapter::assertExists() */ | ||
public function assertExists($path, $content = null) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
/** @see \Illuminate\Filesystem\FilesystemAdapter::assertMissing() */ | ||
public function assertMissing($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
/** @see \Illuminate\Filesystem\FilesystemAdapter::assertDirectoryEmpty() */ | ||
public function assertDirectoryEmpty($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function exists($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function get($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function readStream($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function put($path, $contents, $options = []) | ||
{ | ||
$description = is_string($contents) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($contents))) : $path; | ||
|
||
return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path', 'options')); | ||
} | ||
|
||
public function writeStream($path, $resource, array $options = []) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'options')); | ||
} | ||
|
||
public function getVisibility($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function setVisibility($path, $visibility) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'visibility')); | ||
} | ||
|
||
public function prepend($path, $data) | ||
{ | ||
$description = is_string($data) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($data))) : $path; | ||
|
||
return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path')); | ||
} | ||
|
||
public function append($path, $data) | ||
{ | ||
$description = is_string($data) ? sprintf('%s (%s)', $path, Filesize::toHuman(strlen($data))) : $path; | ||
|
||
return $this->withSentry(__FUNCTION__, func_get_args(), $description, compact('path')); | ||
} | ||
|
||
public function delete($paths) | ||
{ | ||
if (is_array($paths)) { | ||
$data = compact('paths'); | ||
$description = sprintf('%s paths', count($paths)); | ||
} else { | ||
$data = ['path' => $paths]; | ||
$description = $paths; | ||
} | ||
|
||
return $this->withSentry(__FUNCTION__, func_get_args(), $description, $data); | ||
} | ||
|
||
public function copy($from, $to) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), sprintf('from "%s" to "%s"', $from, $to), compact('from', 'to')); | ||
} | ||
|
||
public function move($from, $to) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), sprintf('from "%s" to "%s"', $from, $to), compact('from', 'to')); | ||
} | ||
|
||
public function size($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function lastModified($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function files($directory = null, $recursive = false) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory', 'recursive')); | ||
} | ||
|
||
public function allFiles($directory = null) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory')); | ||
} | ||
|
||
public function directories($directory = null, $recursive = false) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory', 'recursive')); | ||
} | ||
|
||
public function allDirectories($directory = null) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory')); | ||
} | ||
|
||
public function makeDirectory($path) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path')); | ||
} | ||
|
||
public function deleteDirectory($directory) | ||
{ | ||
return $this->withSentry(__FUNCTION__, func_get_args(), $directory, compact('directory')); | ||
} | ||
|
||
public function __call($name, $arguments) | ||
{ | ||
return $this->filesystem->{$name}(...$arguments); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.