Skip to content

Commit 028b70e

Browse files
committed
Add Monolog handler configuration
1 parent f21d3f8 commit 028b70e

File tree

7 files changed

+188
-12
lines changed

7 files changed

+188
-12
lines changed

README.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,20 @@ the [PHP specific](https://docs.sentry.io/platforms/php/#php-specific-options) o
103103
#### Optional: use monolog handler provided by `sentry/sentry`
104104
*Note: this step is optional*
105105

106-
If You're using `monolog` for logging e.g. in-app errors, You
106+
If you're using `monolog` for logging e.g. in-app errors, you
107107
can use this handler in order for them to show up in Sentry.
108108

109-
First, define `Sentry\Monolog\Handler` as a service in `config/services.yaml`
109+
First, enable & configure the `Sentry\Monolog\Handler`; you'll also need
110+
to disable the `Sentry\SentryBundle\EventListener\ErrorListener` to
111+
avoid having duplicate events in Sentry:
110112

111113
```yaml
112-
services:
113-
sentry.monolog.handler:
114-
class: Sentry\Monolog\Handler
115-
arguments:
116-
$level: 'error'
114+
sentry:
115+
register_error_listener: false # Disables the ErrorListener
116+
monolog:
117+
error_handler:
118+
enabled: true
119+
level: error
117120
```
118121

119122
Then enable it in `monolog` config:
@@ -123,8 +126,7 @@ monolog:
123126
handlers:
124127
sentry:
125128
type: service
126-
id: sentry.monolog.handler
127-
level: error
129+
id: Sentry\Monolog\Handler
128130
```
129131

130132
## Maintained versions

composer.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,20 +32,24 @@
3232
"require-dev": {
3333
"friendsofphp/php-cs-fixer": "^2.8",
3434
"jangregor/phpstan-prophecy": "^0.3.0",
35+
"monolog/monolog": "^1.11||^2.0",
3536
"php-http/mock-client": "^1.0",
3637
"phpstan/phpstan": "^0.11",
3738
"phpstan/phpstan-phpunit": "^0.11",
3839
"phpunit/phpunit": "^7.5",
3940
"scrutinizer/ocular": "^1.4",
4041
"symfony/expression-language": "^2.8||^3.0||^4.0"
4142
},
43+
"suggest": {
44+
"monolog/monolog": "Required to use the Monolog handler"
45+
},
4246
"autoload": {
43-
"psr-4" : {
47+
"psr-4": {
4448
"Sentry\\SentryBundle\\": "src/"
4549
}
4650
},
4751
"autoload-dev": {
48-
"psr-4" : {
52+
"psr-4": {
4953
"Sentry\\SentryBundle\\Test\\": "test"
5054
}
5155
},

src/DependencyInjection/Configuration.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,24 @@ public function getConfigTreeBuilder(): TreeBuilder
143143
$listenerPriorities->scalarNode('console_error')
144144
->defaultValue(128);
145145

146+
// Monolog handler configuration
147+
$monologConfiguration = $rootNode->children()
148+
->arrayNode('monolog')
149+
->addDefaultsIfNotSet()
150+
->children();
151+
152+
$errorHandler = $monologConfiguration
153+
->arrayNode('error_handler')
154+
->addDefaultsIfNotSet()
155+
->children();
156+
$errorHandler->booleanNode('enabled')
157+
->defaultFalse();
158+
$errorHandler->scalarNode('level')
159+
->defaultValue('DEBUG')
160+
->cannotBeEmpty();
161+
$errorHandler->booleanNode('bubble')
162+
->defaultTrue();
163+
146164
return $treeBuilder;
147165
}
148166

src/DependencyInjection/SentryExtension.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
namespace Sentry\SentryBundle\DependencyInjection;
44

5+
use Monolog\Logger as MonologLogger;
56
use Sentry\ClientBuilderInterface;
7+
use Sentry\Monolog\Handler;
68
use Sentry\Options;
79
use Sentry\SentryBundle\Command\SentryTestCommand;
810
use Sentry\SentryBundle\ErrorTypesParser;
@@ -14,6 +16,7 @@
1416
use Symfony\Component\Config\FileLocator;
1517
use Symfony\Component\Console\ConsoleEvents;
1618
use Symfony\Component\DependencyInjection\ContainerBuilder;
19+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1720
use Symfony\Component\DependencyInjection\Loader;
1821
use Symfony\Component\DependencyInjection\Reference;
1922
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -49,6 +52,7 @@ public function load(array $configs, ContainerBuilder $container): void
4952

5053
$this->configureErrorListener($container, $processedConfiguration);
5154
$this->setLegacyVisibilities($container);
55+
$this->configureMonologHandler($container, $processedConfiguration['monolog']);
5256
}
5357

5458
private function passConfigurationToOptions(ContainerBuilder $container, array $processedConfiguration): void
@@ -185,4 +189,32 @@ private function setLegacyVisibilities(ContainerBuilder $container): void
185189
}
186190
}
187191
}
192+
193+
private function configureMonologHandler(ContainerBuilder $container, array $monologConfiguration): void
194+
{
195+
$errorHandler = $monologConfiguration['error_handler'];
196+
197+
if (! $errorHandler['enabled']) {
198+
$container->removeDefinition(Handler::class);
199+
200+
return;
201+
}
202+
203+
if (! class_exists(Handler::class)) {
204+
throw new LogicException(
205+
sprintf('Missing class "%s", try updating "sentry/sentry" to a newer version.', Handler::class)
206+
);
207+
}
208+
209+
if (! class_exists(MonologLogger::class)) {
210+
throw new LogicException(
211+
sprintf('You cannot use "%s" if Monolog is not available.', Handler::class)
212+
);
213+
}
214+
215+
$container
216+
->getDefinition(Handler::class)
217+
->replaceArgument('$level', MonologLogger::toMonologLevel($errorHandler['level']))
218+
->replaceArgument('$bubble', $errorHandler['bubble']);
219+
}
188220
}

src/Resources/config/services.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,11 @@
5151
<service id="Sentry\SentryBundle\Command\SentryTestCommand" class="Sentry\SentryBundle\Command\SentryTestCommand" public="false">
5252
<tag name="console.command" />
5353
</service>
54+
55+
<service id="Sentry\Monolog\Handler" class="Sentry\Monolog\Handler" public="false">
56+
<argument type="service" id="Sentry\State\HubInterface" />
57+
<argument key="$level" />
58+
<argument key="$bubble" />
59+
</service>
5460
</services>
5561
</container>

test/DependencyInjection/ConfigurationTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,13 @@ public function testConfigurationDefaults(): void
6868
'project_root' => '%kernel.root_dir%/..',
6969
'tags' => [],
7070
],
71+
'monolog' => [
72+
'error_handler' => [
73+
'enabled' => false,
74+
'level' => 'DEBUG',
75+
'bubble' => true,
76+
],
77+
],
7178
];
7279

7380
if (method_exists(Kernel::class, 'getProjectDir')) {

test/DependencyInjection/SentryExtensionTest.php

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@
33
namespace Sentry\SentryBundle\Test\DependencyInjection;
44

55
use Jean85\PrettyVersions;
6+
use Monolog\Logger as MonologLogger;
67
use Sentry\Breadcrumb;
8+
use Sentry\ClientInterface;
79
use Sentry\Event;
810
use Sentry\Integration\IntegrationInterface;
11+
use Sentry\Monolog\Handler;
912
use Sentry\Options;
1013
use Sentry\SentryBundle\DependencyInjection\SentryExtension;
1114
use Sentry\SentryBundle\EventListener\ErrorListener;
1215
use Sentry\SentryBundle\Test\BaseTestCase;
16+
use Sentry\Severity;
17+
use Sentry\State\Scope;
1318
use Symfony\Component\DependencyInjection\Alias;
1419
use Symfony\Component\DependencyInjection\Container;
1520
use Symfony\Component\DependencyInjection\ContainerBuilder;
1621
use Symfony\Component\DependencyInjection\Definition;
22+
use Symfony\Component\DependencyInjection\Exception\LogicException;
1723
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1824
use Symfony\Component\HttpFoundation\RequestStack;
1925
use Symfony\Component\HttpKernel\Kernel;
@@ -22,6 +28,7 @@ class SentryExtensionTest extends BaseTestCase
2228
{
2329
private const OPTIONS_TEST_PUBLIC_ALIAS = 'sentry.options.public_alias';
2430
private const ERROR_LISTENER_TEST_PUBLIC_ALIAS = 'sentry.error_listener.public_alias';
31+
private const MONOLOG_HANDLER_TEST_PUBLIC_ALIAS = 'sentry.monolog_handler.public_alias';
2532

2633
public function testDataProviderIsMappingTheRightNumberOfOptions(): void
2734
{
@@ -346,7 +353,7 @@ public function testErrorListenerIsRegistered(bool $registerErrorListener): void
346353
'register_error_listener' => $registerErrorListener,
347354
]);
348355

349-
$this->assertEquals($registerErrorListener, $container->has(self::ERROR_LISTENER_TEST_PUBLIC_ALIAS));
356+
$this->assertSame($registerErrorListener, $container->has(self::ERROR_LISTENER_TEST_PUBLIC_ALIAS));
350357
}
351358

352359
public function errorListenerConfigurationProvider(): array
@@ -357,6 +364,56 @@ public function errorListenerConfigurationProvider(): array
357364
];
358365
}
359366

367+
/**
368+
* @dataProvider monologHandlerConfigurationProvider
369+
*/
370+
public function testMonologHandlerIsConfiguredProperly($level, bool $bubble, int $monologLevel): void
371+
{
372+
$this->expectExceptionIfMonologHandlerDoesNotExist();
373+
374+
$container = $this->getContainer([
375+
'monolog' => [
376+
'error_handler' => [
377+
'enabled' => true,
378+
'level' => $level,
379+
'bubble' => $bubble,
380+
],
381+
],
382+
]);
383+
384+
$this->assertTrue($container->has(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS));
385+
386+
/** @var Handler $handler */
387+
$handler = $container->get(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS);
388+
$this->assertEquals($monologLevel, $handler->getLevel());
389+
$this->assertEquals($bubble, $handler->getBubble());
390+
}
391+
392+
public function monologHandlerConfigurationProvider(): array
393+
{
394+
return [
395+
['DEBUG', true, MonologLogger::DEBUG],
396+
['debug', false, MonologLogger::DEBUG],
397+
['ERROR', true, MonologLogger::ERROR],
398+
['error', false, MonologLogger::ERROR],
399+
[MonologLogger::ALERT, true, MonologLogger::ALERT],
400+
[MonologLogger::EMERGENCY, false, MonologLogger::EMERGENCY],
401+
];
402+
}
403+
404+
public function testMonologHandlerIsNotRegistered(): void
405+
{
406+
$container = $this->getContainer([
407+
'monolog' => [
408+
'error_handler' => [
409+
'enabled' => false,
410+
],
411+
],
412+
]);
413+
414+
$this->assertFalse($container->has(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS));
415+
}
416+
360417
private function getContainer(array $configuration = []): Container
361418
{
362419
$containerBuilder = new ContainerBuilder();
@@ -385,10 +442,17 @@ private function getContainer(array $configuration = []): Container
385442
$extension = new SentryExtension();
386443
$extension->load(['sentry' => $configuration], $containerBuilder);
387444

445+
$client = new Definition(ClientMock::class);
446+
$containerBuilder->setDefinition(ClientInterface::class, $client);
447+
388448
if ($containerBuilder->hasDefinition(ErrorListener::class)) {
389449
$containerBuilder->setAlias(self::ERROR_LISTENER_TEST_PUBLIC_ALIAS, new Alias(ErrorListener::class, true));
390450
}
391451

452+
if ($containerBuilder->hasDefinition(Handler::class)) {
453+
$containerBuilder->setAlias(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS, new Alias(Handler::class, true));
454+
}
455+
392456
$containerBuilder->compile();
393457

394458
return $containerBuilder;
@@ -403,6 +467,16 @@ private function getOptionsFrom(Container $container): Options
403467

404468
return $options;
405469
}
470+
471+
private function expectExceptionIfMonologHandlerDoesNotExist(): void
472+
{
473+
if (! class_exists(Handler::class)) {
474+
$this->expectException(LogicException::class);
475+
$this->expectExceptionMessage(
476+
sprintf('Missing class "%s", try updating "sentry/sentry" to a newer version.', Handler::class)
477+
);
478+
}
479+
}
406480
}
407481

408482
function mockBeforeSend(Event $event): ?Event
@@ -439,3 +513,36 @@ public function setupOnce(): void
439513
{
440514
}
441515
}
516+
517+
class ClientMock implements ClientInterface
518+
{
519+
public function getOptions(): Options
520+
{
521+
return new Options();
522+
}
523+
524+
public function captureMessage(string $message, ?Severity $level = null, ?Scope $scope = null): ?string
525+
{
526+
return null;
527+
}
528+
529+
public function captureException(\Throwable $exception, ?Scope $scope = null): ?string
530+
{
531+
return null;
532+
}
533+
534+
public function captureLastError(?Scope $scope = null): ?string
535+
{
536+
return null;
537+
}
538+
539+
public function captureEvent(array $payload, ?Scope $scope = null): ?string
540+
{
541+
return null;
542+
}
543+
544+
public function getIntegration(string $className): ?IntegrationInterface
545+
{
546+
return null;
547+
}
548+
}

0 commit comments

Comments
 (0)