Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,47 @@
# Changelog

## Unreleased

### Features

- The filesystem adapters for the `sentry` driver now extend the well-known Laravel classes they decorate,
`Illuminate\Filesystem\FilesystemAdapter` and `Illuminate\Filesystem\AwsS3V3Adapter`.

Enabling the feature can be simplified by wrapping the configuration for all disks
with a call to `Sentry\Laravel\Features\Storage\Integration::configureDisks()`
in your `config/filesystems.php` file:

```php
'disks' => Sentry\Laravel\Features\Storage\Integration::configureDisks([
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],

// ...
], /* enableSpans: */ true, /* enableBreadcrumbs: */ true),
```

Alternatively, you can enable this feature only for select disks:

```php
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],

's3' => Sentry\Laravel\Features\Storage\Integration::configureDisk('s3', [
// ...
], /* enableSpans: */ true, /* enableBreadcrumbs: */ true),
],
```

By default, both spans and breadcrumbs are enabled.
You may disable them by passing the second argument `$enableSpans` or the third argument `$enableBreadcrumbs`.

## 3.7.3

The Sentry SDK team is happy to announce the immediate availability of Sentry Laravel SDK v3.7.3.
Expand Down
13 changes: 13 additions & 0 deletions src/Sentry/Laravel/Features/Storage/CloudFilesystemDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Sentry\Laravel\Features\Storage;

trait CloudFilesystemDecorator
{
use FilesystemDecorator;

public function url($path)
{
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
}
}
33 changes: 33 additions & 0 deletions src/Sentry/Laravel/Features/Storage/FilesystemAdapterDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace Sentry\Laravel\Features\Storage;

trait FilesystemAdapterDecorator
{
use CloudFilesystemDecorator;

public function assertExists($path, $content = null)
{
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
}

public function assertMissing($path)
{
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
}

public function assertDirectoryEmpty($path)
{
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path'));
}

public function temporaryUrl($path, $expiration, array $options = [])
{
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'expiration', 'options'));
}

public function temporaryUploadUrl($path, $expiration, array $options = [])
{
return $this->withSentry(__FUNCTION__, func_get_args(), $path, compact('path', 'expiration', 'options'));
}
}
188 changes: 188 additions & 0 deletions src/Sentry/Laravel/Features/Storage/FilesystemDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
<?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.
*/
trait FilesystemDecorator
{
/** @var Filesystem */
protected $filesystem;

/** @var array */
protected $defaultData;

/** @var bool */
protected $recordSpans;

/** @var bool */
protected $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);
}

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, $separator = PHP_EOL)
{
$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, $separator = PHP_EOL)
{
$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);
}
}
76 changes: 68 additions & 8 deletions src/Sentry/Laravel/Features/Storage/Integration.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
use Illuminate\Contracts\Filesystem\Cloud as CloudFilesystem;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Filesystem\AwsS3V3Adapter;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Filesystem\FilesystemManager;
use RuntimeException;
use Sentry\Laravel\Features\Feature;
Expand All @@ -22,11 +24,6 @@ public function isApplicable(): bool
}

public function register(): void
{
$this->registerDiskDriver();
}

