├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ ├── php-cs-fixer.yml │ └── run-tests.yml ├── .php_cs.dist.php ├── .phpunit.result.cache ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── failed-job-monitor.php └── src ├── Exceptions └── InvalidConfiguration.php ├── FailedJobMonitorServiceProvider.php ├── FailedJobNotifier.php ├── Notifiable.php └── Notification.php /.editorconfig: -------------------------------------------------------------------------------- 1 | ; This file is for unifying the coding style for different editors and IDEs. 2 | ; More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | indent_size = 4 9 | indent_style = space 10 | end_of_line = lf 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: spatie 2 | -------------------------------------------------------------------------------- /.github/workflows/php-cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: Check & fix styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | php-cs-fixer: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | 15 | - name: Run PHP CS Fixer 16 | uses: docker://oskarstark/php-cs-fixer-ga 17 | with: 18 | args: --config=.php_cs.dist.php --allow-risky=yes 19 | 20 | - name: Commit changes 21 | uses: stefanzweifel/git-auto-commit-action@v4 22 | with: 23 | commit_message: Fix styling 24 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: run-tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | test: 9 | runs-on: ${{ matrix.os }} 10 | 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest] 15 | php: [8.2, 8.1, 8.0] 16 | laravel: ['7.*', '8.*', '9.*', '10.*', '11.*', '12.*'] 17 | dependency-version: [prefer-stable] 18 | include: 19 | - laravel: 10.* 20 | testbench: 8.* 21 | - laravel: 9.* 22 | testbench: 7.* 23 | - laravel: 8.* 24 | testbench: 6.* 25 | - laravel: 7.* 26 | testbench: 5.* 27 | - laravel: 11.* 28 | testbench: 9.* 29 | - laravel: 12.* 30 | testbench: 10.* 31 | exclude: 32 | - laravel: 10.* 33 | php: 8.0 34 | - laravel: 7.* 35 | php: 8.2 36 | - laravel: 7.* 37 | php: 8.1 38 | - laravel: 11.* 39 | php: 8.1 40 | - laravel: 11.* 41 | php: 8.0 42 | - laravel: 12.* 43 | php: 8.1 44 | - laravel: 12.* 45 | php: 8.0 46 | 47 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} 48 | 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v2 52 | 53 | - name: Setup PHP 54 | uses: shivammathur/setup-php@v2 55 | with: 56 | php-version: ${{ matrix.php }} 57 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick 58 | coverage: none 59 | 60 | - name: Install dependencies 61 | run: | 62 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 63 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 64 | 65 | - name: Execute tests 66 | run: vendor/bin/pest 67 | -------------------------------------------------------------------------------- /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /.phpunit.result.cache: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"\/Users\/alexandrumanase\/Documents\/repos\/laravel-failed-job-monitor\/tests\/FailedJobMonitorTest.php::it":0.004,"\/Users\/alexandrumanase\/Documents\/repos\/laravel-failed-job-monitor\/tests\/NotifiableTest.php::it":0.005}} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to laravel-failed-job-monitor will be documented in this file 4 | 5 | ## 4.0.0 - 2021-03-26 6 | 7 | - Require PHP 8+ 8 | - Drop support for PHP 7.x 9 | - Convert syntax to PHP 8 where possible 10 | - Drop support for Laravel 5.8, 6.x 11 | 12 | ## 3.4.0 - 2020-12-01 13 | 14 | - Support PHP 8 15 | - Drop support for PHP 7.2 16 | 17 | ## 3.3.2 - 2020-09-09 18 | 19 | - Support Laravel 8 20 | 21 | ## 3.3.1 - 2020-04-20 22 | 23 | - allow extending Illuminate notification class (#56) 24 | 25 | ## 3.3.0 - 2020-03-03 26 | 27 | - add support for Laravel 7 28 | 29 | ## 3.2.0 - 2019-09-04 30 | 31 | - make compatible with Laravel 6 32 | 33 | ## 3.1.0 - 2019-02-27 34 | 35 | - drop support for PHP 7.1 36 | - drop support for anything lower than Laravel 5.8 37 | 38 | ## 3.0.3 - 2019-02-27 39 | 40 | - add support for Laravel 5.8 41 | 42 | ## 3.0.2 - 2018-08-27 43 | 44 | - add support for Laravel 5.7 45 | 46 | ## 3.0.1 - 2018-02-13 47 | 48 | - add support for Laravel 5.6 49 | 50 | ## 3.0.0 - 2017-09-01 51 | 52 | - add support for Laravel 5.5 53 | - renamed config file from `laravel-failed-job-monitor` to `failed-job-monitor` 54 | 55 | ## 2.2.0 - 2017-01-23 56 | 57 | - add support for Laravel 5.4 58 | 59 | ## 2.1.1 - 2016-11-07 60 | 61 | - fix `InvalidConfiguration` exception 62 | 63 | ## 2.1.0 - 2016-11-07 64 | 65 | - add `notificationFilter` 66 | 67 | ## 2.0.0 - 2016-10-18 68 | 69 | - use Laravel 5.3's notifications 70 | 71 | ## 1.1.0 - 2016-09-11 72 | 73 | - the used view can now be customized 74 | 75 | ## 1.0.3 - 2016-08-27 76 | 77 | - upped the required Laravel version to 5.3 78 | 79 | ## 1.0.2 - 2016-03-02 80 | 81 | - Fix a bug where package would crash during a failed job 82 | 83 | ## 1.0.1 - 2016-02-26 84 | 85 | - Fix typo in config 86 | 87 | ## 1.0.0 - 2016-02-25 88 | 89 | - Initial release 90 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Get notified when a queued job fails 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-failed-job-monitor.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-failed-job-monitor) 4 | ![Test Status](https://img.shields.io/github/actions/workflow/status/spatie/laravel-failed-job-monitor/run-tests.yml?label=tests&style=flat-square) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-failed-job-monitor.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-failed-job-monitor) 7 | 8 | This package sends notifications if a queued job fails. Out of the box it can send a notification via mail and/or Slack. It leverages Laravel's native notification system. 9 | 10 | ## Support us 11 | 12 | [](https://spatie.be/github-ad-click/laravel-failed-job-monitor) 13 | 14 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 15 | 16 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 17 | 18 | ## Installation 19 | 20 | > For Laravel versions 5.8 and 6.x, use v3.x of this package. 21 | 22 | You can install the package via composer: 23 | 24 | ``` bash 25 | composer require spatie/laravel-failed-job-monitor 26 | ``` 27 | If you intend to use Slack notifications you should also install the guzzle client: 28 | 29 | ``` bash 30 | composer require guzzlehttp/guzzle 31 | ``` 32 | 33 | The service provider will automatically be registered. 34 | 35 | Next, you must publish the config file: 36 | 37 | ```bash 38 | php artisan vendor:publish --tag=failed-job-monitor-config 39 | ``` 40 | 41 | This is the contents of the default configuration file. Here you can specify the notifiable to which the notifications should be sent. The default notifiable will use the variables specified in this config file. 42 | 43 | ```php 44 | return [ 45 | 46 | /** 47 | * The notification that will be sent when a job fails. 48 | */ 49 | 'notification' => \Spatie\FailedJobMonitor\Notification::class, 50 | 51 | /** 52 | * The notifiable to which the notification will be sent. The default 53 | * notifiable will use the mail and slack configuration specified 54 | * in this config file. 55 | */ 56 | 'notifiable' => \Spatie\FailedJobMonitor\Notifiable::class, 57 | 58 | /* 59 | * By default notifications are sent for all failures. You can pass a callable to filter 60 | * out certain notifications. The given callable will receive the notification. If the callable 61 | * return false, the notification will not be sent. 62 | */ 63 | 'notificationFilter' => null, 64 | 65 | /** 66 | * The channels to which the notification will be sent. 67 | */ 68 | 'channels' => ['mail', 'slack'], 69 | 70 | 'mail' => [ 71 | 'to' => ['email@example.com'], 72 | ], 73 | 74 | 'slack' => [ 75 | 'webhook_url' => env('FAILED_JOB_SLACK_WEBHOOK_URL'), 76 | ], 77 | ]; 78 | ``` 79 | 80 | ## Configuration 81 | 82 | ### Customizing the notification 83 | 84 | The default notification class provided by this package has support for mail and Slack. 85 | 86 | If you want to customize the notification you can specify your own notification class in the config file. 87 | 88 | ```php 89 | // config/failed-job-monitor.php 90 | return [ 91 | ... 92 | 'notification' => \App\Notifications\CustomNotificationForFailedJobMonitor::class, 93 | ... 94 | ``` 95 | 96 | ### Customizing the notifiable 97 | 98 | The default notifiable class provided by this package use the `channels`, `mail` and `slack` keys from the `config` file to determine how notifications must be sent 99 | 100 | If you want to customize the notifiable you can specify your own notifiable class in the config file. 101 | 102 | ```php 103 | // config/failed-job-monitor.php 104 | return [ 105 | 'notifiable' => \App\CustomNotifiableForFailedJobMonitor::class, 106 | ... 107 | ``` 108 | 109 | ### Filtering the notifications 110 | 111 | To filter the notifications, pass a closure to the `notificationFilter`. 112 | 113 | ```php 114 | // config/failed-job-monitor.php 115 | return [ 116 | ... 117 | 'notificationFilter' => function (Spatie\FailedJobMonitor\Notification $notification): bool 118 | { 119 | return true; 120 | } 121 | ... 122 | ``` 123 | 124 | The above works only that Laravel doesn't support closure serialization. Thus you will get the following error when you run `php artisan config:cache` 125 | 126 | ``` 127 | LogicException : Your configuration files are not serializable. 128 | ``` 129 | 130 | It would thus be better to create a separate class and use a callable as the callback. 131 | 132 | 133 | ```php 134 | [App\Notifications\FailedJobNotification::class, 'notificationFilter'], 157 | ... 158 | ``` 159 | 160 | ## Usage 161 | 162 | If you configured the package correctly, you're done. You'll receive a notification when a queued job fails. 163 | 164 | ## Changelog 165 | 166 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 167 | 168 | ## Testing 169 | 170 | ``` bash 171 | composer test 172 | ``` 173 | 174 | ## Contributing 175 | 176 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 177 | 178 | ## Security 179 | 180 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 181 | 182 | ## Credits 183 | 184 | - [Freek Van der Herten](https://github.com/freekmurze) 185 | - [All Contributors](../../contributors) 186 | 187 | A big thank you to [Egor Talantsev](https://github.com/spyric) for his help creating `v2` of the package. 188 | 189 | ## License 190 | 191 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 192 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-failed-job-monitor", 3 | "description": "Get notified when a queued job fails", 4 | "keywords": [ 5 | "spatie", 6 | "queue", 7 | "job", 8 | "monitor", 9 | "notify", 10 | "slack", 11 | "laravel-failed-job-monitor" 12 | ], 13 | "homepage": "https://github.com/spatie/laravel-failed-job-monitor", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Freek Van der Herten", 18 | "email": "freek@spatie.be", 19 | "homepage": "https://spatie.be", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php": "^8.0", 25 | "laravel/framework": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 26 | "spatie/laravel-package-tools": "^1.6" 27 | }, 28 | "require-dev": { 29 | "mockery/mockery": "^1.4", 30 | "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0|^10.0", 31 | "pestphp/pest": "^1.22|^2.34|^3.7" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Spatie\\FailedJobMonitor\\": "src" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "Spatie\\FailedJobMonitor\\Test\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "test": "vendor/bin/pest" 45 | }, 46 | "suggest": { 47 | "guzzlehttp/guzzle": "Allows notifications to be sent to Slack", 48 | "laravel/slack-notification-channel": "Required for sending notifications via Slack" 49 | }, 50 | "extra": { 51 | "laravel": { 52 | "providers": [ 53 | "Spatie\\FailedJobMonitor\\FailedJobMonitorServiceProvider" 54 | ] 55 | } 56 | }, 57 | "minimum-stability": "dev", 58 | "prefer-stable": true, 59 | "config": { 60 | "allow-plugins": { 61 | "pestphp/pest-plugin": true 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config/failed-job-monitor.php: -------------------------------------------------------------------------------- 1 | \Spatie\FailedJobMonitor\Notification::class, 9 | 10 | /* 11 | * The notifiable to which the notification will be sent. The default 12 | * notifiable will use the mail and slack configuration specified 13 | * in this config file. 14 | */ 15 | 'notifiable' => \Spatie\FailedJobMonitor\Notifiable::class, 16 | 17 | /* 18 | * By default notifications are sent for all failures. You can pass a callable to filter 19 | * out certain notifications. The given callable will receive the notification. If the callable 20 | * return false, the notification will not be sent. 21 | */ 22 | 'notificationFilter' => null, 23 | 24 | /* 25 | * The channels to which the notification will be sent. 26 | */ 27 | 'channels' => ['mail', 'slack'], 28 | 29 | 'mail' => [ 30 | 'to' => ['email@example.com'], 31 | ], 32 | 33 | 'slack' => [ 34 | 'webhook_url' => env('FAILED_JOB_SLACK_WEBHOOK_URL'), 35 | ], 36 | ]; 37 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidConfiguration.php: -------------------------------------------------------------------------------- 1 | name('laravel-failed-job-monitor') 14 | ->hasConfigFile(); 15 | } 16 | 17 | public function packageBooted(): void 18 | { 19 | $this->app->make(FailedJobNotifier::class)->register(); 20 | } 21 | 22 | public function packageRegistered(): void 23 | { 24 | $this->app->singleton(FailedJobNotifier::class); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/FailedJobNotifier.php: -------------------------------------------------------------------------------- 1 | failing(function (JobFailed $event) { 15 | $notifiable = app(config('failed-job-monitor.notifiable')); 16 | 17 | $notification = app(config('failed-job-monitor.notification'))->setEvent($event); 18 | 19 | if (! $this->isValidNotificationClass($notification)) { 20 | throw InvalidConfiguration::notificationClassInvalid(get_class($notification)); 21 | } 22 | 23 | if ($this->shouldSendNotification($notification)) { 24 | $notifiable->notify($notification); 25 | } 26 | }); 27 | } 28 | 29 | public function isValidNotificationClass($notification): bool 30 | { 31 | if (get_class($notification) === Notification::class) { 32 | return true; 33 | } 34 | 35 | if (is_subclass_of($notification, IlluminateNotification::class)) { 36 | return true; 37 | } 38 | 39 | return false; 40 | } 41 | 42 | public function shouldSendNotification($notification): bool 43 | { 44 | $callable = config('failed-job-monitor.notificationFilter'); 45 | 46 | if (! is_callable($callable)) { 47 | return true; 48 | } 49 | 50 | return $callable($notification); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Notifiable.php: -------------------------------------------------------------------------------- 1 | event = $event; 23 | 24 | return $this; 25 | } 26 | 27 | public function getEvent(): JobFailed 28 | { 29 | return $this->event; 30 | } 31 | 32 | public function toMail($notifiable): MailMessage 33 | { 34 | return (new MailMessage()) 35 | ->error() 36 | ->subject('A job failed at '.config('app.url')) 37 | ->line("Exception message: {$this->event->exception->getMessage()}") 38 | ->line("Job class: {$this->event->job->resolveName()}") 39 | ->line("Job body: {$this->event->job->getRawBody()}") 40 | ->line("Exception: {$this->event->exception->getTraceAsString()}") 41 | ->when(config('horizon') !== null, fn (MailMessage $mailMessage) => $mailMessage->action('View Error', url(config('horizon.path').'/failed/'.$this->event->job->getJobId()))); 42 | } 43 | 44 | public function toSlack(): SlackMessage 45 | { 46 | return (new SlackMessage()) 47 | ->error() 48 | ->content('A job failed at '.config('app.url')) 49 | ->attachment(function (SlackAttachment $attachment) { 50 | $attachment->fields([ 51 | 'Exception message' => $this->event->exception->getMessage(), 52 | 'Job class' => $this->event->job->resolveName(), 53 | 'Job body' => $this->event->job->getRawBody(), 54 | 'Exception' => $this->event->exception->getTraceAsString(), 55 | ]); 56 | }); 57 | } 58 | } 59 | --------------------------------------------------------------------------------