diff --git a/src/Sentry/Laravel/Features/ConsoleIntegration.php b/src/Sentry/Laravel/Features/ConsoleIntegration.php index a329ee41..516ef7c4 100644 --- a/src/Sentry/Laravel/Features/ConsoleIntegration.php +++ b/src/Sentry/Laravel/Features/ConsoleIntegration.php @@ -2,16 +2,25 @@ namespace Sentry\Laravel\Features; +use Illuminate\Console\Application as ConsoleApplication; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Console\Events as ConsoleEvents; use Sentry\Breadcrumb; +use Sentry\Laravel\Features\Concerns\TracksPushedScopesAndSpans; use Sentry\Laravel\Integration; +use Sentry\SentrySdk; use Sentry\State\Scope; +use Sentry\Tracing\SpanStatus; +use Sentry\Tracing\TransactionContext; +use Sentry\Tracing\TransactionSource; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; class ConsoleIntegration extends Feature { + use TracksPushedScopesAndSpans; + private const FEATURE_KEY = 'command_info'; public function isApplicable(): bool @@ -21,6 +30,12 @@ public function isApplicable(): bool public function onBoot(Dispatcher $events): void { + ConsoleApplication::starting(static function (ConsoleApplication $console) { + $console->getDefinition()->addOption( + new InputOption('--sentry-trace', null, InputOption::VALUE_OPTIONAL, 'Trace the execution of this command using the Sentry SDK') + ); + }); + $events->listen(ConsoleEvents\CommandStarting::class, [$this, 'commandStarting']); $events->listen(ConsoleEvents\CommandFinished::class, [$this, 'commandFinished']); } @@ -31,6 +46,23 @@ public function commandStarting(ConsoleEvents\CommandStarting $event): void return; } + $shouldTrace = $event->input->hasOption('sentry-trace') && $event->input->getParameterOption('--sentry-trace') !== false; + + // If `--sentry-trace` is passed, we start a new transaction and optionally take the operation name from the option value + if ($shouldTrace) { + $sentryTraceOp = $event->input->getOption('sentry-trace'); + + $context = TransactionContext::make() + ->setName($event->command) + ->setSource(TransactionSource::task()) + ->setOp($sentryTraceOp ?? 'console.command') + ->setStartTimestamp(microtime(true)); + + $transaction = SentrySdk::getCurrentHub()->startTransaction($context); + + $this->pushSpan($transaction); + } + Integration::configureScope(static function (Scope $scope) use ($event): void { $scope->setTag('command', $event->command); }); @@ -63,6 +95,14 @@ public function commandFinished(ConsoleEvents\CommandFinished $event): void )); } + $span = $this->maybePopSpan(); + + if ($span) { + $span->finish(); + + $span->setStatus($event->exitCode === 0 ? SpanStatus::ok() : SpanStatus::internalError()); + } + // Flush any and all events that were possibly generated by the command Integration::flushEvents(); diff --git a/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php b/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php index 31e8165a..edbe6e71 100644 --- a/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php +++ b/src/Sentry/Laravel/Features/ConsoleSchedulingIntegration.php @@ -11,6 +11,7 @@ use Illuminate\Contracts\Cache\Factory as Cache; use Illuminate\Contracts\Cache\Repository; use Illuminate\Contracts\Events\Dispatcher; +use Illuminate\Support\ProcessUtils; use Illuminate\Support\Str; use RuntimeException; use Sentry\CheckIn; @@ -136,8 +137,28 @@ public function useCacheStore(?string $name): void public function handleScheduledTaskStarting(ScheduledTaskStarting $event): void { - // There is nothing for us to track if it's a background task since it will be handled by a separate process - if (!$event->task || $event->task->runInBackground) { + if (!$event->task) { + return; + } + + // If the command is run in the background we need to add the trace argument to the command string + if ($event->task->command && $event->task->runInBackground) { + if (Str::contains($event->task->command, '--sentry-trace')) { + return; + } + + $traceArgument = ProcessUtils::escapeArgument('console.command.scheduled'); + + $event->task->command = "{$event->task->command} --sentry-trace={$traceArgument}"; + + // We have modified the command string and at this point there is nothing for us to do + // The framework will create a child process and run the command in the background with the new command + // We will pick up the `--sentry-trace` option in the new process and start a new transaction from there + return; + } + + // If the command is run in the background we don't want to start a transaction here since it will be useless because the actual work will take place in a different process + if ($event->task->runInBackground) { return; }