Skip to content

Commit

Permalink
Add tracing for storage access (#726)
Browse files Browse the repository at this point in the history
Co-authored-by: Alex Bouma <alex@bouma.me>
  • Loading branch information
spawnia and stayallive authored Jul 27, 2023
1 parent 7760e70 commit 2d45a88
Show file tree
Hide file tree
Showing 14 changed files with 499 additions and 12 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,8 @@
"minimum-stability": "dev",
"config": {
"allow-plugins": {
"kylekatarnls/update-helper": false
"kylekatarnls/update-helper": false,
"php-http/discovery": false
}
}
}
6 changes: 6 additions & 0 deletions config/sentry.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
// Capture Laravel cache events in breadcrumbs
'cache' => true,

// Capture storage access as breadcrumbs
'storage' => true,

// Capture Livewire components in breadcrumbs
'livewire' => true,

Expand Down Expand Up @@ -54,6 +57,9 @@
// Capture views as spans
'views' => true,

// Capture storage access as spans
'storage' => true,

// Capture Livewire components as spans
'livewire' => true,

Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Laravel/EventHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ public function __call(string $method, array $arguments)
}

try {
call_user_func_array([$this, $handlerMethod], $arguments);
$this->{$handlerMethod}(...$arguments);
} catch (Exception $exception) {
// Ignore
}
Expand Down
82 changes: 82 additions & 0 deletions src/Sentry/Laravel/Features/Storage/Integration.php
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 src/Sentry/Laravel/Features/Storage/SentryCloudFilesystem.php
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 src/Sentry/Laravel/Features/Storage/SentryFilesystem.php
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);
}
}
1 change: 1 addition & 0 deletions src/Sentry/Laravel/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ class ServiceProvider extends BaseServiceProvider
Features\CacheIntegration::class,
Features\QueueIntegration::class,
Features\ConsoleIntegration::class,
Features\Storage\Integration::class,
Features\LivewirePackageIntegration::class,
];

Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Laravel/Tracing/EventHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ public function __call(string $method, array $arguments)
}

try {
call_user_func_array([$this, $handlerMethod], $arguments);
$this->{$handlerMethod}(...$arguments);
} catch (Exception $e) {
// Ignore to prevent bubbling up errors in the SDK
}
Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/Laravel/Tracing/ViewEngineDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ public function get($path, array $data = []): string

public function __call($name, $arguments)
{
return call_user_func_array([$this->engine, $name], $arguments);
return $this->engine->{$name}(...$arguments);
}
}
Loading

0 comments on commit 2d45a88

Please sign in to comment.