Skip to content

Commit f587b90

Browse files
emarrefJean85
andauthored
Capture messenger exceptions (#326)
* Add messenger to dev requirements * Add subscriber to capture messages thrown in worker * Add flushable client interface alias * Add listener service and configuration * Remove messenger listener if messenger isn't used * Support messenger >4.3 when event was introduced * Use worker_error timeout instead of handler_error * Add worker_error to ConfigurationTest * Remove messenger dev dependency * Flush client on message handled event * Capture messenger soft fails by default * Revert "Remove messenger dev dependency" * Add changelog entry * Make listener final for consistency * Flush client on failed message * Add unit test for MessengerListener * Test HandlerFailedException is unwrapped * Support for failed message event bc break * User _SERVER in test * Check for retry method * Keep messenger version * Move messenger config option up a level * Add messenger config to root child * Get messenger option from the correct place * Call setForRetry only in builder method * Rename messenger soft fails parameter * Default capture soft fails to true * Remove capture soft fails from parameters * Expand messenger configuration * Prophecy 1.7 compatibility * Remove messenger from 3.4 install * Skip tests when no messenger available * Dynamic configuration test for messenger * Run CS fixer * Pass configuration as argument Co-Authored-By: Alessandro Lai <[email protected]> * PHPStan tidy * Do not mark for a specific release yet * Add thank you note * Add docs to the readme Co-authored-by: Alessandro Lai <[email protected]>
1 parent f5a0918 commit f587b90

File tree

12 files changed

+312
-10
lines changed

12 files changed

+312
-10
lines changed

.travis.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,23 +14,27 @@ cache:
1414
before_install:
1515
- |
1616
if [ "$SYMFONY_VERSION" != "" ]; then
17-
sed -ri '/symfony\/monolog-bundle/! s/"symfony\/(.+)": "(.+)"/"symfony\/\1": "'$SYMFONY_VERSION'"/' composer.json;
17+
sed -ri '/symfony\/(monolog-bundle|messenger)/! s/"symfony\/(.+)": "(.+)"/"symfony\/\1": "'$SYMFONY_VERSION'"/' composer.json;
18+
fi;
19+
- |
20+
if [ "$SYMFONY_VERSION" == "3.4.*" ]; then
21+
composer remove --dev symfony/messenger
1822
fi;
1923
- composer self-update
2024
- composer global require hirak/prestissimo
2125

22-
install:
26+
install:
2327
- travis_retry travis_wait composer update --no-interaction --prefer-dist --prefer-stable
2428

2529
script: >-
26-
vendor/bin/phpunit -v --coverage-clover=build/coverage-report.xml
30+
vendor/bin/phpunit -v --coverage-clover=build/coverage-report.xml
2731
&& bash <(curl -s https://codecov.io/bash) -f build/coverage-report.xml
2832
2933
jobs:
3034
include:
3135
- stage: Test
3236
php: 7.1
33-
env:
37+
env:
3438
- SYMFONY_VERSION: 3.4.*
3539
- SYMFONY_DEPRECATIONS_HELPER: disabled
3640
- php: 7.3
@@ -48,7 +52,7 @@ jobs:
4852
name: PHPStan
4953
script:
5054
- composer phpstan
51-
- script:
55+
- script:
5256
- composer cs-check
5357
name: PHP-CS-Fixer
5458
allow_failures:

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
55
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
66

77
## Unreleased
8+
- Capture and flush messages in a Messenger Worker context (#326, thanks to @emarref)
89
- ...
910

1011
## 3.4.4 (2020-03-16)

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ Keep in mind that leaving the `dsn` value empty (or undeclared) in other environ
8080
```yaml
8181
sentry:
8282
dsn: "https://public:[email protected]/1"
83+
messenger:
84+
enabled: true # flushes Sentry messages at the end of each message handling
85+
capture_soft_fails: true # captures exceptions marked for retry too
8386
options:
8487
environment: '%kernel.environment%'
8588
release: '%env(VERSION)%' #your app version
@@ -171,7 +174,7 @@ The 3.0 version of the bundle uses the newest version (2.x) of the underlying Se
171174

172175
The Sentry 2.0 SDK uses the Unified API, hence it uses the concept of `Scope`s to hold information about the current
173176
state of the app, and attach it to any event that is reported. This bundle has three listeners (`RequestListener`,
174-
`SubRequestListener` and `ConsoleListener`) that adds some easy default information.
177+
`SubRequestListener` and `ConsoleListener`) that adds some easy default information. Since 3.5, a fourth listener has been added to handle the case of Messanger Workers: `MessengerListener`.
175178

176179
Those listeners normally are executed with a priority of `1` to allow easier customization with custom listener, that by
177180
default run with a lower priority of `0`.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,13 @@
3434
"jangregor/phpstan-prophecy": "^0.3.0",
3535
"monolog/monolog": "^1.11||^2.0",
3636
"php-http/mock-client": "^1.0",
37-
"phpstan/phpstan-shim": "^0.11",
3837
"phpstan/phpstan-phpunit": "^0.11",
38+
"phpstan/phpstan-shim": "^0.11",
3939
"phpunit/phpunit": "^7.5||^8.5",
4040
"symfony/browser-kit": "^3.4||^4.0||^5.0",
4141
"symfony/expression-language": "^3.4||^4.0||^5.0",
4242
"symfony/framework-bundle": "^3.4||^4.0||^5.0",
43+
"symfony/messenger": "^4.3||^5.0",
4344
"symfony/monolog-bundle": "^3.4",
4445
"symfony/phpunit-bridge": "^5.0",
4546
"symfony/yaml": "^3.4||^4.0||^5.0"

phpstan.neon

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ parameters:
88
- "/Call to function method_exists.. with 'Symfony.+' and 'getThrowable' will always evaluate to false./"
99
- '/Class PHPUnit_Framework_TestCase not found/'
1010
- '/Symfony\\Component\\HttpKernel\\Event\\(GetResponse|FilterController)Event not found.$/'
11-
-
11+
-
1212
message: '/Symfony\\Bundle\\FrameworkBundle\\Client/'
1313
path: test/End2End/End2EndTest.php
14-
-
14+
-
1515
message: '/^Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder::root...$/'
1616
path: src/DependencyInjection/Configuration.php
1717
-
@@ -23,6 +23,9 @@ parameters:
2323
-
2424
message: '/Sentry\\SentryBundle\\EventListener\\SubRequestListenerRequestEvent( not found)?.$/'
2525
path: src/EventListener/SubRequestListener.php
26+
-
27+
message: '/Class Symfony\\Component\\Messenger\\Event\\WorkerMessage[a-zA-Z]*Event constructor invoked with [0-9]+ parameters, [0-9]+ required\.$/'
28+
path: test/EventListener/MessengerListenerTest.php
2629

2730
includes:
2831
- vendor/jangregor/phpstan-prophecy/src/extension.neon

src/DependencyInjection/Configuration.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
99
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
1010
use Symfony\Component\Config\Definition\ConfigurationInterface;
11+
use Symfony\Component\Messenger\MessageBusInterface;
1112

1213
/**
1314
* This is the class that validates and merges configuration from your app/config files
@@ -40,7 +41,6 @@ public function getConfigTreeBuilder(): TreeBuilder
4041
$rootNode->children()
4142
->booleanNode('register_error_listener')
4243
->defaultTrue();
43-
4444
// Options array (to be passed to Sentry\Options constructor) -- please keep alphabetical order!
4545
$optionsNode = $rootNode->children()
4646
->arrayNode('options')
@@ -141,6 +141,8 @@ public function getConfigTreeBuilder(): TreeBuilder
141141
->defaultValue(128);
142142
$listenerPriorities->scalarNode('console_error')
143143
->defaultValue(128);
144+
$listenerPriorities->scalarNode('worker_error')
145+
->defaultValue(128);
144146

145147
// Monolog handler configuration
146148
$monologConfiguration = $rootNode->children()
@@ -160,6 +162,17 @@ public function getConfigTreeBuilder(): TreeBuilder
160162
$errorHandler->booleanNode('bubble')
161163
->defaultTrue();
162164

165+
// Messenger configuration
166+
$messengerConfiguration = $rootNode->children()
167+
->arrayNode('messenger')
168+
->addDefaultsIfNotSet()
169+
->children();
170+
171+
$messengerConfiguration->booleanNode('enabled')
172+
->defaultValue(interface_exists(MessageBusInterface::class));
173+
$messengerConfiguration->booleanNode('capture_soft_fails')
174+
->defaultTrue();
175+
163176
return $treeBuilder;
164177
}
165178

src/DependencyInjection/SentryExtension.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Sentry\Options;
1010
use Sentry\SentryBundle\ErrorTypesParser;
1111
use Sentry\SentryBundle\EventListener\ErrorListener;
12+
use Sentry\SentryBundle\EventListener\MessengerListener;
1213
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1314
use Symfony\Component\Config\FileLocator;
1415
use Symfony\Component\DependencyInjection\ContainerBuilder;
@@ -49,6 +50,7 @@ public function load(array $configs, ContainerBuilder $container): void
4950
}
5051

5152
$this->configureErrorListener($container, $processedConfiguration);
53+
$this->configureMessengerListener($container, $processedConfiguration['messenger']);
5254
$this->configureMonologHandler($container, $processedConfiguration['monolog']);
5355
}
5456

@@ -160,6 +162,17 @@ private function configureErrorListener(ContainerBuilder $container, array $proc
160162
$this->tagExceptionListener($container);
161163
}
162164

165+
private function configureMessengerListener(ContainerBuilder $container, array $processedConfiguration): void
166+
{
167+
if (! $processedConfiguration['enabled']) {
168+
$container->removeDefinition(MessengerListener::class);
169+
170+
return;
171+
}
172+
173+
$container->getDefinition(MessengerListener::class)->setArgument(1, $processedConfiguration['capture_soft_fails']);
174+
}
175+
163176
/**
164177
* BC layer for Symfony < 4.3
165178
*/
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Sentry\SentryBundle\EventListener;
4+
5+
use Sentry\FlushableClientInterface;
6+
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
7+
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
8+
use Symfony\Component\Messenger\Exception\HandlerFailedException;
9+
10+
final class MessengerListener
11+
{
12+
/**
13+
* @var FlushableClientInterface
14+
*/
15+
private $client;
16+
17+
/**
18+
* @var bool
19+
*/
20+
private $captureSoftFails;
21+
22+
/**
23+
* @param FlushableClientInterface $client
24+
* @param bool $captureSoftFails
25+
*/
26+
public function __construct(FlushableClientInterface $client, bool $captureSoftFails = true)
27+
{
28+
$this->client = $client;
29+
$this->captureSoftFails = $captureSoftFails;
30+
}
31+
32+
/**
33+
* @param WorkerMessageFailedEvent $event
34+
*/
35+
public function onWorkerMessageFailed(WorkerMessageFailedEvent $event): void
36+
{
37+
if (! $this->captureSoftFails && $event->willRetry()) {
38+
// Don't capture soft fails. I.e. those that will be scheduled for retry.
39+
return;
40+
}
41+
42+
$error = $event->getThrowable();
43+
44+
if ($error instanceof HandlerFailedException && null !== $error->getPrevious()) {
45+
// Unwrap the messenger exception to get the original error
46+
$error = $error->getPrevious();
47+
}
48+
49+
$this->client->captureException($error);
50+
$this->client->flush();
51+
}
52+
53+
/**
54+
* @param WorkerMessageHandledEvent $event
55+
*/
56+
public function onWorkerMessageHandled(WorkerMessageHandledEvent $event): void
57+
{
58+
// Flush normally happens at shutdown... which only happens in the worker if it is run with a lifecycle limit
59+
// such as --time=X or --limit=Y. Flush immediately in a background worker.
60+
$this->client->flush();
61+
}
62+
}

src/Resources/config/services.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<factory service="Sentry\ClientBuilderInterface" method="getClient" />
1616
</service>
1717

18+
<service id="Sentry\FlushableClientInterface" alias="Sentry\ClientInterface" public="false"/>
19+
1820
<service id="Sentry\State\HubInterface" class="Sentry\State\Hub" public="false">
1921
<factory class="Sentry\SentryBundle\SentryBundle" method="getCurrentHub" />
2022
<call method="bindClient">
@@ -50,6 +52,12 @@
5052
<tag name="kernel.event_listener" event="kernel.finish_request" method="onKernelFinishRequest" priority="%sentry.listener_priorities.sub_request%" />
5153
</service>
5254

55+
<service id="Sentry\SentryBundle\EventListener\MessengerListener" class="Sentry\SentryBundle\EventListener\MessengerListener" public="false">
56+
<argument type="service" id="Sentry\FlushableClientInterface" />
57+
<tag name="kernel.event_listener" event="Symfony\Component\Messenger\Event\WorkerMessageFailedEvent" method="onWorkerMessageFailed" priority="%sentry.listener_priorities.worker_error%" />
58+
<tag name="kernel.event_listener" event="Symfony\Component\Messenger\Event\WorkerMessageHandledEvent" method="onWorkerMessageHandled" priority="%sentry.listener_priorities.worker_error%" />
59+
</service>
60+
5361
<service id="Sentry\SentryBundle\Command\SentryTestCommand" class="Sentry\SentryBundle\Command\SentryTestCommand" public="false">
5462
<tag name="console.command" />
5563
</service>

test/DependencyInjection/ConfigurationTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Sentry\SentryBundle\Test\BaseTestCase;
99
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
1010
use Symfony\Component\Config\Definition\Processor;
11+
use Symfony\Component\Messenger\MessageBusInterface;
1112

1213
class ConfigurationTest extends BaseTestCase
1314
{
@@ -50,6 +51,7 @@ public function testConfigurationDefaults(): void
5051
'console' => 1,
5152
'request_error' => 128,
5253
'console_error' => 128,
54+
'worker_error' => 128,
5355
],
5456
'options' => [
5557
'class_serializers' => [],
@@ -72,6 +74,10 @@ public function testConfigurationDefaults(): void
7274
'bubble' => true,
7375
],
7476
],
77+
'messenger' => [
78+
'enabled' => interface_exists(MessageBusInterface::class),
79+
'capture_soft_fails' => true,
80+
],
7581
];
7682

7783
$this->assertEquals($expectedDefaults, $processed);

test/DependencyInjection/SentryExtensionTest.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Sentry\Options;
1616
use Sentry\SentryBundle\DependencyInjection\SentryExtension;
1717
use Sentry\SentryBundle\EventListener\ErrorListener;
18+
use Sentry\SentryBundle\EventListener\MessengerListener;
1819
use Sentry\SentryBundle\Test\BaseTestCase;
1920
use Sentry\SentrySdk;
2021
use Sentry\Severity;
@@ -423,6 +424,17 @@ public function testMonologHandlerIsNotRegistered(): void
423424
$this->assertFalse($container->has(self::MONOLOG_HANDLER_TEST_PUBLIC_ALIAS));
424425
}
425426

427+
public function testMessengerHandlerIsNotRegistered(): void
428+
{
429+
$container = $this->getContainer([
430+
'messenger' => [
431+
'enabled' => false,
432+
],
433+
]);
434+
435+
$this->assertFalse($container->has(MessengerListener::class));
436+
}
437+
426438
private function getContainer(array $configuration = []): Container
427439
{
428440
$containerBuilder = new ContainerBuilder();

0 commit comments

Comments
 (0)