From 06b69cd47f56080c17e0e62cf15f59526e0cd9de Mon Sep 17 00:00:00 2001 From: Kevin Pfeifer Date: Sun, 28 Dec 2025 15:54:40 +0100 Subject: [PATCH] add generic queue tracing feature --- README.md | 40 ++++++++++++ phpstan.neon | 2 + src/CakeSentryPlugin.php | 11 ++++ src/Event/QueueEventListener.php | 107 +++++++++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 src/Event/QueueEventListener.php diff --git a/README.md b/README.md index 12d01b3..10433b9 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,46 @@ Finally, you have to enable the `enable_logs` flag in the Sentry SDK as well via ], ``` +### Queue Integration (optional) + +To get queue insights working, your application and/or queue plugin needs to dispatch events according to the following structure: + +```php +// When a job is being enqueued +$this->dispatchEvent('CakeSentry.Queue.enqueue', [ + 'class' => '\App\Job\ExampleJob', // optional, but recommended + 'id' => 'unique-job-id', // optional, but recommended + 'queue' => 'some-queue-name', // optional, defaults to 'default' + 'data' => ['some' => 'data'], // optional, defaults to [] +]); + +// When a job starts processing +$this->dispatchEvent('CakeSentry.Queue.beforeExecute', [ + 'class' => '\App\Job\ExampleJob', // optional, but recommended + 'sentry_trace' => '', // optional + 'sentry_baggage' => '', // optional +]); + +// When a job has been processed successfully +$this->dispatchEvent('CakeSentry.Queue.afterExecute', [ + 'id' => 'unique-job-id', // optional, but recommended + 'queue' => 'some-queue-name', // optional, defaults to 'default' + 'data' => ['some' => 'data'], // optional, defaults to [] + 'execution_time' => 123, // optional, in milliseconds + 'retry_count' => 0, // optional +]); + +// When a job has failed during processing +$this->dispatchEvent('CakeSentry.Queue.afterExecute', [ + 'id' => 'unique-job-id', // optional, but recommended + 'queue' => 'some-queue-name', // optional, defaults to 'default' + 'data' => ['some' => 'data'], // optional, defaults to [] + 'execution_time' => 123, // optional, in milliseconds + 'retry_count' => 0, // optional + 'exception' => $exception, // required, the exception that was thrown +]); +``` + ## Upgrade from 2 to 3 There are a few major changes from 2.0 to 3.0 diff --git a/phpstan.neon b/phpstan.neon index 71a7caa..2d4999c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -7,3 +7,5 @@ parameters: ignoreErrors: - identifier: missingType.iterableValue + - + identifier: missingType.generics diff --git a/src/CakeSentryPlugin.php b/src/CakeSentryPlugin.php index f2655f3..8c0209a 100644 --- a/src/CakeSentryPlugin.php +++ b/src/CakeSentryPlugin.php @@ -5,7 +5,9 @@ use Cake\Core\BasePlugin; use Cake\Core\Configure; +use Cake\Event\EventManagerInterface; use Cake\Http\MiddlewareQueue; +use CakeSentry\Event\QueueEventListener; use CakeSentry\Middleware\CakeSentryPerformanceMiddleware; use CakeSentry\Middleware\CakeSentryQueryMiddleware; @@ -29,4 +31,13 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue return $middlewareQueue; } + + /** + * @param \Cake\Event\EventManagerInterface $eventManager + * @return \Cake\Event\EventManagerInterface + */ + public function events(EventManagerInterface $eventManager): EventManagerInterface + { + return $eventManager->on(new QueueEventListener()); + } } diff --git a/src/Event/QueueEventListener.php b/src/Event/QueueEventListener.php new file mode 100644 index 0000000..4dc15e5 --- /dev/null +++ b/src/Event/QueueEventListener.php @@ -0,0 +1,107 @@ + 'handleEnqueue', + 'CakeSentry.Queue.beforeExecute' => 'handleBeforeExecute', + 'CakeSentry.Queue.afterExecute' => 'handleAfterExecute', + ]; + } + + /** + * @param \Cake\Event\Event $event + * @return void + */ + public function handleEnqueue(Event $event): void + { + $parentSpan = SentrySdk::getCurrentHub()->getSpan(); + $jobData = $event->getData(); + $jobClass = $jobData['class'] ?? 'Unknown'; + + if ($parentSpan === null) { + return; + } + + $context = SpanContext::make()->setOp('queue.publish'); + $span = $parentSpan->startChild($context); + SentrySdk::getCurrentHub()->setSpan($span); + + $span + ->setDescription(sprintf('queue.publish %s', $jobClass)) + ->setData([ + 'messaging.message.id' => $jobData['id'] ?? null, + 'messaging.destination.name' => $jobData['queue'] ?? 'default', + 'messaging.message.body.size' => strlen(json_encode($jobData['data'] ?? []) ?: ''), + ]) + ->finish(); + + SentrySdk::getCurrentHub()->setSpan($parentSpan); + } + + /** + * @param \Cake\Event\Event $event + * @return void + */ + public function handleBeforeExecute(Event $event): void + { + $jobData = $event->getData(); + $jobClass = $jobData['class'] ?? 'Unknown'; + + $context = continueTrace( + $jobData['sentry_trace'] ?? '', + $jobData['sentry_baggage'] ?? '', + ) + ->setOp('queue.process') + ->setName($jobClass); + + $this->consumerTransaction = startTransaction($context); + SentrySdk::getCurrentHub()->setSpan($this->consumerTransaction); + } + + /** + * @param \Cake\Event\Event $event + * @return void + */ + public function handleAfterExecute(Event $event): void + { + $jobData = $event->getData(); + $result = $event->getResult(); + + if ($this->consumerTransaction === null) { + return; + } + + $success = $result !== false && !isset($jobData['exception']); + + $this->consumerTransaction + ->setData([ + 'messaging.message.id' => $jobData['id'] ?? null, + 'messaging.destination.name' => $jobData['queue'] ?? 'default', + 'messaging.message.body.size' => strlen(json_encode($jobData['data'] ?? []) ?: ''), + 'messaging.message.receive.latency' => $jobData['execution_time'] ?? 0, + 'messaging.message.retry.count' => $jobData['retry_count'] ?? 0, + ]) + ->setStatus($success ? SpanStatus::ok() : SpanStatus::internalError()) + ->finish(); + } +}