Skip to content

Commit

Permalink
Add Scheduled Task Tracing (#968)
Browse files Browse the repository at this point in the history
Co-authored-by: Michael Hoffmann <[email protected]>
  • Loading branch information
stayallive and cleptric authored Jan 22, 2025
1 parent 596bfa1 commit c3808bd
Show file tree
Hide file tree
Showing 2 changed files with 114 additions and 1 deletion.
62 changes: 61 additions & 1 deletion src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
53 changes: 53 additions & 0 deletions test/Sentry/Features/ConsoleSchedulingIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
}
}

0 comments on commit c3808bd

Please sign in to comment.