diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..aa4dad0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,45 @@ +# Define the line ending behavior of the different file extensions +# Set default behavior, in case users don't have core.autocrlf set. +* text text=auto eol=lf + +.php diff=php + +# Declare files that will always have CRLF line endings on checkout. +*.bat eol=crlf + +# Declare files that will always have LF line endings on checkout. +*.pem eol=lf + +# Denote all files that are truly binary and should not be modified. +*.png binary +*.jpg binary +*.gif binary +*.ico binary +*.mo binary +*.pdf binary +*.phar binary +*.woff binary +*.woff2 binary +*.ttf binary +*.otf binary +*.eot binary + +# Remove files for archives generated using `git archive` +.github export-ignore +.phive export-ignore +contrib export-ignore +tests/test_app export-ignore +tests/TestCase export-ignore + +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.mailmap export-ignore +.stickler.yml export-ignore +Makefile export-ignore +phpcs.xml export-ignore +phpstan.neon.dist export-ignore +phpstan-baseline.neon export-ignore +phpunit.xml.dist export-ignore +psalm.xml export-ignore +psalm-baseline.xml export-ignore diff --git a/CHANGELOG.md b/CHANGELOG.md index c7536ff..71bb20c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2024-12-10 +* added commands to purge queues and test queues +* fixed test queue template path +* changed `queue_monitor purge` to `queue-monitor purge-logs` +* changed `queue_monitor notify` to `queue-monitor notify +* changed default configuration of `purgeLogsOlderThanDays` to 7 days instead of 30 + +## [2.0.2] - 2024-12-09 +* ported fixes and enhancement from version 1.x +* fixed handling `QueueMonitor.purgeLogsOlderThanDays` in Purge command +* added `QueueMonitor.disabled` option +* added support for disabling queue monitor commands +* updated README.md +* decreased log level when queue monitor is disabled +* replaced saveOrFail with save + +## [2.0.1] - 2024-05-14 +### Fixed +- Removed TimestampBehavior from LogsTable to avoid problems when TimestampBehavior is overriden + ## [2.0] - 2024-04-17 ### Added diff --git a/README.md b/README.md index 61078e8..85ec9b2 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## Versions and branches | CakePHP | CakeDC Queue Monitor Plugin | Tag | Notes | |:-------:|:--------------------------------------------------------------------------:|:------------:|:-------| -| ^5.0 | [2.0.0](https://github.com/CakeDC/cakephp-queue-monitor/tree/2.next-cake5) | 2.next-cake5 | stable | +| ^5.0 | [2.1.0](https://github.com/CakeDC/cakephp-queue-monitor/tree/2.next-cake5) | 2.next-cake5 | stable | | ^4.4 | [1.0.0](https://github.com/CakeDC/cakephp-queue-monitor/tree/1.next-cake4) | 1.next-cake4 | stable | ## Overview @@ -26,28 +26,14 @@ composer require cakedc/queue-monitor ``` ## Configuration - -Add QueueMonitorPlugin to your `Application::bootstrap`: -```php -use Cake\Http\BaseApplication; -use CakeDC\QueueMonitor\QueueMonitorPlugin; - -class Application extends BaseApplication -{ - // ... - - public function bootstrap(): void - { - parent::bootstrap(); - - $this->addPlugin(QueueMonitorPlugin::class); - } - - // ... -} - +Add QueueMonitorPlugin to your application by running command: +```shell +bin/cake plugin load CakeDC/QueueMonitor +``` +Run the required migrations +```shell +bin/cake migrations migrate -p CakeDC/QueueMonitor ``` - Set up the QueueMonitor configuration in your `config/app_local.php`: ```php // ... @@ -58,16 +44,15 @@ Set up the QueueMonitor configuration in your `config/app_local.php`: // mailer config, the default is `default` mailer, you can ommit // this setting if you use default value - 'mailerConfig' => 'myCustomMailer', + 'mailerConfig' => 'default', - // the default is 30 minutes, you can ommit this setting if you - // use the default value - 'longJobInMinutes' => 45, + // the default is 30 minutes, you can ommit this setting if you use the default value + 'longJobInMinutes' => 30, - // the default is 30 days, you can ommit this setting if you + // the default is 7 days, you can ommit this setting if you use the default value // its advised to set this value correctly after queue usage analysis to avoid // high space usage in db - 'purgeLogsOlderThanDays' => 10, + 'purgeLogsOlderThanDays' => 7, // comma separated list of recipients of notification about long running queue jobs 'notificationRecipients' => 'recipient1@yourdomain.com,recipient2@yourdomain.com,recipient3@yourdomain.com', @@ -75,11 +60,6 @@ Set up the QueueMonitor configuration in your `config/app_local.php`: // ... ``` -Run the required migrations -```shell -bin/cake migrations migrate -p CakeDC/QueueMonitor -``` - For each queue configuration add `listener` setting ```php // ... @@ -95,19 +75,46 @@ For each queue configuration add `listener` setting ## Notification command -To set up notifications when there are long running or possible stuck jobs please use command +To set up notifications when there are jobs running for a long time or jobs that may be stuck and blocking the queue +please use command: ```shell -bin/cake queue_monitor notify +bin/cake queue-monitor notify ``` This command will send notification emails to recipients specified in `QueueMonitor.notificationRecipients`. Best is -to use it as a cronjob +to use it as a cronjob. + +## Test Enqueue command + +To quickly test if all queues are running correctly please run this command (replace `your-email@domain.com` with working +email address: +```shell +bin/cake queue-monitor test-enqueue your-email@domain.com +``` + +This command will send the command through all configured queues. + +## Purge queues command +To purge the content of a specified queue you can use the purge queue command: +```shell +bin/cake queue-monitor purge-queue your-queue-name + +``` + +The above command will remove all pending queue jobs from the specified queue. + +To purge all queues you can use command: + +```shell +bin/cake queue-monitor purge-queue --all + +``` -## Purge command +## Purge Logs command The logs table may grow overtime, to keep it slim you can use the purge command: ```shell -bin/cake queue_monitor purge +bin/cake queue-monitor purge-logs ``` This command will purge logs older than value specified in `QueueMonitor.purgeLogsOlderThanDays`, the value is in diff --git a/src/Command/NotifyCommand.php b/src/Command/NotifyCommand.php index aef878d..abe8077 100644 --- a/src/Command/NotifyCommand.php +++ b/src/Command/NotifyCommand.php @@ -48,7 +48,15 @@ public function __construct( */ public static function defaultName(): string { - return 'queue_monitor notify'; + return 'queue-monitor notify'; + } + + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return __('Queue Monitoring notifier'); } /** @@ -57,7 +65,7 @@ public static function defaultName(): string public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { return parent::buildOptionParser($parser) - ->setDescription(__('Queue Monitoring notifier')); + ->setDescription(self::getDescription()); } /** diff --git a/src/Command/PurgeCommand.php b/src/Command/PurgeLogsCommand.php similarity index 87% rename from src/Command/PurgeCommand.php rename to src/Command/PurgeLogsCommand.php index a60d1d3..3d1b9ac 100644 --- a/src/Command/PurgeCommand.php +++ b/src/Command/PurgeLogsCommand.php @@ -25,14 +25,14 @@ use function Cake\I18n\__; /** - * Purge command. + * Purge Logs command. */ -final class PurgeCommand extends Command +final class PurgeLogsCommand extends Command { use DisableTrait; use LogTrait; - private const DEFAULT_PURGE_DAYS_OLD = 30; + private const DEFAULT_PURGE_DAYS_OLD = 7; /** * Constructor @@ -48,7 +48,15 @@ public function __construct( */ public static function defaultName(): string { - return 'queue_monitor purge'; + return 'queue-monitor purge-logs'; + } + + /** + * @inheritDoc + */ + public static function getDescription(): string + { + return __('Queue Monitoring log purger'); } /** @@ -57,7 +65,7 @@ public static function defaultName(): string public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser { return parent::buildOptionParser($parser) - ->setDescription(__('Queue Monitoring log purger')); + ->setDescription(self::getDescription()); } /** @@ -73,6 +81,7 @@ public function execute(Arguments $args, ConsoleIo $io) return self::CODE_SUCCESS; } + $purgeLogsOlderThanDays = (int)Configure::read( 'QueueMonitor.purgeLogsOlderThanDays', self::DEFAULT_PURGE_DAYS_OLD diff --git a/src/Command/PurgeQueueCommand.php b/src/Command/PurgeQueueCommand.php new file mode 100644 index 0000000..c963be8 --- /dev/null +++ b/src/Command/PurgeQueueCommand.php @@ -0,0 +1,195 @@ +setDescription(self::getDescription()) + ->addArgument('queue-config', [ + 'help' => __('Queue configuration key'), + ]) + ->addOption('all', [ + 'help' => __('All messages will be purged'), + 'short' => 'a', + 'boolean' => true, + 'default' => false, + ]) + ->addOption('yes', [ + 'short' => 'y', + 'boolean' => true, + 'default' => false, + 'help' => __('Yes - skip confirmation prompt'), + ]); + } + + /** + * @inheritDoc + */ + public function execute(Arguments $args, ConsoleIo $io) + { + if ($this->isDisabled()) { + $this->log( + 'Logs were not purged because Queue Monitor is disabled.', + LogLevel::WARNING + ); + + return self::CODE_SUCCESS; + } + + if ($args->getOption('all') === true) { + $this->checkConfirmation( + __('Are you sure you want to purge messages from all queues?'), + $args, + $io + ); + + collection($this->getConfiguredQueues())->each(function (string $queueConfig) use ($io): void { + try { + $this->enqueueClientService->purgeQueue($queueConfig); + $io->success(__('Queue `{0}` purged successfully', $queueConfig)); + } catch (QueueMonitorException $e) { + $io->error(__('Unable to purge queue `{0}`, reason: {1}', $queueConfig, $e->getMessage())); + } + }); + } else { + $queueConfig = $args->getArgument('queue-config'); + + if (!$this->validateQueueConfig($queueConfig)) { + $io->error(__('Queue configuration key is invalid')); + $configuredQueues = $this->getConfiguredQueues(); + if ($configuredQueues) { + $io->error(__('Valid configuration keys are: {0}', implode(', ', $configuredQueues))); + } else { + $io->error(__('There are no queue configurations')); + } + $this->displayHelp($this->getOptionParser(), $args, $io); + + return self::CODE_ERROR; + } + + $this->checkConfirmation( + __('Are you sure you want to purge messages from specified queue?'), + $args, + $io + ); + + try { + $this->enqueueClientService->purgeQueue((string)$queueConfig); + $io->success(__('Queue `{0}` purged successfully', $queueConfig)); + + return self::CODE_SUCCESS; + } catch (QueueMonitorException $e) { + $io->error(__('Unable to purge queue `{0}`, reason: {1}', $queueConfig, $e->getMessage())); + + return self::CODE_ERROR; + } + } + } + + /** + * Validate queue config + */ + private function validateQueueConfig(?string $queueConfig): bool + { + if (is_null($queueConfig) || !strlen($queueConfig)) { + return false; + } + + $validQueueConfigs = $this->getConfiguredQueues(); + + if (!in_array($queueConfig, $validQueueConfigs, true)) { + return false; + } + + return true; + } + + /** + * Get configured queues + */ + private function getConfiguredQueues(): array + { + return array_keys(Configure::read('Queue', [])); + } + + /** + * Check confirmation + */ + private function checkConfirmation(string $prompt, Arguments $args, ConsoleIo $io): void + { + if ($args->getOption('yes') === false) { + $confirmation = $io->askChoice( + $prompt, + [ + __('yes'), + __('no'), + ], + __('no') + ); + + if ($confirmation === __('no')) { + $io->abort(__('Aborting')); + } + } + } +} diff --git a/src/Command/TestQueueCommand.php b/src/Command/TestQueueCommand.php new file mode 100644 index 0000000..ebc855c --- /dev/null +++ b/src/Command/TestQueueCommand.php @@ -0,0 +1,117 @@ +setDescription(self::getDescription()) + ->addArgument($this::ARGUMENT_EMAIL, [ + 'help' => __('Email to send to'), + 'required' => true, + ]); + } + + /** + * @inheritDoc + */ + public function execute(Arguments $args, ConsoleIo $io) + { + if ($this->isDisabled()) { + $this->log( + 'Test Enqueue was not performed because Queue Monitor is disabled.', + LogLevel::WARNING + ); + + return self::CODE_SUCCESS; + } + + $email = $args->getArgument(self::ARGUMENT_EMAIL); + if (!Validation::email($email)) { + $io->error(__('Invalid email')); + + return $this::CODE_ERROR; + } + + collection(Configure::read('Queue', [])) + ->each(function ( + array $queueConfig, + string $queueConfigKey + ) use ( + $email, + $io + ): void { + /** @var \CakeDC\QueueMonitor\Mailer\TestQueueMailer $mailer */ + $mailer = $this->getMailer('CakeDC/QueueMonitor.TestQueue'); + /** @uses \CakeDC\QueueMonitor\Mailer\TestQueueMailer::testQueue() */ + $mailer->push( + action: $mailer::SEND_TEST_QUEUE, + args: [ + $email, + $queueConfigKey, + ], + options: [ + 'config' => $queueConfigKey, + ] + ); + $io->info(__( + 'Queued test email `{0}` in queue `{1}`', + $email, + $queueConfigKey + )); + }); + + return $this::CODE_SUCCESS; + } +} diff --git a/src/Mailer/TestQueueMailer.php b/src/Mailer/TestQueueMailer.php new file mode 100644 index 0000000..b4378c2 --- /dev/null +++ b/src/Mailer/TestQueueMailer.php @@ -0,0 +1,51 @@ +setProfile(Configure::read('QueueMonitor.mailerConfig', 'default')) + ->setTo($emailAddress) + ->setSubject(__('Test enqueue from queue `{0}`', $queueConfig)) + ->setEmailFormat(Message::MESSAGE_BOTH) + ->viewBuilder() + ->disableAutoLayout() + ->setTemplate('CakeDC/QueueMonitor.test'); + } +} diff --git a/src/QueueMonitorPlugin.php b/src/QueueMonitorPlugin.php index 3a98a02..9a0bd7b 100644 --- a/src/QueueMonitorPlugin.php +++ b/src/QueueMonitorPlugin.php @@ -12,11 +12,12 @@ */ namespace CakeDC\QueueMonitor; -use Cake\Console\CommandCollection; use Cake\Core\BasePlugin; use Cake\Core\ContainerInterface; use CakeDC\QueueMonitor\Command\NotifyCommand; -use CakeDC\QueueMonitor\Command\PurgeCommand; +use CakeDC\QueueMonitor\Command\PurgeLogsCommand; +use CakeDC\QueueMonitor\Command\PurgeQueueCommand; +use CakeDC\QueueMonitor\Service\EnqueueClientService; use CakeDC\QueueMonitor\Service\QueueMonitoringService; /** @@ -39,27 +40,24 @@ class QueueMonitorPlugin extends BasePlugin */ protected bool $middlewareEnabled = false; - /** - * @inheritDoc - */ - public function console(CommandCollection $commands): CommandCollection - { - return parent::console($commands) - ->add('queue_monitor purge', PurgeCommand::class) - ->add('queue_monitor notify', NotifyCommand::class); - } - /** * @inheritDoc */ public function services(ContainerInterface $container): void { $container->add(QueueMonitoringService::class); + $container->addShared(EnqueueClientService::class); + $container - ->add(PurgeCommand::class) + ->add(PurgeLogsCommand::class) ->addArguments([ QueueMonitoringService::class, ]); + $container + ->add(PurgeQueueCommand::class) + ->addArguments([ + EnqueueClientService::class, + ]); $container ->add(NotifyCommand::class) ->addArguments([ diff --git a/src/Service/EnqueueClientService.php b/src/Service/EnqueueClientService.php new file mode 100644 index 0000000..0c7eb01 --- /dev/null +++ b/src/Service/EnqueueClientService.php @@ -0,0 +1,54 @@ +getDriver()->getContext(); + $queueName = $this->getEnqueueInternalQueueName($simpleClient->getDriver()->getConfig()); + $queue = $context->createQueue($queueName); + $context->purgeQueue($queue); + } catch (PurgeQueueNotSupportedException $e) { + throw new QueueMonitorException($e->getMessage(), $e->getCode(), $e); + } + } + + /** + * Get enqueue internal queue name + */ + private function getEnqueueInternalQueueName(Config $enqueueClientConfig): string + { + return implode('.', [ + $enqueueClientConfig->getPrefix(), + $enqueueClientConfig->getApp(), + $enqueueClientConfig->getDefaultQueue(), + ]); + } +} diff --git a/templates/email/html/test.php b/templates/email/html/test.php new file mode 100644 index 0000000..515332a --- /dev/null +++ b/templates/email/html/test.php @@ -0,0 +1,5 @@ + + +Test email diff --git a/templates/email/text/test.php b/templates/email/text/test.php new file mode 100644 index 0000000..515332a --- /dev/null +++ b/templates/email/text/test.php @@ -0,0 +1,5 @@ + + +Test email