Skip to content
Open
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
108 changes: 102 additions & 6 deletions Classes/Log/SentryStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@

use Flownative\Sentry\SentryClientTrait;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Log\ThrowableStorage\FileStorage;
use Neos\Flow\Log\ThrowableStorageInterface;
use Neos\Flow\ObjectManagement\CompileTimeObjectManager;
use Neos\Flow\ObjectManagement\ObjectManagerInterface;

/**
* Captures a throwable to Sentry.
* Captures a throwable to Sentry and (optionally) also stores a Flow exception dump file
* (Data/Logs/Exceptions/*.txt) like the default FileStorage.
*
* The file dump logging can be disabled via `Flownative.Sentry.storageLogging` (default: true).
*
* @phpstan-consistent-constructor
* @Flow\Proxy(false)
Expand All @@ -18,6 +26,15 @@ class SentryStorage implements ThrowableStorageInterface
{
use SentryClientTrait;

private ?FileStorage $fileStorage = null;
private array $fileStorageOptions = [];

private ?bool $storageLoggingOption = null;
private ?bool $resolvedStorageLogging = null;

private ?\Closure $requestInformationRenderer = null;
private ?\Closure $backtraceRenderer = null;

/**
* Factory method to get an instance.
*
Expand All @@ -26,7 +43,24 @@ class SentryStorage implements ThrowableStorageInterface
*/
public static function createWithOptions(array $options): ThrowableStorageInterface
{
return new static();
$storageLoggingOption = array_key_exists('storageLogging', $options) ? (bool)$options['storageLogging'] : null;

// Allow passing FileStorage options either directly, or nested under "fileStorageOptions".
$fileStorageOptions = $options['fileStorageOptions'] ?? $options;
if (is_array($fileStorageOptions) && array_key_exists('storageLogging', $fileStorageOptions)) {
unset($fileStorageOptions['storageLogging']);
}
if (is_array($fileStorageOptions) && array_key_exists('fileStorageOptions', $fileStorageOptions)) {
unset($fileStorageOptions['fileStorageOptions']);
}

return new static($fileStorageOptions, $storageLoggingOption);
}

public function __construct(array $fileStorageOptions = [], ?bool $storageLoggingOption = null)
{
$this->fileStorageOptions = $fileStorageOptions;
$this->storageLoggingOption = $storageLoggingOption;
}

/**
Expand All @@ -35,6 +69,10 @@ public static function createWithOptions(array $options): ThrowableStorageInterf
*/
public function setRequestInformationRenderer(\Closure $requestInformationRenderer): ThrowableStorageInterface
{
$this->requestInformationRenderer = $requestInformationRenderer;
if ($this->fileStorage !== null) {
$this->fileStorage->setRequestInformationRenderer($requestInformationRenderer);
}
return $this;
}

Expand All @@ -44,6 +82,10 @@ public function setRequestInformationRenderer(\Closure $requestInformationRender
*/
public function setBacktraceRenderer(\Closure $backtraceRenderer): ThrowableStorageInterface
{
$this->backtraceRenderer = $backtraceRenderer;
if ($this->fileStorage !== null) {
$this->fileStorage->setBacktraceRenderer($backtraceRenderer);
}
return $this;
}

Expand All @@ -61,7 +103,11 @@ public function setBacktraceRenderer(\Closure $backtraceRenderer): ThrowableStor
*/
public function logThrowable(\Throwable $throwable, array $additionalData = []): string
{
$message = $this->getErrorLogMessage($throwable);
$message = $this->isStorageLoggingEnabled()
? $this->getFileStorage()->logThrowable($throwable, $additionalData)
: $this->getErrorLogMessage($throwable);

// Also capture to Sentry (best-effort)
try {
if ($sentryClient = self::getSentryClient()) {
$captureResult = $sentryClient->captureThrowable($throwable, $additionalData);
Expand All @@ -74,13 +120,63 @@ public function logThrowable(\Throwable $throwable, array $additionalData = []):
);
}
} catch (\Throwable $e) {
return $message . ' (Sentry: Error capturing message – ' . $this->getErrorLogMessage($e);
return sprintf('%s (Sentry: Error capturing message – %s [%s])', $message, $e->getMessage(), (string)$e->getCode());
}

return $message . '(Sentry: no client available)';
return $message . ' (Sentry: no client available)';
}

private function getFileStorage(): FileStorage
{
if ($this->fileStorage === null) {
$this->fileStorage = FileStorage::createWithOptions($this->fileStorageOptions);
if ($this->requestInformationRenderer !== null) {
$this->fileStorage->setRequestInformationRenderer($this->requestInformationRenderer);
}
if ($this->backtraceRenderer !== null) {
$this->fileStorage->setBacktraceRenderer($this->backtraceRenderer);
}
}

return $this->fileStorage;
}

private function isStorageLoggingEnabled(): bool
{
if ($this->resolvedStorageLogging !== null) {
return $this->resolvedStorageLogging;
}

if ($this->storageLoggingOption !== null) {
return $this->resolvedStorageLogging = $this->storageLoggingOption;
}

$settings = $this->getPackageSettings();
if (is_array($settings) && array_key_exists('storageLogging', $settings)) {
return $this->resolvedStorageLogging = (bool)$settings['storageLogging'];
}

return $this->resolvedStorageLogging = true;
}

private function getPackageSettings(): ?array
{
try {
if (!Bootstrap::$staticObjectManager instanceof ObjectManagerInterface || Bootstrap::$staticObjectManager instanceof CompileTimeObjectManager) {
return null;
}

$configurationManager = Bootstrap::$staticObjectManager->get(ConfigurationManager::class);
/** @var array|null $settings */
$settings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Flownative.Sentry');

return is_array($settings) ? $settings : null;
} catch (\Throwable $e) {
return null;
}
}

protected function getErrorLogMessage(\Throwable $error): string
private function getErrorLogMessage(\Throwable $error): string
{
// getCode() does not always return an integer, e.g. in PDOException it can be a string
if (is_int($error->getCode()) && $error->getCode() > 0) {
Expand Down
2 changes: 1 addition & 1 deletion Classes/SentryClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ public function initializeObject(): void
'release' => $this->release,
'sample_rate' => $this->sampleRate,
'traces_sample_rate' => $this->tracesSampleRate,
'ignore_exceptions' => array_keys(array_filter($this->excludeExceptionTypes)),
'ignore_exceptions' => array_map('strval', array_keys(array_filter($this->excludeExceptionTypes))),
'in_app_exclude' => [
FLOW_PATH_ROOT . '/Packages/Application/Flownative.Sentry/Classes/',
FLOW_PATH_ROOT . '/Packages/Framework/Neos.Flow/Classes/Aop/',
Expand Down
1 change: 1 addition & 0 deletions Configuration/Settings.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Flownative:
sampleRate: 1.0
tracesSampleRate: 0
errorLevel: null
storageLogging: false
capture:
excludeExceptionTypes:
'Neos\Flow\Mvc\Controller\Exception\InvalidControllerException': true
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ The default is `null`, that makes Sentry use the value returned by the
`error_reporting()` function. The available error levels are documented at
[PHP error constants](https://www.php.net/manual/en/errorfunc.constants.php).

By default, this package also keeps Flow's exception dump files in
`Data/Logs/Exceptions/*.txt` (so the Neos/Flow message "For a full stacktrace, open …"
still works). You can disable writing those dump files with:

```yaml
Flownative:
Sentry:
storageLogging: false
```

**Beware:** a low error log level can lead to your application not loading
anymore and your Sentry account being flooded with error messages.

Expand Down