From c3808bdf63abb64fdffde443b2b25f5f392b4667 Mon Sep 17 00:00:00 2001 From: Alex Bouma Date: Wed, 22 Jan 2025 17:55:47 +0100 Subject: [PATCH] Add Scheduled Task Tracing (#968) Co-authored-by: Michael Hoffmann --- .../Features/ConsoleSchedulingIntegration.php | 62 ++++++++++++++++++- .../ConsoleSchedulingIntegrationTest.php | 53 ++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php b/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php index e359c941..76e10b0f 100644 --- a/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php +++ b/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php @@ -4,20 +4,30 @@ use DateTimeZone; use Illuminate\Console\Application as ConsoleApplication; +use Illuminate\Console\Events\ScheduledTaskFailed; +use Illuminate\Console\Events\ScheduledTaskFinished; +use Illuminate\Console\Events\ScheduledTaskStarting; use Illuminate\Console\Scheduling\Event as SchedulingEvent; use Illuminate\Contracts\Cache\Factory as Cache; use Illuminate\Contracts\Cache\Repository; +use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Str; use RuntimeException; use Sentry\CheckIn; use Sentry\CheckInStatus; use Sentry\Event as SentryEvent; +use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans; use Sentry\MonitorConfig; use Sentry\MonitorSchedule; use Sentry\SentrySdk; +use Sentry\Tracing\SpanStatus; +use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; class ConsoleSchedulingIntegration extends Feature { + use TracksPushedScopesAndSpans; + /** * @var string|null */ @@ -105,9 +115,13 @@ public function isApplicable(): bool return true; } - public function onBoot(): void + public function onBoot(Dispatcher $events): void { $this->shouldHandleCheckIn = true; + + $events->listen(ScheduledTaskStarting::class, [$this, 'handleScheduledTaskStarting']); + $events->listen(ScheduledTaskFinished::class, [$this, 'handleScheduledTaskFinished']); + $events->listen(ScheduledTaskFailed::class, [$this, 'handleScheduledTaskFailed']); } public function onBootInactive(): void @@ -120,6 +134,40 @@ public function useCacheStore(?string $name): void $this->cacheStore = $name; } + public function handleScheduledTaskStarting(ScheduledTaskStarting $event): void + { + if (!$event->task) { + return; + } + + // When scheduling a command class the command name will be the most descriptive + // When a job is scheduled the command name is `null` and the job class name (or display name) is set as the description + // When a closure is scheduled both the command name and description are `null` + $name = $this->getCommandNameForScheduled($event->task) ?? $event->task->description ?? 'Closure'; + + $context = TransactionContext::make() + ->setName($name) + ->setSource(TransactionSource::task()) + ->setOp('console.command.scheduled') + ->setStartTimestamp(microtime(true)); + + $transaction = SentrySdk::getCurrentHub()->startTransaction($context); + + $this->pushSpan($transaction); + } + + public function handleScheduledTaskFinished(): void + { + $this->maybeFinishSpan(SpanStatus::ok()); + $this->maybePopScope(); + } + + public function handleScheduledTaskFailed(): void + { + $this->maybeFinishSpan(SpanStatus::internalError()); + $this->maybePopScope(); + } + private function startCheckIn( ?string $slug, SchedulingEvent $scheduled, @@ -248,6 +296,18 @@ private function makeSlugForScheduled(SchedulingEvent $scheduled): string return "scheduled_{$generatedSlug}"; } + private function getCommandNameForScheduled(SchedulingEvent $scheduled): ?string + { + if (!$scheduled->command) { + return null; + } + + // The command string always starts with the PHP binary and artisan binary, so we remove it since it's not relevant to the name + return trim( + Str::after($scheduled->command, ConsoleApplication::phpBinary() . ' ' . ConsoleApplication::artisanBinary()) + ); + } + private function resolveCache(): Repository { return $this->container()->make(Cache::class)->store($this->cacheStore); diff --git a/test/Sentry/Features/ConsoleSchedulingIntegrationTest.php b/test/Sentry/Features/ConsoleSchedulingIntegrationTest.php index b7794298..f0bc8c0a 100644 --- a/test/Sentry/Features/ConsoleSchedulingIntegrationTest.php +++ b/test/Sentry/Features/ConsoleSchedulingIntegrationTest.php @@ -3,7 +3,9 @@ namespace Sentry\Laravel\Tests\Features; use DateTimeZone; +use Illuminate\Bus\Queueable; use Illuminate\Console\Scheduling\Schedule; +use Illuminate\Contracts\Queue\ShouldQueue; use RuntimeException; use Sentry\Laravel\Tests\TestCase; use Illuminate\Console\Scheduling\Event; @@ -121,8 +123,59 @@ public function testScheduleMacroIsRegisteredWithoutDsnSet(): void $this->assertTrue(Event::hasMacro('sentryMonitor')); } + /** @define-env envSamplingAllTransactions */ + public function testScheduledCommandCreatesTransaction(): void + { + $this->getScheduler()->command('inspire')->everyMinute(); + + $this->artisan('schedule:run'); + + $this->assertSentryTransactionCount(1); + + $transaction = $this->getLastSentryEvent(); + + $this->assertEquals('inspire', $transaction->getTransaction()); + } + + /** @define-env envSamplingAllTransactions */ + public function testScheduledClosureCreatesTransaction(): void + { + $this->getScheduler()->call(function () {})->everyMinute(); + + $this->artisan('schedule:run'); + + $this->assertSentryTransactionCount(1); + + $transaction = $this->getLastSentryEvent(); + + $this->assertEquals('Closure', $transaction->getTransaction()); + } + + /** @define-env envSamplingAllTransactions */ + public function testScheduledJobCreatesTransaction(): void + { + $this->getScheduler()->job(ScheduledQueuedJob::class)->everyMinute(); + + $this->artisan('schedule:run'); + + $this->assertSentryTransactionCount(1); + + $transaction = $this->getLastSentryEvent(); + + $this->assertEquals(ScheduledQueuedJob::class, $transaction->getTransaction()); + } + private function getScheduler(): Schedule { return $this->app->make(Schedule::class); } } + +class ScheduledQueuedJob implements ShouldQueue +{ + use Queueable; + + public function handle(): void + { + } +}