private function registerDiskDriver(): void
{
$this->container()->afterResolving(FilesystemManager::class, function (FilesystemManager $filesystemManager): void {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although this line of code was written by you in the previous PR, I think it is not a good idea to extend the filesystemManager call when afterResolving it.
This is because the FilesystemManager class may be resolved before the SentryServiceProvider, and therefore, the callback will never be called.

Suggested change
$this->container()->afterResolving(FilesystemManager::class, function (FilesystemManager $filesystemManager): void {
$callback = function (FilesystemManager $filesystemManager): void {
$filesystemManager->extend(self::STORAGE_DRIVER_NAME, function (Application $application, array $config) use ($filesystemManager): Filesystem {
//...
});
};
if ($this->container()->resolved(FilesystemManager::class)) {
$callback($this->container()->get(FilesystemManager::class));
} else {
$this->container()->afterResolving(FilesystemManager::class, $callback);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That should never happen. We register in the register callback of a service provider. And resolving should have never happened there so this should be perfectly fine unless somethin wonky is going on. Do you have a specific use case in mind where this would actually fail?

Copy link

@yankewei yankewei Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it actually happend in our project, but I need time to reproduce in a simple Laravel project.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you have a service provider that is not well behaved, meaning that it does more than just register bindings.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you register the Sentry service provider manually and from inside another service provider, but we use the afterResolving callback on more places and I've never seen the issues you describe or heard people having them.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we have a service provider that calls $this->app->make('filesystem') in the boot method.

Copy link
Collaborator

@stayallive stayallive Aug 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right, we are doing this in the boot not register. I'll see what we can do to change that. But your suggestion wouldn't currently fix the problem since it will still be too late, but I see where the problem is.

Edit: working on solution here #759.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yankewei it took a bit, but I think we solved the issue and now register the driver before Laravel is booted which should solve the issues you were seeing!

$filesystemManager->extend(
Expand Down Expand Up @@ -57,18 +54,81 @@ function (Application $application, array $config) use ($filesystemManager): Fil
return $this->resolve($disk);
})->bindTo($filesystemManager, FilesystemManager::class);

/** @var Filesystem $originalFilesystem */
$originalFilesystem = $diskResolver($disk, $config);

$defaultData = ['disk' => $disk, 'driver' => $config['driver']];

$recordSpans = $config['sentry_enable_spans'] ?? $this->isTracingFeatureEnabled(self::FEATURE_KEY);
$recordBreadcrumbs = $config['sentry_enable_breadcrumbs'] ?? $this->isBreadcrumbFeatureEnabled(self::FEATURE_KEY);

return $originalFilesystem instanceof CloudFilesystem
? new SentryCloudFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs)
: new SentryFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
if ($originalFilesystem instanceof AwsS3V3Adapter) {
return new SentryS3V3Adapter($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
}

if ($originalFilesystem instanceof FilesystemAdapter) {
return new SentryFilesystemAdapter($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
}

if ($originalFilesystem instanceof CloudFilesystem) {
return new SentryCloudFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
}

return new SentryFilesystem($originalFilesystem, $defaultData, $recordSpans, $recordBreadcrumbs);
}
);
});
}

/**
* Decorates the configuration for a single disk with Sentry driver configuration.

* This replaces the driver with a custom driver that will capture performance traces and breadcrumbs.
*
* The custom driver will be an instance of @see \Sentry\Laravel\Features\Storage\SentryS3V3Adapter
* if the original driver is an @see \Illuminate\Filesystem\AwsS3V3Adapter,
* and an instance of @see \Sentry\Laravel\Features\Storage\SentryFilesystemAdapter
* if the original driver is an @see \Illuminate\Filesystem\FilesystemAdapter.
* If the original driver is neither of those, it will be @see \Sentry\Laravel\Features\Storage\SentryFilesystem
* or @see \Sentry\Laravel\Features\Storage\SentryCloudFilesystem based on the contract of the original driver.
*
* You might run into problems if you expect another specific driver class.
*
* @param array<string, mixed> $diskConfig
*
* @return array<string, mixed>
*/
public static function configureDisk(string $diskName, array $diskConfig, bool $enableSpans = true, bool $enableBreadcrumbs = true): array
{
$currentDriver = $diskConfig['driver'];

if ($currentDriver !== self::STORAGE_DRIVER_NAME) {
$diskConfig['driver'] = self::STORAGE_DRIVER_NAME;
$diskConfig['sentry_disk_name'] = $diskName;
$diskConfig['sentry_original_driver'] = $currentDriver;
$diskConfig['sentry_enable_spans'] = $enableSpans;
$diskConfig['sentry_enable_breadcrumbs'] = $enableBreadcrumbs;
}

return $diskConfig;
}

/**
* Decorates the configuration for all disks with Sentry driver configuration.
*
* @see self::configureDisk()
*
* @param array<string, array<string, mixed>> $diskConfigs
*
* @return array<string, array<string, mixed>>
*/
public static function configureDisks(array $diskConfigs, bool $enableSpans = true, bool $enableBreadcrumbs = true): array
{
$diskConfigsWithSentryDriver = [];
foreach ($diskConfigs as $diskName => $diskConfig) {
$diskConfigsWithSentryDriver[$diskName] = static::configureDisk($diskName, $diskConfig, $enableSpans, $enableBreadcrumbs);
}

return $diskConfigsWithSentryDriver;
}
}
Loading