diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7d38cf2 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100755 index 0000000..7f346f6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Path-based git attributes +# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html + +# Ignore all test and documentation with "export-ignore". +/.github export-ignore +/tests export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/.scrutinizer.yml export-ignore +/phpcs.xml export-ignore +/phpmd.xml export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..5e75a87 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + pull_request: + schedule: + - cron: '0 0 * * *' + +jobs: + build: + + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + php: ['7.4'] + laravel: ['6.*', '7.*'] + include: + - laravel: '7.*' + testbench: '5.*' + - laravel: '6.*' + testbench: '4.*' + + name: P${{ matrix.php }} - L${{ matrix.laravel }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ~/.composer/cache/files + key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: common, curl, json, mbstring, zip, sqlite, pdo_sqlite + coverage: xdebug + + - name: Install dependencies + run: | + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update + composer update --prefer-dist --no-interaction --no-suggest + + # lower php and laravel versions + + - name: PHPUnit + if: matrix.php != '7.4' || matrix.laravel != '7.*' + run: vendor/bin/phpunit + + # last php and laravel versions + + - name: Code analysis + if: matrix.php == '7.4' && matrix.laravel == '7.*' + run: | + vendor/bin/phpcs + vendor/bin/phpmd src text phpmd.xml + vendor/bin/phpstan analyse + + - name: PHPUnit + Code coverage + if: matrix.php == '7.4' && matrix.laravel == '7.*' + run: | + mkdir -p build/logs + vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml + + - name: Code coverage upload to Coveralls + if: matrix.php == '7.4' && matrix.laravel == '7.*' + env: + COVERALLS_RUN_LOCALLY: 1 + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + run: | + echo ${GITHUB_REF##*/} + composer require php-coveralls/php-coveralls + vendor/bin/php-coveralls -v --coverage_clover=build/logs/clover.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..33ebff1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +composer.lock +vendor +coverage +tests/Support/temp +tests/temp +.phpunit.result.cache \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml new file mode 100644 index 0000000..a7fcc3a --- /dev/null +++ b/.scrutinizer.yml @@ -0,0 +1,28 @@ +filter: + excluded_paths: [tests/*] +checks: + php: true +tools: + php_mess_detector: true + php_pdepend: true + php_code_coverage: false + external_code_coverage: false + php_analyzer: true + php_code_sniffer: + config: + standard: PSR4 + filter: + paths: ['src'] + sensiolabs_security_checker: true + php_loc: + enabled: true + excluded_dirs: [vendor, tests] + php_cpd: + enabled: true + excluded_dirs: [vendor, tests] +build: + nodes: + analysis: + tests: + override: + - php-scrutinizer-run \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0e80857 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## [1.0.0](https://github.com/Okipa/laravel-supervisor-downtime-notifier/releases/tag/1.0.0) + +2020-05-13 + +* First release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100755 index 0000000..b4ae1c4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contributing + +Contributions are **welcome** and will be fully **credited**. + +Please read and understand the contribution guide before creating an issue or pull request. + +## Etiquette + +This project is open source, and as such, the maintainers give their free time to build and maintain the source code +held within. They make the code freely available in the hope that it will be of use to other developers. It would be +extremely unfair for them to suffer abuse or anger for their hard work. + +Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the +world that developers are civilized and selfless people. + +It's the duty of the maintainer to ensure that all submissions to the project are of sufficient +quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. + +## Viability + +When requesting or submitting new features, first consider whether it might be useful to others. Open +source projects are used by many developers, who may have entirely different needs to your own. Think about +whether or not your feature is likely to be used by other users of the project. + +## Procedure + +Before filing an issue: + +- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. +- Check to make sure your feature suggestion isn't already present within the project. +- Check the pull requests tab to ensure that the bug doesn't have a fix in progress. +- Check the pull requests tab to ensure that the feature isn't already in progress. + +Before submitting a pull request: + +- Check the codebase to ensure that your feature doesn't already exist. +- Check the pull requests to ensure that another person hasn't already submitted the feature or fix. + +## Requirements + +If the project maintainer has any additional requirements, you will find them listed here. + +- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). + +- **Add tests!** - Your patch won't be accepted if it doesn't have tests. + +- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. + +- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. + +- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. + +- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. + +**Happy coding**! diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..5040c21 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Arthur LORENT + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d1301b2 --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# Get notified when supervisor is down + +[![Source Code](https://img.shields.io/badge/source-okipa/laravel--supervisor--downtime--notifier-blue.svg)](https://github.com/Okipa/laravel-supervisor-downtime-notifier) +[![Latest Version](https://img.shields.io/github/release/okipa/laravel-supervisor-downtime-notifier.svg?style=flat-square)](https://github.com/Okipa/laravel-supervisor-downtime-notifier/releases) +[![Total Downloads](https://img.shields.io/packagist/dt/okipa/laravel-supervisor-downtime-notifier.svg?style=flat-square)](https://packagist.org/packages/okipa/laravel-supervisor-downtime-notifier) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![Build status](https://github.com/Okipa/laravel-supervisor-downtime-notifier/workflows/CI/badge.svg)](https://github.com/Okipa/laravel-supervisor-downtime-notifier/actions) +[![Coverage Status](https://coveralls.io/repos/github/Okipa/laravel-supervisor-downtime-notifier/badge.svg?branch=master)](https://coveralls.io/github/Okipa/laravel-supervisor-downtime-notifier?branch=master) +[![Quality Score](https://img.shields.io/scrutinizer/g/Okipa/laravel-supervisor-downtime-notifier.svg?style=flat-square)](https://scrutinizer-ci.com/g/Okipa/laravel-supervisor-downtime-notifier/?branch=master) + +Get notified and execute PHP callback when: +* the supervisor service is not running on your server. +* your environment supervisor processes are down. + +Notifications can be sent by mail, Slack and webhooks (chats often provide a webhook API). + +## Compatibility + +| Laravel version | PHP version | Package version | +|---|---|---| +| ^6.0 | ^7.4 | ^1.0 | + +## Table of Contents +- [Requirements](#requirements) +- [Installation](#installation) +- [Configuration](#configuration) +- [Usage](#usage) +- [Testing](#testing) +- [Changelog](#changelog) +- [Contributing](#contributing) +- [Credits](#credits) +- [Licence](#license) + +## Requirements + +By default, this package monitors supervisor downtime for projects running on Linux servers. + +The user running PHP CLI will execute the following commands: + +* `systemctl is-active supervisor` +* `supervisorctl status ""` + +As so, make sure you give him permission to execute these actions (`sudo visudo -f /etc/sudoers.d/`) : + +* ` ALL=NOPASSWD:/bin/systemctl is-active supervisor` +* ` ALL=NOPASSWD:/usr/bin/supervisorctl status *` + +That being said, you still can use this package for other servers OS by using your own `SupervisorChecker` class and defining OS-specific commands. + +## Installation + +Install the package with composer: + +```bash +composer require "okipa/laravel-supervisor-downtime-notifier:^1.0" +``` + +In case you want to use `Slack` notifications you'll also have to install: + +```bash +composer require guzzlehttp/guzzle +``` + +## Configuration + +Publish the package configuration: + +```bash +php artisan vendor:publish --tag=supervisor-downtime-notifier:config +``` + +## Usage + +Just add this command in the `schedule()` method of your `\App\Console\Kernel` class : + +```php +$schedule->command('supervisor:downtime:notify')->everyFifteenMinutes(); +``` + +And you will be notified if your supervisor service is not running, or if your environment supervisor processes are down when the command will be executed. + +## Testing + +```bash +composer test +``` + +## Changelog + +Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. + +## Contributing + +Please see [CONTRIBUTING](CONTRIBUTING.md) for details. + +## Credits + +- [Arthur LORENT](https://github.com/okipa) +- [All Contributors](../../contributors) + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..028634d --- /dev/null +++ b/composer.json @@ -0,0 +1,73 @@ +{ + "name": "okipa/laravel-supervisor-downtime-notifier", + "description": "Get notified when supervisor is down.", + "keywords": [ + "okipa", + "package", + "php", + "laravel", + "supervisor", + "service", + "process", + "processes", + "worker", + "workers", + "down", + "downtime", + "notify", + "notifier", + "laravel-supervisor-downtime-notifier" + ], + "homepage": "https://github.com/Okipa/laravel-supervisor-downtime-notifier", + "license": "MIT", + "authors": [ + { + "name": "Arthur LORENT", + "email": "arthur.lorent@gmail.com", + "role": "Developer" + } + ], + "require": { + "php": "^7.4", + "illuminate/support": "^6.0||^7.0", + "laravel-notification-channels/webhook": "^2.0", + "laravel/slack-notification-channel": "^2.0" + }, + "require-dev": { + "nunomaduro/larastan": "^0.5", + "orchestra/testbench": "^4.0||^5.0", + "phpmd/phpmd": "^2.8", + "squizlabs/php_codesniffer": "^3.5" + }, + "autoload": { + "psr-4": { + "Okipa\\LaravelSupervisorDowntimeNotifier\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Okipa\\LaravelSupervisorDowntimeNotifier\\Test\\": "tests/" + } + }, + "scripts": { + "test": [ + "vendor/bin/phpcbf", + "vendor/bin/phpcs", + "vendor/bin/phpmd config,src text phpmd.xml", + "vendor/bin/phpstan analyse", + "vendor/bin/phpunit" + ] + }, + "extra": { + "laravel": { + "providers": [ + "Okipa\\LaravelSupervisorDowntimeNotifier\\SupervisorDowntimeNotifierServiceProvider" + ] + } + }, + "config": { + "sort-packages": true + }, + "minimum-stability": "dev", + "prefer-stable": true +} diff --git a/config/supervisor-downtime-notifier.php b/config/supervisor-downtime-notifier.php new file mode 100644 index 0000000..0e27318 --- /dev/null +++ b/config/supervisor-downtime-notifier.php @@ -0,0 +1,77 @@ + env('APP_ENV') !== 'local', + + /* + * The supervisor processes to check for each environment. + * Each process will be checked with the `supervisorctl status` command, + * which makes possible the use of wildcard. + */ + 'supervisor' => [ + 'production' => [ + 'sudo' => true, + 'processes' => [ + // 'laravel-queue-production-worker:*', + ] + ], + 'staging' => [ + 'sudo' => true, + 'processes' => [ + // 'laravel-queue-staging-worker:*', + ] + ], + ], + + /* + * The downtime checker which will analyse each process and return the identified the down ones. + * You may use your own supervisor checker but make sure you extends this one. + */ + 'supervisor_checker' => Okipa\LaravelSupervisorDowntimeNotifier\SupervisorChecker::class, + + /* + * The notifiable to which the notification will be sent. + * The default notifiable will use the mail, slack and webhook configuration specified in this config file. + * You may use your own notifiable but make sure it extends this one. + */ + 'notifiable' => Okipa\LaravelSupervisorDowntimeNotifier\Notifiable::class, + + /* + * The notification that will be sent when stuck jobs are detected. + * You may use your own notifications but make sure they extend these ones. + */ + 'notifications' => [ + 'service_not_started' => Okipa\LaravelSupervisorDowntimeNotifier\Notifications\ServiceNotStarted::class, + 'down_processes' => Okipa\LaravelSupervisorDowntimeNotifier\Notifications\ProcessesAreDown::class, + ], + + /* + * The callbacks that will be executed after the related events. + * You may use your own callbacks but make sure they extend these ones. + * Each callback be set to null if you do not want any to be executed. + */ + 'callbacks' => [ + 'service_not_started' => Okipa\LaravelSupervisorDowntimeNotifier\Callbacks\OnServiceNotStarted::class, + 'down_processes' => Okipa\LaravelSupervisorDowntimeNotifier\Callbacks\OnDownProcesses::class, + ], + + /* + * The channels to which the notification will be sent. + */ + 'channels' => ['mail', 'slack', WebhookChannel::class], + + 'mail' => ['to' => 'email@example.test'], + + 'slack' => ['webhookUrl' => 'https://your-slack-webhook.slack.com'], + + // rocket chat webhook example + 'webhook' => ['url' => 'https://rocket.chat/hooks/1234/5678'], + +]; diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..59a4a2b --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,12 @@ + + + PSR-2 validation + ./config + ./src + ./tests + + + ./tests/* + + + diff --git a/phpmd.xml b/phpmd.xml new file mode 100644 index 0000000..ee86b4e --- /dev/null +++ b/phpmd.xml @@ -0,0 +1,67 @@ + + + Written using this resource : https://phpmd.org/rules/index.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 3 + + + + + + + + + + + + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..9fc34ce --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,17 @@ +includes: + - ./vendor/nunomaduro/larastan/extension.neon + +parameters: + + paths: + - config + - src + + # The level 8 is the highest level + level: 5 + + ignoreErrors: + - '#Access to an undefined property#' + + excludes_analyse: + - ./*/*/FileToBeExcluded.php diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..4627524 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,28 @@ + + + + + ./tests/ + + + + + src + + + + + + + + + diff --git a/src/Callbacks/OnDownProcesses.php b/src/Callbacks/OnDownProcesses.php new file mode 100644 index 0000000..c751d76 --- /dev/null +++ b/src/Callbacks/OnDownProcesses.php @@ -0,0 +1,22 @@ +count() > 1 + ? 'Down supervisor processes detected: "' . $downProcesses->implode('", "') . '".' + : 'Down supervisor process detected: "' . $downProcesses->first() . '".'); + } +} diff --git a/src/Callbacks/OnServiceNotStarted.php b/src/Callbacks/OnServiceNotStarted.php new file mode 100644 index 0000000..fef4640 --- /dev/null +++ b/src/Callbacks/OnServiceNotStarted.php @@ -0,0 +1,17 @@ +notify(); + } +} diff --git a/src/Exceptions/InvalidAllowedToRun.php b/src/Exceptions/InvalidAllowedToRun.php new file mode 100644 index 0000000..1136f5e --- /dev/null +++ b/src/Exceptions/InvalidAllowedToRun.php @@ -0,0 +1,10 @@ +downProcesses = $downProcesses; + $this->processesCount = $downProcesses->count(); + } + + /** + * Get the notification's delivery channels. + * + * @return array + */ + public function via(): array + { + return config('supervisor-downtime-notifier.channels'); + } + + /** + * Get the mail representation of the notification. + * + * @return \Illuminate\Notifications\Messages\MailMessage + */ + public function toMail(): MailMessage + { + return (new MailMessage)->level('error') + ->subject(trans_choice( + '{1}[:app - :env] :count supervisor down process has been detected' + . '|[2,*][:app - :env] :count supervisor down processes have been detected', + $this->processesCount, + [ + 'app' => config('app.name'), + 'env' => config('app.env'), + 'count' => $this->processesCount, + ] + )) + ->line(trans_choice( + '{1}We have detected :count supervisor down process on [:app - :env](:url): ":processes".' + . '|[2,*]We have detected :count supervisor down processes on [:app - :env](:url): ":processes".', + $this->processesCount, + [ + 'count' => $this->processesCount, + 'app' => config('app.name'), + 'env' => config('app.env'), + 'url' => config('app.url'), + 'processes' => $this->downProcesses->implode('", "'), + ] + )) + ->line('Please check your down processes connecting to your server and executing the ' + . '"supervisorctl status" command.'); + } + + /** + * Get the slack representation of the notification. + * + * @return \Illuminate\Notifications\Messages\SlackMessage + */ + public function toSlack(): SlackMessage + { + return (new SlackMessage)->error()->content('⚠ ' . trans_choice( + '{1}`[:app - :env]` :count supervisor down process has been detected on :url: ":processes".' + . '|[2,*]`[:app - :env]` :count supervisor down processes have been detected on :url: ":processes".', + $this->processesCount, + [ + 'app' => config('app.name'), + 'env' => config('app.env'), + 'count' => $this->processesCount, + 'url' => config('app.url'), + 'processes' => $this->downProcesses->implode('", "'), + ] + )); + } + + /** + * Get the webhook representation of the notification. + * + * @return \NotificationChannels\Webhook\WebhookMessage + */ + public function toWebhook(): WebhookMessage + { + // rocket chat webhook example + return WebhookMessage::create()->data([ + 'text' => '⚠ ' . trans_choice( + '{1}`[:app - :env]` :count supervisor down process has been detected on :url: ":processes".' + . '|[2,*]`[:app - :env]` :count supervisor down processes have been detected on :url: ' + . '":processes".', + $this->processesCount, + [ + 'app' => config('app.name'), + 'env' => config('app.env'), + 'count' => $this->processesCount, + 'url' => config('app.url'), + 'processes' => $this->downProcesses->implode('", "'), + ] + ), + ])->header('Content-Type', 'application/json'); + } +} diff --git a/src/Notifications/ServiceNotStarted.php b/src/Notifications/ServiceNotStarted.php new file mode 100644 index 0000000..5efa25a --- /dev/null +++ b/src/Notifications/ServiceNotStarted.php @@ -0,0 +1,74 @@ +level('error') + ->subject(__('[:app - :env] supervisor service is not started', [ + 'app' => config('app.name'), + 'env' => config('app.env'), + ])) + ->line(__('We have detected that the supervisor service is not started on [:app - :env](:url).', [ + 'app' => config('app.name'), + 'env' => config('app.env'), + 'url' => config('app.url'), + ])) + ->line('Please restart you supervisor service connecting to your server and executing the ' + . '"supervisorctl restart" command line.'); + } + + /** + * Get the slack representation of the notification. + * + * @return \Illuminate\Notifications\Messages\SlackMessage + */ + public function toSlack(): SlackMessage + { + return (new SlackMessage)->error() + ->content('⚠ ' . __('`[:app - :env]` supervisor service is not started on :url.', [ + 'app' => config('app.name'), + 'env' => config('app.env'), + 'url' => config('app.url'), + ])); + } + + /** + * Get the webhook representation of the notification. + * + * @return \NotificationChannels\Webhook\WebhookMessage + */ + public function toWebhook(): WebhookMessage + { + // rocket chat webhook example + return WebhookMessage::create()->data([ + 'text' => '⚠ ' . __('`[:app - :env]` supervisor service is not started on :url.', [ + 'app' => config('app.name'), + 'env' => config('app.env'), + 'url' => config('app.url'), + ]), + ])->header('Content-Type', 'application/json'); + } +} diff --git a/src/SupervisorChecker.php b/src/SupervisorChecker.php new file mode 100644 index 0000000..d623405 --- /dev/null +++ b/src/SupervisorChecker.php @@ -0,0 +1,60 @@ +run(); + if (! $shellProcess->isSuccessful()) { + throw new ProcessFailedException($shellProcess); + } + $output = $shellProcess->getOutput(); + $processIsDown = Str::contains($output, [ + // http://supervisord.org/subprocess.html#process-states + 'STOPPED', + 'BACKOFF', + 'STOPPING', + 'EXITED', + 'FATAL', + 'UNKNOWN', + 'ERROR', + ]); + if ($processIsDown) { + $downProcesses[] = $supervisorProcess; + } + } + + return collect($downProcesses); + } + + /** + * @return bool + * @throws \Exception + */ + public function isServiceRunning(): bool + { + $command = '$(which systemctl) is-active --quiet supervisor'; + $shellProcess = Process::fromShellCommandline($command); + $shellProcess->run(); + + return $shellProcess->isSuccessful(); + } +} diff --git a/src/SupervisorDowntimeNotifier.php b/src/SupervisorDowntimeNotifier.php new file mode 100644 index 0000000..5f4a812 --- /dev/null +++ b/src/SupervisorDowntimeNotifier.php @@ -0,0 +1,117 @@ +isAllowedToRun()) { + $this->monitorSupervisorService(); + $this->monitorDownProcesses(); + } + } + + /** + * @return bool + * @throws \Okipa\LaravelSupervisorDowntimeNotifier\Exceptions\InvalidAllowedToRun + */ + public function isAllowedToRun(): bool + { + $allowedToRun = config('supervisor-downtime-notifier.allowed_to_run'); + if (is_callable($allowedToRun)) { + return $allowedToRun(); + } elseif (is_bool($allowedToRun)) { + return $allowedToRun; + } + throw new InvalidAllowedToRun('The `supervisor-downtime-notifier.allowed_to_run` config is not a ' + . 'boolean or a callable.'); + } + + /** + * @throws \Okipa\LaravelSupervisorDowntimeNotifier\Exceptions\SupervisorServiceNotStarted + */ + public function monitorSupervisorService(): void + { + if (! $this->getSupervisorChecker()->isServiceRunning()) { + $this->getNotifiable()->notify($this->getServiceNotStartedNotification()); + $onServiceNotStarted = $this->getServiceNotStartedCallback(); + if ($onServiceNotStarted) { + $onServiceNotStarted(); + } + } + } + + public function getSupervisorChecker(): SupervisorChecker + { + return app(config('supervisor-downtime-notifier.supervisor_checker')); + } + + public function getNotifiable(): Notifiable + { + return app(config('supervisor-downtime-notifier.notifiable')); + } + + public function getServiceNotStartedNotification(): ServiceNotStarted + { + return app(config('supervisor-downtime-notifier.notifications.service_not_started')); + } + + public function getServiceNotStartedCallback(): ?OnServiceNotStarted + { + $callbackClass = config('supervisor-downtime-notifier.callbacks.service_not_started'); + + return $callbackClass ? app($callbackClass) : null; + } + + /** + * @throws \Okipa\LaravelSupervisorDowntimeNotifier\Exceptions\SupervisorDownProcessesDetected + * @throws \Exception + */ + public function monitorDownProcesses(): void + { + $envSupervisorProcessConfig = $this->getEnvSupervisorProcessesConfig(); + if (! $envSupervisorProcessConfig) { + return; + } + $downProcesses = $this->getSupervisorChecker()->getDownProcesses($envSupervisorProcessConfig); + if ($downProcesses->isNotEmpty()) { + $this->getNotifiable()->notify($this->getDownProcessesNotification($downProcesses)); + $onDownProcesses = $this->getDownProcessesCallback(); + if ($onDownProcesses) { + $onDownProcesses($downProcesses); + } + } + } + + public function getEnvSupervisorProcessesConfig(): ?array + { + return config('supervisor-downtime-notifier.supervisor.' . app()->environment()); + } + + public function getDownProcessesNotification(Collection $downProcesses): ProcessesAreDown + { + return app(config('supervisor-downtime-notifier.notifications.down_processes'), compact('downProcesses')); + } + + public function getDownProcessesCallback(): ?OnDownProcesses + { + $callbackClass = config('supervisor-downtime-notifier.callbacks.down_processes'); + + return $callbackClass ? app($callbackClass) : null; + } +} diff --git a/src/SupervisorDowntimeNotifierServiceProvider.php b/src/SupervisorDowntimeNotifierServiceProvider.php new file mode 100644 index 0000000..99ff3fb --- /dev/null +++ b/src/SupervisorDowntimeNotifierServiceProvider.php @@ -0,0 +1,34 @@ +app->runningInConsole()) { + $this->commands([NotifySupervisorDownTime::class]); + } + $this->publishes([ + __DIR__ . '/../config/supervisor-downtime-notifier.php' => config_path('supervisor-downtime-notifier.php'), + ], 'supervisor-downtime-notifier:config'); + } + + /** + * Register any application services. + * + * @return void + */ + public function register(): void + { + $this->mergeConfigFrom(__DIR__ . '/../config/supervisor-downtime-notifier.php', 'supervisor-downtime-notifier'); + } +} diff --git a/tests/Dummy/AnotherNotifiable.php b/tests/Dummy/AnotherNotifiable.php new file mode 100644 index 0000000..b6d06dd --- /dev/null +++ b/tests/Dummy/AnotherNotifiable.php @@ -0,0 +1,10 @@ +faker = Factory::create(); + } +} diff --git a/tests/Unit/SupervisorMonitoringTest.php b/tests/Unit/SupervisorMonitoringTest.php new file mode 100644 index 0000000..28df560 --- /dev/null +++ b/tests/Unit/SupervisorMonitoringTest.php @@ -0,0 +1,348 @@ +set('supervisor-downtime-notifier.allowed_to_run', 'test'); + $this->expectException(InvalidAllowedToRun::class); + (new SupervisorDowntimeNotifier)->isAllowedToRun(); + } + + public function testAllowedToRunWithBoolean() + { + config()->set('supervisor-downtime-notifier.allowed_to_run', false); + $allowedToRun = (new SupervisorDowntimeNotifier)->isAllowedToRun(); + $this->assertEquals($allowedToRun, false); + } + + public function testAllowedToRunWithCallable() + { + config()->set('supervisor-downtime-notifier.allowed_to_run', function () { + return true; + }); + $allowedToRun = (new SupervisorDowntimeNotifier)->isAllowedToRun(); + $this->assertEquals($allowedToRun, true); + } + + public function testSetCustomSupervisorChecker() + { + config()->set('supervisor-downtime-notifier.supervisor_checker', AnotherSupervisorChecker::class); + $supervisorChecker = (new SupervisorDowntimeNotifier)->getSupervisorChecker(); + $this->assertInstanceOf(AnotherSupervisorChecker::class, $supervisorChecker); + } + + public function testSetCustomNotifiable() + { + config()->set('supervisor-downtime-notifier.notifiable', AnotherNotifiable::class); + $notifiable = (new SupervisorDowntimeNotifier)->getNotifiable(); + $this->assertInstanceOf(AnotherNotifiable::class, $notifiable); + } + + public function testGetCustomServiceNotStartedNotification() + { + config()->set( + 'supervisor-downtime-notifier.notifications.service_not_started', + AnotherServiceNotStarted::class + ); + $notification = (new SupervisorDowntimeNotifier)->getServiceNotStartedNotification(); + $this->assertInstanceOf(AnotherServiceNotStarted::class, $notification); + } + + public function testGetCustomDownProcessesNotification() + { + config()->set('supervisor-downtime-notifier.notifications.down_processes', AnotherProcessesAreDown::class); + $notification = (new SupervisorDowntimeNotifier)->getDownProcessesNotification(collect()); + $this->assertInstanceOf(AnotherProcessesAreDown::class, $notification); + } + + public function testGetCustomServiceNotStartedCallback() + { + config()->set('supervisor-downtime-notifier.callbacks.service_not_started', AnotherOnServiceNotStarted::class); + $callback = (new SupervisorDowntimeNotifier)->getServiceNotStartedCallback(); + $this->assertInstanceOf(AnotherOnServiceNotStarted::class, $callback); + } + + public function testGetCustomDownProcessesCallback() + { + config()->set('supervisor-downtime-notifier.callbacks.down_processes', AnotherOnDownProcesses::class); + $callback = (new SupervisorDowntimeNotifier)->getDownProcessesCallback(); + $this->assertInstanceOf(AnotherOnDownProcesses::class, $callback); + } + + public function testNothingHappensWhenNotAllowed() + { + $this->partialMock(SupervisorDowntimeNotifier::class, function ($mock) { + $mock->shouldReceive('monitorSupervisorService')->never(); + $mock->shouldReceive('monitorDownProcesses')->never(); + }); + config()->set('supervisor-downtime-notifier.allowed_to_run', false); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + NotificationFacade::assertNothingSent(); + } + + public function testNoNotificationIsSentWhenSupervisorServiceIsRunning() + { + $this->partialMock(SupervisorChecker::class, function ($mock) { + $mock->shouldReceive('isServiceRunning')->once()->andReturn(true); + }); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + NotificationFacade::assertNothingSent(); + } + + public function testNotificationIsSentWhenSupervisorServiceIsNotRunning() + { + $this->partialMock(SupervisorChecker::class, function ($mock) { + $mock->shouldReceive('isServiceRunning')->once()->andReturn(false); + }); + config()->set('supervisor-downtime-notifier.callbacks.service_not_started', null); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + NotificationFacade::assertSentTo(new Notifiable(), ServiceNotStarted::class); + } + + public function testCallbackIsTriggeredWhenSupervisorServiceIsNotRunning() + { + $this->partialMock(SupervisorChecker::class, function ($mock) { + $mock->shouldReceive('isServiceRunning')->once()->andReturn(false); + }); + $this->expectException(SupervisorServiceNotStarted::class); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + } + + public function testNoNotificationIsSentWhenSupervisorProcessesAreUp() + { + config()->set('supervisor-downtime-notifier.supervisor', [ + 'testing' => ['laravel-queue-testing-worker:*'], + ]); + $this->partialMock(SupervisorChecker::class, function ($mock) { + $mock->shouldReceive('isServiceRunning')->once()->andReturn(true); + $mock->shouldReceive('getDownProcesses')->once()->andReturn(collect()); + }); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + NotificationFacade::assertNothingSent(); + } + + public function testNotificationIsSentWhenSupervisorProcessesAreDown() + { + config()->set('supervisor-downtime-notifier.supervisor', [ + 'testing' => ['laravel-queue-testing-worker:*'], + ]); + config()->set('supervisor-downtime-notifier.callbacks.down_processes', null); + $this->partialMock(SupervisorChecker::class, function ($mock) { + $mock->shouldReceive('isServiceRunning')->once()->andReturn(true); + $mock->shouldReceive('getDownProcesses')->once()->andReturn(collect(['laravel-testing-process'])); + }); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + NotificationFacade::assertSentTo(new Notifiable(), ProcessesAreDown::class); + } + + public function testCallbackIsTriggeredWhenProcessesAreDown() + { + config()->set('supervisor-downtime-notifier.supervisor', [ + 'testing' => ['laravel-queue-testing-worker:*'], + ]); + $this->partialMock(SupervisorChecker::class, function ($mock) { + $mock->shouldReceive('isServiceRunning')->once()->andReturn(true); + $mock->shouldReceive('getDownProcesses')->once()->andReturn(collect(['laravel-testing-process'])); + }); + $this->expectException(SupervisorDownProcessesDetected::class); + $this->artisan('supervisor:downtime:notify')->assertExitCode(0); + } + + public function testDefaultServiceNotStartedMessage() + { + $notification = (new SupervisorDowntimeNotifier)->getServiceNotStartedNotification(); + $notifiable = (new SupervisorDowntimeNotifier)->getNotifiable(); + $notifiable->notify($notification); + NotificationFacade::assertSentTo( + new Notifiable(), + ServiceNotStarted::class, + function ($notification, $channels) { + $this->assertEquals(config('supervisor-downtime-notifier.channels'), $channels); + // mail + $mailData = $notification->toMail($channels)->toArray(); + $this->assertEquals('error', $mailData['level']); + $this->assertEquals('[Laravel - testing] supervisor service is not started', $mailData['subject']); + $this->assertEquals( + 'We have detected that the supervisor service is not started on ' + . '[Laravel - testing](http://localhost).', + $mailData['introLines'][0] + ); + $this->assertEquals( + 'Please restart you supervisor service connecting to your server and executing the ' + . '"supervisorctl restart" command line.', + $mailData['introLines'][1] + ); + // slack + $slackData = $notification->toSlack($channels); + $this->assertEquals('error', $slackData->level); + $this->assertEquals( + '⚠ `[Laravel - testing]` supervisor service is not started on http://localhost.', + $slackData->content + ); + // webhook + $webhookData = $notification->toWebhook($channels)->toArray(); + $this->assertEquals( + '⚠ `[Laravel - testing]` supervisor service is not started on http://localhost.', + $webhookData['data']['text'] + ); + + return true; + } + ); + } + + public function testDefaultServiceNotStartedCallbackExceptionMessage() + { + $callback = (new SupervisorDowntimeNotifier)->getServiceNotStartedCallback(); + $this->expectExceptionMessage('Supervisor service is not started.'); + $callback(); + } + + public function testDefaultProcessesAreDownNotificationSingularMessage() + { + $downProcesses = collect(['laravel-queue-testing-worker:*']); + $notification = (new SupervisorDowntimeNotifier)->getDownProcessesNotification($downProcesses); + $notifiable = (new SupervisorDowntimeNotifier)->getNotifiable(); + $notifiable->notify($notification); + NotificationFacade::assertSentTo( + new Notifiable(), + ProcessesAreDown::class, + function ($notification, $channels) { + $this->assertEquals(config('supervisor-downtime-notifier.channels'), $channels); + // mail + $mailData = $notification->toMail($channels)->toArray(); + $this->assertEquals('error', $mailData['level']); + $this->assertEquals( + '[Laravel - testing] 1 supervisor down process has been detected', + $mailData['subject'] + ); + $this->assertEquals( + 'We have detected 1 supervisor down process on [Laravel - testing](http://localhost): ' + . '"laravel-queue-testing-worker:*".', + $mailData['introLines'][0] + ); + $this->assertEquals( + 'Please check your down processes connecting to your server and executing the ' + . '"supervisorctl status" command.', + $mailData['introLines'][1] + ); + // slack + $slackData = $notification->toSlack($channels); + $this->assertEquals('error', $slackData->level); + $this->assertEquals( + '⚠ `[Laravel - testing]` 1 supervisor down process has been detected on http://localhost: ' + . '"laravel-queue-testing-worker:*".', + $slackData->content + ); + // webhook + $webhookData = $notification->toWebhook($channels)->toArray(); + $this->assertEquals( + '⚠ `[Laravel - testing]` 1 supervisor down process has been detected on http://localhost: ' + . '"laravel-queue-testing-worker:*".', + $webhookData['data']['text'] + ); + + return true; + } + ); + } + + public function testDefaultProcessesAreDownNotificationPluralMessage() + { + $downProcesses = collect([ + 'laravel-queue-testing-worker:process-1', + 'laravel-queue-testing-worker:process-2', + ]); + $notification = (new SupervisorDowntimeNotifier)->getDownProcessesNotification($downProcesses); + $notifiable = (new SupervisorDowntimeNotifier)->getNotifiable(); + $notifiable->notify($notification); + NotificationFacade::assertSentTo( + new Notifiable(), + ProcessesAreDown::class, + function ($notification, $channels) { + $this->assertEquals(config('supervisor-downtime-notifier.channels'), $channels); + // mail + $mailData = $notification->toMail($channels)->toArray(); + $this->assertEquals('error', $mailData['level']); + $this->assertEquals( + '[Laravel - testing] 2 supervisor down processes have been detected', + $mailData['subject'] + ); + $this->assertEquals( + 'We have detected 2 supervisor down processes on [Laravel - testing](http://localhost): ' + . '"laravel-queue-testing-worker:process-1", "laravel-queue-testing-worker:process-2".', + $mailData['introLines'][0] + ); + $this->assertEquals( + 'Please check your down processes connecting to your server and executing the ' + . '"supervisorctl status" command.', + $mailData['introLines'][1] + ); + // slack + $slackData = $notification->toSlack($channels); + $this->assertEquals('error', $slackData->level); + $this->assertEquals( + '⚠ `[Laravel - testing]` 2 supervisor down processes have been detected on http://localhost: ' + . '"laravel-queue-testing-worker:process-1", "laravel-queue-testing-worker:process-2".', + $slackData->content + ); + // webhook + $webhookData = $notification->toWebhook($channels)->toArray(); + $this->assertEquals( + '⚠ `[Laravel - testing]` 2 supervisor down processes have been detected on http://localhost: ' + . '"laravel-queue-testing-worker:process-1", "laravel-queue-testing-worker:process-2".', + $webhookData['data']['text'] + ); + + return true; + } + ); + } + + public function testDefaultDownProcessesCallbackExceptionSingularMessage() + { + $downProcesses = collect(['laravel-queue-testing-worker:*']); + $callback = (new SupervisorDowntimeNotifier)->getDownProcessesCallback(); + $this->expectExceptionMessage('Down supervisor process detected: "laravel-queue-testing-worker:*".'); + $callback($downProcesses); + } + + public function testDefaultDownProcessesCallbackExceptionPluralMessage() + { + $downProcesses = collect([ + 'laravel-queue-testing-worker:process-1', + 'laravel-queue-testing-worker:process-2', + ]); + $callback = (new SupervisorDowntimeNotifier)->getDownProcessesCallback(); + $this->expectExceptionMessage('Down supervisor processes detected: "laravel-queue-testing-worker:process-1", ' + . '"laravel-queue-testing-worker:process-2"'); + $callback($downProcesses); + } +} diff --git a/tests/database/migrations/2014_10_12_200000_create_failed_jobs_table.php b/tests/database/migrations/2014_10_12_200000_create_failed_jobs_table.php new file mode 100644 index 0000000..d432dff --- /dev/null +++ b/tests/database/migrations/2014_10_12_200000_create_failed_jobs_table.php @@ -0,0 +1,35 @@ +bigIncrements('id'); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('failed_jobs'); + } +}