diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 7a3c9a39..fb146cce 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -187,6 +187,7 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode): void
->{interface_exists(MessageBusInterface::class) ? 'canBeDisabled' : 'canBeEnabled'}()
->children()
->booleanNode('capture_soft_fails')->defaultTrue()->end()
+ ->booleanNode('isolate_breadcrumbs_by_message')->defaultFalse()->end()
->end()
->end()
->end();
diff --git a/src/DependencyInjection/SentryExtension.php b/src/DependencyInjection/SentryExtension.php
index a127069b..44218b56 100644
--- a/src/DependencyInjection/SentryExtension.php
+++ b/src/DependencyInjection/SentryExtension.php
@@ -200,7 +200,7 @@ private function registerMessengerListenerConfiguration(ContainerBuilder $contai
return;
}
- $container->getDefinition(MessengerListener::class)->setArgument(1, $config['capture_soft_fails']);
+ $container->getDefinition(MessengerListener::class)->setArgument(1, $config['capture_soft_fails'])->setArgument(2, $config['isolate_breadcrumbs_by_message']);
}
/**
diff --git a/src/EventListener/MessengerListener.php b/src/EventListener/MessengerListener.php
index b66b962c..0e42ac01 100644
--- a/src/EventListener/MessengerListener.php
+++ b/src/EventListener/MessengerListener.php
@@ -11,6 +11,7 @@
use Sentry\State\Scope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
+use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\WrappedExceptionsInterface;
@@ -30,15 +31,25 @@ final class MessengerListener
private $captureSoftFails;
/**
- * @param HubInterface $hub The current hub
- * @param bool $captureSoftFails Whether to capture errors thrown
- * while processing a message that
- * will be retried
+ * @var bool When this is enabled, a new scope is pushed on the stack when a message
+ * is received and will pop it again after the message was finished (either success or fail).
+ * This allows us to have breadcrumbs only for one message and no breadcrumb is leaked into
+ * future messages.
*/
- public function __construct(HubInterface $hub, bool $captureSoftFails = true)
+ private $isolateBreadcrumbsByMessage;
+
+ /**
+ * @param HubInterface $hub The current hub
+ * @param bool $captureSoftFails Whether to capture errors thrown
+ * while processing a message that
+ * will be retried
+ * @param bool $isolateBreadcrumbsByMessage Whether messages should have isolated breadcrumbs
+ */
+ public function __construct(HubInterface $hub, bool $captureSoftFails = true, bool $isolateBreadcrumbsByMessage = false)
{
$this->hub = $hub;
$this->captureSoftFails = $captureSoftFails;
+ $this->isolateBreadcrumbsByMessage = $isolateBreadcrumbsByMessage;
}
/**
@@ -48,28 +59,36 @@ public function __construct(HubInterface $hub, bool $captureSoftFails = true)
*/
public function handleWorkerMessageFailedEvent(WorkerMessageFailedEvent $event): void
{
- if (!$this->captureSoftFails && $event->willRetry()) {
- return;
- }
+ try {
+ if (!$this->captureSoftFails && $event->willRetry()) {
+ return;
+ }
- $this->hub->withScope(function (Scope $scope) use ($event): void {
- $envelope = $event->getEnvelope();
- $exception = $event->getThrowable();
+ $this->hub->withScope(function (Scope $scope) use ($event): void {
+ $envelope = $event->getEnvelope();
+ $exception = $event->getThrowable();
- $scope->setTag('messenger.receiver_name', $event->getReceiverName());
- $scope->setTag('messenger.message_class', \get_class($envelope->getMessage()));
+ $scope->setTag('messenger.receiver_name', $event->getReceiverName());
+ $scope->setTag('messenger.message_class', \get_class($envelope->getMessage()));
- /** @var BusNameStamp|null $messageBusStamp */
- $messageBusStamp = $envelope->last(BusNameStamp::class);
+ /** @var BusNameStamp|null $messageBusStamp */
+ $messageBusStamp = $envelope->last(BusNameStamp::class);
- if (null !== $messageBusStamp) {
- $scope->setTag('messenger.message_bus', $messageBusStamp->getBusName());
- }
+ if (null !== $messageBusStamp) {
+ $scope->setTag('messenger.message_bus', $messageBusStamp->getBusName());
+ }
- $this->captureException($exception, $event->willRetry());
- });
+ $this->captureException($exception, $event->willRetry());
+ });
- $this->flushClient();
+ $this->flushClient();
+ } finally {
+ // We always want to pop the scope at the end of this method to add the breadcrumbs
+ // to any potential event that is produced.
+ if ($this->isolateBreadcrumbsByMessage) {
+ $this->hub->popScope();
+ }
+ }
}
/**
@@ -82,6 +101,24 @@ public function handleWorkerMessageHandledEvent(WorkerMessageHandledEvent $event
// Flush normally happens at shutdown... which only happens in the worker if it is run with a lifecycle limit
// such as --time=X or --limit=Y. Flush immediately in a background worker.
$this->flushClient();
+ if ($this->isolateBreadcrumbsByMessage) {
+ $this->hub->popScope();
+ }
+ }
+
+ /**
+ * Method that will push a new scope on the hub to create message local breadcrumbs that will not
+ * "leak" into future messages.
+ *
+ * @param WorkerMessageReceivedEvent $event
+ *
+ * @return void
+ */
+ public function handleWorkerMessageReceivedEvent(WorkerMessageReceivedEvent $event): void
+ {
+ if ($this->isolateBreadcrumbsByMessage) {
+ $this->hub->pushScope();
+ }
}
/**
diff --git a/src/Resources/config/schema/sentry-1.0.xsd b/src/Resources/config/schema/sentry-1.0.xsd
index d8c81294..5d03008b 100644
--- a/src/Resources/config/schema/sentry-1.0.xsd
+++ b/src/Resources/config/schema/sentry-1.0.xsd
@@ -101,6 +101,7 @@
+
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index e4b916df..dae3e7df 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -76,6 +76,7 @@
+
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index c993f6f2..e9aed67e 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -42,6 +42,7 @@ public function testProcessConfigurationWithDefaultConfiguration(): void
'messenger' => [
'enabled' => interface_exists(MessageBusInterface::class),
'capture_soft_fails' => true,
+ 'isolate_breadcrumbs_by_message' => false,
],
'tracing' => [
'enabled' => true,
diff --git a/tests/DependencyInjection/Fixtures/php/full.php b/tests/DependencyInjection/Fixtures/php/full.php
index 973b125c..6cd2c1aa 100644
--- a/tests/DependencyInjection/Fixtures/php/full.php
+++ b/tests/DependencyInjection/Fixtures/php/full.php
@@ -60,6 +60,7 @@
'messenger' => [
'enabled' => true,
'capture_soft_fails' => false,
+ 'isolate_breadcrumbs_by_message' => true,
],
'tracing' => [
'dbal' => [
diff --git a/tests/DependencyInjection/Fixtures/xml/full.xml b/tests/DependencyInjection/Fixtures/xml/full.xml
index ddf466a2..9615eb2e 100644
--- a/tests/DependencyInjection/Fixtures/xml/full.xml
+++ b/tests/DependencyInjection/Fixtures/xml/full.xml
@@ -57,7 +57,7 @@
Symfony\Component\HttpKernel\Exception\BadRequestHttpException
GET tracing_ignored_transaction
-
+
default
diff --git a/tests/DependencyInjection/Fixtures/yml/full.yml b/tests/DependencyInjection/Fixtures/yml/full.yml
index e9097964..760b398e 100644
--- a/tests/DependencyInjection/Fixtures/yml/full.yml
+++ b/tests/DependencyInjection/Fixtures/yml/full.yml
@@ -59,6 +59,7 @@ sentry:
messenger:
enabled: true
capture_soft_fails: false
+ isolate_breadcrumbs_by_message: true
tracing:
dbal:
enabled: false
diff --git a/tests/DependencyInjection/SentryExtensionTest.php b/tests/DependencyInjection/SentryExtensionTest.php
index ce7e4234..0d64d77b 100644
--- a/tests/DependencyInjection/SentryExtensionTest.php
+++ b/tests/DependencyInjection/SentryExtensionTest.php
@@ -41,6 +41,7 @@
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
+use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\MessageBusInterface;
abstract class SentryExtensionTest extends TestCase
@@ -138,10 +139,16 @@ public function testMessengerListener(): void
'method' => 'handleWorkerMessageHandledEvent',
'priority' => 50,
],
+ [
+ 'event' => WorkerMessageReceivedEvent::class,
+ 'method' => 'handleWorkerMessageReceivedEvent',
+ 'priority' => 50,
+ ],
],
], $definition->getTags());
$this->assertFalse($definition->getArgument(1));
+ $this->assertTrue($definition->getArgument(2));
}
public function testMessengerListenerIsRemovedWhenDisabled(): void
diff --git a/tests/End2End/App/Controller/MessengerController.php b/tests/End2End/App/Controller/MessengerController.php
index 46afffec..9c7f5626 100644
--- a/tests/End2End/App/Controller/MessengerController.php
+++ b/tests/End2End/App/Controller/MessengerController.php
@@ -4,6 +4,7 @@
namespace Sentry\SentryBundle\Tests\End2End\App\Controller;
+use Psr\Log\LoggerInterface;
use Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessage;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -15,9 +16,15 @@ class MessengerController
*/
private $messenger;
- public function __construct(MessageBusInterface $messenger)
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(MessageBusInterface $messenger, LoggerInterface $logger)
{
$this->messenger = $messenger;
+ $this->logger = $logger;
}
public function dispatchMessage(): Response
@@ -29,6 +36,7 @@ public function dispatchMessage(): Response
public function dispatchUnrecoverableMessage(): Response
{
+ $this->logger->warning('Dispatch FooMessage');
$this->messenger->dispatch(new FooMessage(false));
return new Response('Success');
diff --git a/tests/End2End/App/Messenger/FooMessageHandler.php b/tests/End2End/App/Messenger/FooMessageHandler.php
index 28518d96..2e0db467 100644
--- a/tests/End2End/App/Messenger/FooMessageHandler.php
+++ b/tests/End2End/App/Messenger/FooMessageHandler.php
@@ -4,14 +4,27 @@
namespace Sentry\SentryBundle\Tests\End2End\App\Messenger;
+use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Exception\UnrecoverableExceptionInterface;
class FooMessageHandler
{
+ /**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ public function __construct(LoggerInterface $logger)
+ {
+ $this->logger = $logger;
+ }
+
public function __invoke(FooMessage $message): void
{
+ $this->logger->warning('Handle FooMessage');
if (!$message->shouldRetry()) {
- throw new class extends \Exception implements UnrecoverableExceptionInterface { };
+ throw new class extends \Exception implements UnrecoverableExceptionInterface {
+ };
}
throw new \Exception('This is an intentional failure while handling a message of class ' . \get_class($message));
diff --git a/tests/End2End/App/messenger.yml b/tests/End2End/App/messenger.yml
index c729467d..a4b91825 100644
--- a/tests/End2End/App/messenger.yml
+++ b/tests/End2End/App/messenger.yml
@@ -6,6 +6,7 @@ services:
class: \Sentry\SentryBundle\Tests\End2End\App\Messenger\StaticInMemoryTransportFactory
Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessageHandler:
+ autowire: true
class: \Sentry\SentryBundle\Tests\End2End\App\Messenger\FooMessageHandler
tags:
- { name: messenger.message_handler }
@@ -15,6 +16,18 @@ services:
tags:
- controller.service_arguments
+ Sentry\Monolog\BreadcrumbHandler:
+ arguments:
+ - '@Sentry\State\HubInterface'
+ - !php/const Monolog\Logger::WARNING
+
+monolog:
+ handlers:
+ sentry_breadcrumbs:
+ type: service
+ name: sentry_breadcrumbs
+ id: Sentry\Monolog\BreadcrumbHandler
+
framework:
messenger:
transports:
@@ -28,3 +41,4 @@ framework:
sentry:
messenger:
capture_soft_fails: false
+ isolate_breadcrumbs_by_message: true
diff --git a/tests/End2End/End2EndTest.php b/tests/End2End/End2EndTest.php
index 19ddfe06..d346dcca 100644
--- a/tests/End2End/End2EndTest.php
+++ b/tests/End2End/End2EndTest.php
@@ -38,6 +38,8 @@ protected function setUp(): void
{
parent::setUp();
+ StubTransport::$events = [];
+
file_put_contents(self::SENT_EVENTS_LOG, '');
}
@@ -250,6 +252,30 @@ public function testMessengerCaptureSoftFailCanBeDisabled(): void
$this->assertLastEventIdIsNull($client);
}
+ public function testIsolateBreadcrumbsByMessage(): void
+ {
+ $this->skipIfMessengerIsMissing();
+
+ $client = static::createClient();
+
+ // Create two messages
+ $client->request('GET', '/dispatch-unrecoverable-message');
+ $this->assertSame(200, $client->getResponse()->getStatusCode());
+
+ $client->request('GET', '/dispatch-unrecoverable-message');
+ $this->assertSame(200, $client->getResponse()->getStatusCode());
+
+ $this->consumeOneMessage($client->getKernel());
+ $this->consumeOneMessage($client->getKernel());
+
+ $events = StubTransport::$events;
+ $this->assertCount(2, $events);
+
+ // Asser that both have the same number of breadcrumbs meaning that each event has isolated breadcrumbs
+ $this->assertCount(4, $events[0]->getBreadcrumbs());
+ $this->assertCount(4, $events[1]->getBreadcrumbs());
+ }
+
private function consumeOneMessage(KernelInterface $kernel): void
{
$application = new Application($kernel);
diff --git a/tests/EventListener/MessengerListenerTest.php b/tests/EventListener/MessengerListenerTest.php
index 8e850aaf..8a38e75c 100644
--- a/tests/EventListener/MessengerListenerTest.php
+++ b/tests/EventListener/MessengerListenerTest.php
@@ -15,6 +15,7 @@
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
+use Symfony\Component\Messenger\Event\WorkerMessageReceivedEvent;
use Symfony\Component\Messenger\Exception\DelayedMessageHandlingException;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\MessageBusInterface;
@@ -116,7 +117,7 @@ public function handleWorkerMessageFailedEventDataProvider(): \Generator
yield 'envelope.throwable INSTANCEOF DelayedMessageHandlingException' => [
$exceptions,
- $this->getMessageFailedEvent($envelope, 'receiver', new DelayedMessageHandlingException($exceptions, $envelope), false),
+ $this->getMessageFailedEvent($envelope, 'receiver', new DelayedMessageHandlingException($exceptions), false),
[
'messenger.receiver_name' => 'receiver',
'messenger.message_class' => \get_class($envelope->getMessage()),
@@ -238,6 +239,77 @@ public function testHandleWorkerMessageHandledEvent(): void
$listener->handleWorkerMessageHandledEvent(new WorkerMessageHandledEvent(Envelope::wrap((object) []), 'receiver'));
}
+ public function testIsolateBreadcrumbsByMessagePushAndPopScopeWhenEnabled(): void
+ {
+ if (!$this->supportsMessenger()) {
+ $this->markTestSkipped('Messenger not supported in this environment.');
+ }
+
+ // Received event should push
+ $this->hub->expects($this->once())
+ ->method('pushScope');
+
+ // Handled event should pop
+ $this->hub->expects($this->once())
+ ->method('popScope');
+
+ $listener = new MessengerListener($this->hub, true, true);
+
+ $envelope = Envelope::wrap((object) []);
+ $listener->handleWorkerMessageReceivedEvent(new WorkerMessageReceivedEvent($envelope, 'receiver'));
+ $listener->handleWorkerMessageHandledEvent(new WorkerMessageHandledEvent($envelope, 'receiver'));
+ }
+
+ public function testIsolateBreadcrumbsByMessageDoesNotPushOrPopWhenDisabled(): void
+ {
+ if (!$this->supportsMessenger()) {
+ $this->markTestSkipped('Messenger not supported in this environment.');
+ }
+
+ $this->hub->expects($this->never())
+ ->method('pushScope');
+
+ $this->hub->expects($this->never())
+ ->method('popScope');
+
+ $listener = new MessengerListener($this->hub, true, false);
+
+ $envelope = Envelope::wrap((object) []);
+ $listener->handleWorkerMessageReceivedEvent(new WorkerMessageReceivedEvent($envelope, 'receiver'));
+ $listener->handleWorkerMessageHandledEvent(new WorkerMessageHandledEvent($envelope, 'receiver'));
+ }
+
+ public function testIsolateBreadcrumbsByMessagePopsAfterFailureWhenEnabled(): void
+ {
+ if (!$this->supportsMessenger()) {
+ $this->markTestSkipped('Messenger not supported in this environment.');
+ }
+
+ $this->hub->expects($this->once())
+ ->method('pushScope');
+
+ $this->hub->expects($this->once())
+ ->method('popScope');
+
+ $this->hub->expects($this->once())
+ ->method('withScope')
+ ->willReturnCallback(function (callable $callback): void {
+ $callback(new Scope());
+ });
+
+ $this->hub->expects($this->any())
+ ->method('getClient')
+ ->willReturn($this->client);
+
+ $listener = new MessengerListener($this->hub, true, true);
+ $envelope = Envelope::wrap((object) []);
+
+ $listener->handleWorkerMessageReceivedEvent(new WorkerMessageReceivedEvent($envelope, 'receiver'));
+
+ $event = $this->getMessageFailedEvent($envelope, 'receiver', new \Exception('boom'), false);
+ $listener->handleWorkerMessageFailedEvent($event);
+ }
+
private function getMessageFailedEvent(Envelope $envelope, string $receiverName, \Throwable $error, bool $retry): WorkerMessageFailedEvent
{
$event = new WorkerMessageFailedEvent($envelope, $receiverName, $error);