diff --git a/CHANGELOG.md b/CHANGELOG.md
index f6a56a76..728a9a87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Fix compatibility with sentry/sentry 2.2+ (#244)
- Add support for `class_serializers` option (#245)
- Add support for `max_request_body_size` option (#249)
+ - Add option to disable the error listener completely (#247, thanks to @HypeMC)
+ - Add options to register the Monolog Handler (#247, thanks to @HypeMC)
## 3.1.0 - 2019-07-02
- Add support for Symfony 2.8 (#233, thanks to @nocive)
diff --git a/README.md b/README.md
index e969e1ed..bbb73be9 100644
--- a/README.md
+++ b/README.md
@@ -103,17 +103,20 @@ the [PHP specific](https://docs.sentry.io/platforms/php/#php-specific-options) o
#### Optional: use monolog handler provided by `sentry/sentry`
*Note: this step is optional*
-If You're using `monolog` for logging e.g. in-app errors, You
+If you're using `monolog` for logging e.g. in-app errors, you
can use this handler in order for them to show up in Sentry.
-First, define `Sentry\Monolog\Handler` as a service in `config/services.yaml`
+First, enable & configure the `Sentry\Monolog\Handler`; you'll also need
+to disable the `Sentry\SentryBundle\EventListener\ErrorListener` to
+avoid having duplicate events in Sentry:
```yaml
-services:
- sentry.monolog.handler:
- class: Sentry\Monolog\Handler
- arguments:
- $level: 'error'
+sentry:
+ register_error_listener: false # Disables the ErrorListener
+ monolog:
+ error_handler:
+ enabled: true
+ level: error
```
Then enable it in `monolog` config:
@@ -123,8 +126,7 @@ monolog:
handlers:
sentry:
type: service
- id: sentry.monolog.handler
- level: error
+ id: Sentry\Monolog\Handler
```
## Maintained versions
diff --git a/composer.json b/composer.json
index 435ce3e7..105153f0 100644
--- a/composer.json
+++ b/composer.json
@@ -32,6 +32,7 @@
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.8",
"jangregor/phpstan-prophecy": "^0.3.0",
+ "monolog/monolog": "^1.11||^2.0",
"php-http/mock-client": "^1.0",
"phpstan/phpstan": "^0.11",
"phpstan/phpstan-phpunit": "^0.11",
@@ -39,13 +40,16 @@
"scrutinizer/ocular": "^1.4",
"symfony/expression-language": "^2.8||^3.0||^4.0"
},
+ "suggest": {
+ "monolog/monolog": "Required to use the Monolog handler"
+ },
"autoload": {
- "psr-4" : {
+ "psr-4": {
"Sentry\\SentryBundle\\": "src/"
}
},
"autoload-dev": {
- "psr-4" : {
+ "psr-4": {
"Sentry\\SentryBundle\\Test\\": "test"
}
},
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 8a25035f..e210bb16 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -37,6 +37,10 @@ public function getConfigTreeBuilder(): TreeBuilder
->ifString()
->then($this->getTrimClosure());
+ $rootNode->children()
+ ->booleanNode('register_error_listener')
+ ->defaultTrue();
+
// Options array (to be passed to Sentry\Options constructor) -- please keep alphabetical order!
$optionsNode = $rootNode->children()
->arrayNode('options')
@@ -139,6 +143,24 @@ public function getConfigTreeBuilder(): TreeBuilder
$listenerPriorities->scalarNode('console_error')
->defaultValue(128);
+ // Monolog handler configuration
+ $monologConfiguration = $rootNode->children()
+ ->arrayNode('monolog')
+ ->addDefaultsIfNotSet()
+ ->children();
+
+ $errorHandler = $monologConfiguration
+ ->arrayNode('error_handler')
+ ->addDefaultsIfNotSet()
+ ->children();
+ $errorHandler->booleanNode('enabled')
+ ->defaultFalse();
+ $errorHandler->scalarNode('level')
+ ->defaultValue('DEBUG')
+ ->cannotBeEmpty();
+ $errorHandler->booleanNode('bubble')
+ ->defaultTrue();
+
return $treeBuilder;
}
diff --git a/src/DependencyInjection/SentryExtension.php b/src/DependencyInjection/SentryExtension.php
index 4d177b40..8a3ba649 100644
--- a/src/DependencyInjection/SentryExtension.php
+++ b/src/DependencyInjection/SentryExtension.php
@@ -2,7 +2,9 @@
namespace Sentry\SentryBundle\DependencyInjection;
+use Monolog\Logger as MonologLogger;
use Sentry\ClientBuilderInterface;
+use Sentry\Monolog\Handler;
use Sentry\Options;
use Sentry\SentryBundle\Command\SentryTestCommand;
use Sentry\SentryBundle\ErrorTypesParser;
@@ -14,6 +16,7 @@
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\DependencyInjection\ContainerBuilder;
+use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\DependencyInjection\Loader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -47,8 +50,9 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('sentry.listener_priorities.' . $key, $priority);
}
- $this->tagConsoleErrorListener($container);
+ $this->configureErrorListener($container, $processedConfiguration);
$this->setLegacyVisibilities($container);
+ $this->configureMonologHandler($container, $processedConfiguration['monolog']);
}
private function passConfigurationToOptions(ContainerBuilder $container, array $processedConfiguration): void
@@ -134,6 +138,17 @@ private function valueToCallable($value)
return $value;
}
+ private function configureErrorListener(ContainerBuilder $container, array $processedConfiguration): void
+ {
+ if (! $processedConfiguration['register_error_listener']) {
+ $container->removeDefinition(ErrorListener::class);
+
+ return;
+ }
+
+ $this->tagConsoleErrorListener($container);
+ }
+
/**
* BC layer for Symfony < 3.3; see https://symfony.com/blog/new-in-symfony-3-3-better-handling-of-command-exceptions
*/
@@ -166,9 +181,40 @@ private function setLegacyVisibilities(ContainerBuilder $container): void
if (Kernel::VERSION_ID < 30300) {
$container->getDefinition(SentryTestCommand::class)->setPublic(true);
$container->getDefinition(ConsoleListener::class)->setPublic(true);
- $container->getDefinition(ErrorListener::class)->setPublic(true);
$container->getDefinition(RequestListener::class)->setPublic(true);
$container->getDefinition(SubRequestListener::class)->setPublic(true);
+
+ if ($container->hasDefinition(ErrorListener::class)) {
+ $container->getDefinition(ErrorListener::class)->setPublic(true);
+ }
}
}
+
+ private function configureMonologHandler(ContainerBuilder $container, array $monologConfiguration): void
+ {
+ $errorHandler = $monologConfiguration['error_handler'];
+
+ if (! $errorHandler['enabled']) {
+ $container->removeDefinition(Handler::class);
+
+ return;
+ }
+
+ if (! class_exists(Handler::class)) {
+ throw new LogicException(
+ sprintf('Missing class "%s", try updating "sentry/sentry" to a newer version.', Handler::class)
+ );
+ }
+
+ if (! class_exists(MonologLogger::class)) {
+ throw new LogicException(
+ sprintf('You cannot use "%s" if Monolog is not available.', Handler::class)
+ );
+ }
+
+ $container
+ ->getDefinition(Handler::class)
+ ->replaceArgument('$level', MonologLogger::toMonologLevel($errorHandler['level']))
+ ->replaceArgument('$bubble', $errorHandler['bubble']);
+ }
}
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index 28c60c8d..4c2e5f65 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -51,5 +51,11 @@
+
+
+
+
+
+
diff --git a/test/DependencyInjection/ConfigurationTest.php b/test/DependencyInjection/ConfigurationTest.php
index 97874a57..2cda4a66 100644
--- a/test/DependencyInjection/ConfigurationTest.php
+++ b/test/DependencyInjection/ConfigurationTest.php
@@ -48,6 +48,7 @@ public function testConfigurationDefaults(): void
$processed = $this->processConfiguration([]);
$expectedDefaults = [
'dsn' => null,
+ 'register_error_listener' => true,
'listener_priorities' => [
'request' => 1,
'sub_request' => 1,
@@ -67,6 +68,13 @@ public function testConfigurationDefaults(): void
'project_root' => '%kernel.root_dir%/..',
'tags' => [],
],
+ 'monolog' => [
+ 'error_handler' => [
+ 'enabled' => false,
+ 'level' => 'DEBUG',
+ 'bubble' => true,
+ ],
+ ],
];
if (method_exists(Kernel::class, 'getProjectDir')) {
diff --git a/test/DependencyInjection/SentryExtensionTest.php b/test/DependencyInjection/SentryExtensionTest.php
index e66f5d6a..f8ba499e 100644
--- a/test/DependencyInjection/SentryExtensionTest.php
+++ b/test/DependencyInjection/SentryExtensionTest.php
@@ -3,16 +3,23 @@
namespace Sentry\SentryBundle\Test\DependencyInjection;
use Jean85\PrettyVersions;
+use Monolog\Logger as MonologLogger;
use Sentry\Breadcrumb;
+use Sentry\ClientInterface;
use Sentry\Event;
use Sentry\Integration\IntegrationInterface;
+use Sentry\Monolog\Handler;
use Sentry\Options;
use Sentry\SentryBundle\DependencyInjection\SentryExtension;
+use Sentry\SentryBundle\EventListener\ErrorListener;
use Sentry\SentryBundle\Test\BaseTestCase;
+use Sentry\Severity;
+use Sentry\State\Scope;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
+use Symfony\Component\DependencyInjection\Exception\LogicException;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Kernel;
@@ -20,6 +27,8 @@
class SentryExtensionTest extends BaseTestCase
{
private const OPTIONS_TEST_PUBLIC_ALIAS = 'sentry.options.public_alias';
+ private const ERROR_LISTENER_TEST_PUBLIC_ALIAS = 'sentry.error_listener.public_alias';
+ private const MONOLOG_HANDLER_TEST_PUBLIC_ALIAS = 'sentry.monolog_handler.public_alias';
public function testDataProviderIsMappingTheRightNumberOfOptions(): void
{
@@ -335,6 +344,76 @@ public function testIntegrations(): void
$this->assertCount(1, $integrations);
}
+ /**
+ * @dataProvider errorListenerConfigurationProvider
+ */
+ public function testErrorListenerIsRegistered(bool $registerErrorListener): void
+ {
+ $container = $this->getContainer([
+ 'register_error_listener' => $registerErrorListener,
+ ]);
+
+ $this->assertSame($registerErrorListener, $container->has(self::ERROR_LISTENER_TEST_PUBLIC_ALIAS));
+ }
+
+ public function errorListenerConfigurationProvider(): array
+ {
+ return [
+ [true],
+ [false],
+ ];
+ }
+
+ /**
+ * @dataProvider monologHandlerConfigurationProvider
+ */
+ public function testMonologHandlerIsConfiguredProperly($level, bool $bubble, int $monologLevel): void
+ {
+ $this->expectExceptionIfMonologHandlerDoesNotExist();
+
+ $container = $this->getContainer([
+ 'monolog' => [
+ 'error_handler' => [
+ 'enabled' => true,
+ 'level' => $level,
+ 'bubble' => $bubble,
+ ],
+ ],
+ ]);
+
+ $this->assertTrue($container->has(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS));
+
+ /** @var Handler $handler */
+ $handler = $container->get(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS);
+ $this->assertEquals($monologLevel, $handler->getLevel());
+ $this->assertEquals($bubble, $handler->getBubble());
+ }
+
+ public function monologHandlerConfigurationProvider(): array
+ {
+ return [
+ ['DEBUG', true, MonologLogger::DEBUG],
+ ['debug', false, MonologLogger::DEBUG],
+ ['ERROR', true, MonologLogger::ERROR],
+ ['error', false, MonologLogger::ERROR],
+ [MonologLogger::ALERT, true, MonologLogger::ALERT],
+ [MonologLogger::EMERGENCY, false, MonologLogger::EMERGENCY],
+ ];
+ }
+
+ public function testMonologHandlerIsNotRegistered(): void
+ {
+ $container = $this->getContainer([
+ 'monolog' => [
+ 'error_handler' => [
+ 'enabled' => false,
+ ],
+ ],
+ ]);
+
+ $this->assertFalse($container->has(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS));
+ }
+
private function getContainer(array $configuration = []): Container
{
$containerBuilder = new ContainerBuilder();
@@ -363,6 +442,17 @@ private function getContainer(array $configuration = []): Container
$extension = new SentryExtension();
$extension->load(['sentry' => $configuration], $containerBuilder);
+ $client = new Definition(ClientMock::class);
+ $containerBuilder->setDefinition(ClientInterface::class, $client);
+
+ if ($containerBuilder->hasDefinition(ErrorListener::class)) {
+ $containerBuilder->setAlias(self::ERROR_LISTENER_TEST_PUBLIC_ALIAS, new Alias(ErrorListener::class, true));
+ }
+
+ if ($containerBuilder->hasDefinition(Handler::class)) {
+ $containerBuilder->setAlias(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS, new Alias(Handler::class, true));
+ }
+
$containerBuilder->compile();
return $containerBuilder;
@@ -377,6 +467,16 @@ private function getOptionsFrom(Container $container): Options
return $options;
}
+
+ private function expectExceptionIfMonologHandlerDoesNotExist(): void
+ {
+ if (! class_exists(Handler::class)) {
+ $this->expectException(LogicException::class);
+ $this->expectExceptionMessage(
+ sprintf('Missing class "%s", try updating "sentry/sentry" to a newer version.', Handler::class)
+ );
+ }
+ }
}
function mockBeforeSend(Event $event): ?Event
@@ -413,3 +513,36 @@ public function setupOnce(): void
{
}
}
+
+class ClientMock implements ClientInterface
+{
+ public function getOptions(): Options
+ {
+ return new Options();
+ }
+
+ public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null): ?string
+ {
+ return null;
+ }
+
+ public function captureException(\Throwable $exception, ?Scope $scope = null): ?string
+ {
+ return null;
+ }
+
+ public function captureLastError(?Scope $scope = null): ?string
+ {
+ return null;
+ }
+
+ public function captureEvent(array $payload, ?Scope $scope = null): ?string
+ {
+ return null;
+ }
+
+ public function getIntegration(string $className): ?IntegrationInterface
+ {
+ return null;
+ }
+}