diff --git a/src/Concerns/CapturesState.php b/src/Concerns/CapturesState.php index a078405c..9dcfc375 100644 --- a/src/Concerns/CapturesState.php +++ b/src/Concerns/CapturesState.php @@ -44,9 +44,13 @@ use function array_unshift; use function debug_backtrace; use function env; +use function in_array; use function memory_reset_peak_usage; use function preg_match; +use function preg_split; use function random_int; +use function str_replace; +use function trim; /** * @internal @@ -59,6 +63,8 @@ trait CapturesState private bool $paused = false; + private bool $captureDefaultVendorCommands = false; + /** * @var WeakMap */ @@ -114,8 +120,14 @@ public function configureRequestSampling(): void /** * @internal */ - public function configureCommandSampling(): void + public function configureCommandSampling(string $command): void { + if (! $this->captureDefaultVendorCommands && in_array($command, $this->defaultVendorCommands(), true)) { + $this->dontSample(); + + return; + } + $this->sample(match (Compatibility::getSamplingFromContext(null)) { true => 1.0, false => 0.0, @@ -128,9 +140,57 @@ public function configureCommandSampling(): void */ public function configureScheduledTaskSampling(Event $event): void { + if (! $this->captureDefaultVendorCommands) { + $command = str_replace( + [Artisan::phpBinary(), Artisan::artisanBinary()], + '', + $event->command ?? '' + ); + + $command = preg_split('/\s+/', trim($command), 2)[0] ?? ''; + + if (in_array($command, $this->defaultVendorCommands(), true)) { + $this->dontSample(); + + return; + } + } + $this->sample(rate: $this->scheduledTasksSampleRates[$event] ?? $this->config['sampling']['scheduled_tasks']); } + /** + * @api + */ + public function captureDefaultVendorCommands(bool $capture = true): void + { + $this->captureDefaultVendorCommands = $capture; + } + + /** + * @api + * + * @return list + */ + public static function defaultVendorCommands(): array + { + return [ + 'auth:clear-resets', + 'config:cache', + 'horizon:snapshot', + 'horizon:status', + 'horizon:supervisor', + 'inertia:start-ssr', + 'invoke-serialized-closure', + 'model:prune', + 'nightwatch:agent', + 'nightwatch:status', + 'queue:monitor', + 'reverb:start', + 'schedule:list', + ]; + } + /** * @api */ diff --git a/src/Facades/Nightwatch.php b/src/Facades/Nightwatch.php index ed3bb561..eeb969a6 100644 --- a/src/Facades/Nightwatch.php +++ b/src/Facades/Nightwatch.php @@ -29,6 +29,8 @@ * @method static void rejectCacheKeys(array $keys) * @method static void captureDefaultVendorCacheKeys(bool $capture = true) * @method static array defaultVendorCacheKeys() + * @method static void captureDefaultVendorCommands(bool $capture = true) + * @method static array defaultVendorCommands() * @method static void rejectMail(callable $callback) * @method static void rejectNotifications(callable $callback) * @method static void rejectOutgoingRequests(callable $callback) diff --git a/src/Hooks/CommandStartingListener.php b/src/Hooks/CommandStartingListener.php index a41ba645..6ee65e77 100644 --- a/src/Hooks/CommandStartingListener.php +++ b/src/Hooks/CommandStartingListener.php @@ -53,7 +53,7 @@ public function __invoke(CommandStarting $event): void match ($event->command) { 'queue:work', 'queue:listen', 'horizon:work', 'vapor:work' => $this->registerJobHooks($event), 'schedule:run', 'schedule:work' => $this->registerScheduledTaskHooks(), - 'schedule:finish' => null, + 'help', 'inspire', 'schedule:finish' => null, default => $this->registerCommandHooks($event), }; } catch (Throwable $e) { @@ -116,7 +116,7 @@ private function registerCommandHooks(CommandStarting $event): void return; } - $this->nightwatch->configureCommandSampling(); + $this->nightwatch->configureCommandSampling($event->command); $this->nightwatch->prepareForCommand($event->command); diff --git a/tests/Feature/Sensors/CommandSensorTest.php b/tests/Feature/Sensors/CommandSensorTest.php index fc6c18a6..0fcf0574 100644 --- a/tests/Feature/Sensors/CommandSensorTest.php +++ b/tests/Feature/Sensors/CommandSensorTest.php @@ -12,7 +12,9 @@ use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; use Laravel\Nightwatch\Compatibility; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\NullOutput; use Tests\TestCase; use function array_shift; @@ -275,16 +277,24 @@ public function test_it_child_commands_do_not_progress_the_modify_execution_stag $ingest->assertLatestWrite('cache-event:0.execution_stage', 'action'); } - public function test_it_ignores_schedule_finish_command(): void + #[DataProvider('vendorCommands')] + public function test_it_ignores_vendor_commands(string $command): void { $ingest = $this->fakeIngest(); - $status = Artisan::handle($input = new StringInput('schedule:finish 123')); + $status = Artisan::handle($input = new StringInput($command), new NullOutput); Artisan::terminate($input, $status); $this->assertSame(0, $status); $ingest->assertWrittenTimes(0); } + + public static function vendorCommands(): iterable + { + yield ['help']; + yield ['inspire']; + yield ['schedule:finish 123']; + } } class ParentCommand extends Command diff --git a/tests/Unit/CliSamplingTest.php b/tests/Unit/CliSamplingTest.php index 38fdedda..d2b03382 100644 --- a/tests/Unit/CliSamplingTest.php +++ b/tests/Unit/CliSamplingTest.php @@ -12,11 +12,14 @@ use Laravel\Nightwatch\Compatibility; use Laravel\Nightwatch\Console\Sample; use Laravel\Nightwatch\Facades\Nightwatch; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Console\Input\StringInput; use Symfony\Component\Console\Output\NullOutput; use Tests\TestCase; +use function dirname; use function event; +use function fake; class CliSamplingTest extends TestCase { @@ -27,6 +30,8 @@ protected function setUp(): void $this->forceCommandExecutionState(); parent::setUp(); + + $this->app->setBasePath(dirname($this->app->basePath())); } public function test_it_samples_job_attempts(): void @@ -129,9 +134,14 @@ public function test_it_pulls_sample_from_context_when_command_starting(): void public function test_it_resets_sampling_after_each_task(): void { event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput)); + Artisan::command($command = fake()->name(), fn () => 0); Nightwatch::dontSample(); - event(new ScheduledTaskStarting($this->app[Schedule::class]->call('php artisan inspire'))); + event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command))); + $this->assertTrue(Nightwatch::sampling()); + + Artisan::command($command = fake()->name(), fn () => 0); + event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command))); $this->assertTrue(Nightwatch::sampling()); } @@ -151,7 +161,7 @@ public function test_it_can_use_global_config_to_sample_scheduled_tasks(): void $ingest->forgetWrites(); } - $this->assertEqualsWithDelta(50, $writes, 10); + $this->assertEqualsWithDelta(50, $writes, 20); $this->assertCount(0, $this->core->ingest->buffer); } @@ -175,4 +185,84 @@ public function test_it_applies_individual_sample_rates_to_scheduled_tasks(): vo $this->assertSame(100, $writes); $this->assertCount(0, $this->core->ingest->buffer); } + + #[DataProvider('vendorCommands')] + public function test_it_does_not_sample_vendor_commands(string $command): void + { + $ingest = $this->fakeIngest(); + Artisan::command($command, fn () => 0); + + $status = Artisan::handle($input = new StringInput($command), new NullOutput); + Artisan::terminate($input, $status); + + $ingest->assertWrittenTimes(0); + } + + #[DataProvider('vendorCommands')] + public function test_it_samples_vendor_commands_when_enabled(string $command): void + { + $ingest = $this->fakeIngest(); + Artisan::command($command, fn () => 0); + + Nightwatch::captureDefaultVendorCommands(); + + $status = Artisan::handle($input = new StringInput($command), new NullOutput); + Artisan::terminate($input, $status); + + $ingest->assertWrittenTimes(1); + } + + #[DataProvider('vendorCommands')] + public function test_it_does_not_sample_scheduled_vendor_commands(string $command): void + { + event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput)); + + event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)->everyMinute())); + + $this->assertFalse(Nightwatch::sampling()); + } + + #[DataProvider('vendorCommands')] + public function test_it_samples_vendor_scheduled_tasks_when_enabled(string $command): void + { + Nightwatch::captureDefaultVendorCommands(); + + event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput)); + + event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)->everyMinute())); + + $this->assertTrue(Nightwatch::sampling()); + } + + #[DataProvider('vendorCommands')] + public function test_it_samples_vendor_scheduled_tasks_when_explicitly_sampled(string $command): void + { + Nightwatch::captureDefaultVendorCommands(); + event(new CommandStarting('schedule:run', new StringInput(''), new NullOutput)); + + $samples = 0; + for ($i = 0; $i < 100; $i++) { + event(new ScheduledTaskStarting($this->app[Schedule::class]->command($command)->everyMinute()->tap(Sample::rate(0.5)))); + $samples += (int) Nightwatch::sampling(); + } + + $this->assertEqualsWithDelta(50, $samples, 20); + } + + public static function vendorCommands(): iterable + { + yield ['auth:clear-resets']; + yield ['config:cache']; + yield ['horizon:snapshot']; + yield ['horizon:status']; + yield ['horizon:supervisor']; + yield ['inertia:start-ssr']; + yield ['invoke-serialized-closure']; + yield ['model:prune']; + yield ['nightwatch:agent']; + yield ['nightwatch:status']; + yield ['queue:monitor']; + yield ['reverb:start']; + yield ['schedule:list']; + } }