├── .all-contributorsrc ├── .github └── workflows │ └── run-tests.yml ├── .gitignore ├── Licence.txt ├── README.md ├── composer.json ├── phpcs.xml ├── phpunit.xml ├── src ├── AlertNotificationsServiceProvider.php ├── Dispatcher │ ├── AlertDispatcher.php │ ├── ThrottleControl.php │ └── Webhook.php ├── Mail │ └── ExceptionOccurredMail.php ├── MicrosoftTeams │ ├── ExceptionOccurredCard.php │ └── Teams.php ├── PagerDuty │ └── .gitkeep ├── Slack │ ├── ExceptionOccurredPayload.php │ └── Slack.php ├── config │ └── laravel_alert_notifications.php └── views │ └── mail.blade.php └── tests ├── AlertDispatcherTest.php ├── NotificationLevelTest.php └── ThrottleControlTest.php /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "abrigham1", 10 | "name": "Aaron Brigham", 11 | "avatar_url": "https://avatars0.githubusercontent.com/u/7387512?v=4", 12 | "profile": "http://www.abrigham.com", 13 | "contributions": [ 14 | "test", 15 | "code" 16 | ] 17 | }, 18 | { 19 | "login": "AlexHupe", 20 | "name": "Alexander Hupe", 21 | "avatar_url": "https://avatars1.githubusercontent.com/u/6893843?v=4", 22 | "profile": "https://github.com/AlexHupe", 23 | "contributions": [ 24 | "review", 25 | "test", 26 | "code" 27 | ] 28 | }, 29 | { 30 | "login": "kitloong", 31 | "name": "Kit Loong", 32 | "avatar_url": "https://avatars2.githubusercontent.com/u/7660346?v=4", 33 | "profile": "https://github.com/kitloong", 34 | "contributions": [ 35 | "review", 36 | "test", 37 | "code" 38 | ] 39 | }, 40 | { 41 | "login": "ikari7789", 42 | "name": "Andrew Miller", 43 | "avatar_url": "https://avatars1.githubusercontent.com/u/1041215?v=4", 44 | "profile": "http://www.standingmist.com", 45 | "contributions": [ 46 | "review", 47 | "test", 48 | "code" 49 | ] 50 | }, 51 | { 52 | "login": "kevincobain2000", 53 | "name": "Pulkit Kathuria", 54 | "avatar_url": "https://avatars2.githubusercontent.com/u/629055?v=4", 55 | "profile": "https://kevincobain2000.github.io", 56 | "contributions": [ 57 | "review", 58 | "test", 59 | "code" 60 | ] 61 | }, 62 | { 63 | "login": "masashi1014", 64 | "name": "Masashi", 65 | "avatar_url": "https://avatars0.githubusercontent.com/u/49443783?v=4", 66 | "profile": "https://github.com/masashi1014", 67 | "contributions": [ 68 | "test" 69 | ] 70 | } 71 | ], 72 | "contributorsPerLine": 7, 73 | "projectName": "laravel-alert-notifications", 74 | "projectOwner": "kevincobain2000", 75 | "repoType": "github", 76 | "repoHost": "https://github.com", 77 | "skipCi": true 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: [fork, pull_request, push, workflow_dispatch] 4 | 5 | jobs: 6 | tests: 7 | name: PHP ${{ matrix.php-version }} - Laravel ${{ matrix.laravel-version }} - ${{ matrix.dependency-version }} 8 | 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | # Enable all tests to run to ease finding multiple issues at once 13 | fail-fast: false 14 | 15 | matrix: 16 | php-version: ['7.3', '7.4', '8.0', '8.1', '8.2', '8.3'] 17 | laravel-version: ['^7.0', '^8.79', '^9.33', '^10.0', '^11.0'] 18 | dependency-version: [prefer-lowest, prefer-stable] 19 | exclude: 20 | - php-version: '7.3' 21 | laravel-version: '^9.33' 22 | - php-version: '7.3' 23 | laravel-version: '^10.0' 24 | - php-version: '7.3' 25 | laravel-version: '^11.0' 26 | - php-version: '7.4' 27 | laravel-version: '^9.33' 28 | - php-version: '7.4' 29 | laravel-version: '^10.0' 30 | - php-version: '7.4' 31 | laravel-version: '^11.0' 32 | - php-version: '8.0' 33 | laravel-version: '^10.0' 34 | - php-version: '8.0' 35 | laravel-version: '^11.0' 36 | - php-version: '8.1' 37 | laravel-version: '^7.0' 38 | - php-version: '8.1' 39 | laravel-version: '^11.0' 40 | - php-version: '8.2' 41 | laravel-version: '^7.0' 42 | - php-version: '8.2' 43 | laravel-version: '^8.79' 44 | - php-version: '8.3' 45 | laravel-version: '^7.0' 46 | - php-version: '8.3' 47 | laravel-version: '^8.79' 48 | 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | 53 | - name: Install PHP with extensions 54 | uses: shivammathur/setup-php@v2 55 | with: 56 | coverage: none 57 | extensions: intl 58 | ini-values: memory_limit=-1 59 | php-version: ${{ matrix.php-version }} 60 | 61 | - name: Determine composer cache directory 62 | id: determine-composer-cache-directory 63 | run: echo "directory=$(composer config cache-dir)" >> $GITHUB_OUTPUT 64 | 65 | - name: Update composer.json for the build 66 | run: composer require --no-interaction --no-update "illuminate/support:${{ matrix.laravel-version }}" 67 | 68 | - name: Cache dependencies installed with composer 69 | uses: actions/cache@v4 70 | with: 71 | path: ${{ steps.determine-composer-cache-directory.outputs.directory }} 72 | key: php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-${{ matrix.dependency-version }}-composer-${{ hashFiles('**/composer.json') }} 73 | restore-keys: php-${{ matrix.php-version }}-laravel-${{ matrix.laravel-version }}-${{ matrix.dependency-version }}-composer- 74 | 75 | - name: Install dependencies with composer 76 | run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 77 | 78 | - name: Setup Problem Matchers 79 | run: | 80 | echo "::add-matcher::${{ runner.tool_cache }}/php.json" 81 | echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json" 82 | 83 | - name: Run tests with phpunit/phpunit 84 | run: vendor/bin/phpunit 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .idea 4 | .php_cs.cache 5 | .phpintel 6 | .phpunit.result.cache 7 | /.idea 8 | /.phpunit.cache 9 | /.vagrant 10 | /_ide_helper.php 11 | /node_modules 12 | /storage/*.key 13 | /vendor 14 | Homestead.json 15 | Homestead.yaml 16 | build/ 17 | cache.properties 18 | composer.lock 19 | npm-debug.log 20 | -------------------------------------------------------------------------------- /Licence.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Pulkit Kathuria 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | Laravel Alert Notifcations 4 |

5 |

6 | Send php exceptions to email, microsoft teams, slack. 7 |
8 | Notifications are throttle enabled so devs don't get a lot of emails from one host 9 |
10 | (or all hosts if cache driver is shared). 11 |

12 | 13 | 14 | 15 | [![All Contributors](https://img.shields.io/badge/all_contributors-6-orange.svg?style=flat-square)](#contributors-) 16 | 17 | 18 | 19 | | Channels | Progress | 20 | | :------- | :--------- | 21 | | Email | Supported | 22 | | Microsoft Teams | Supported | 23 | | Slack | Supported | 24 | | Pager Duty | Open to pull requests | 25 | 26 | ### Installation 27 | 28 | ``` 29 | composer require kevincobain2000/laravel-alert-notifications 30 | ``` 31 | 32 | >If you're using Laravel 5.5+ let the package auto discovery make this for you. 33 | 34 | ``` 35 | 'providers' => [ 36 | \Kevincobain2000\LaravelAlertNotifications\AlertNotificationsServiceProvider::class 37 | ] 38 | ``` 39 | 40 | ### Tests 41 | 42 | ``` 43 | composer install 44 | composer run test 45 | ``` 46 | 47 | ### Publish (Laravel) 48 | 49 | ``` 50 | php artisan vendor:publish --provider="Kevincobain2000\LaravelAlertNotifications\AlertNotificationsServiceProvider" 51 | php artisan config:cache 52 | ``` 53 | 54 | ### Publish (Lumen) 55 | 56 | Since Lumen doesn't support auto-discovery, move config and view files to the destination directories manually 57 | 58 | ```shell 59 | cp vendor/kevincobain2000/laravel-alert-notifications/src/config/laravel_alert_notifications.php config/laravel_alert_notifications.php 60 | mkdir -p "resources/views/vendor/laravel_alert_notifications/" && cp vendor/kevincobain2000/laravel-alert-notifications/src/views/* resources/views/vendor/laravel_alert_notifications/ 61 | ``` 62 | 63 | and add the following to bootstrap/app.php: 64 | 65 | ```php 66 | $app->register(Kevincobain2000\LaravelAlertNotifications\AlertNotificationsServiceProvider::class); 67 | ``` 68 | 69 | ### .env 70 | 71 | ``` 72 | ALERT_NOTIFICATION_MAIL_FROM_ADDRESS= 73 | ALERT_NOTIFICATION_MAIL_TO_ADDRESS= 74 | ALERT_NOTIFICATION_MAIL_NOTICE_TO_ADDRESS= 75 | ALERT_NOTIFICATION_MAIL_WARNING_TO_ADDRESS= 76 | ALERT_NOTIFICATION_MAIL_ERROR_TO_ADDRESS= 77 | ALERT_NOTIFICATION_MAIL_CRITICAL_TO_ADDRESS= 78 | ALERT_NOTIFICATION_MAIL_ALERT_TO_ADDRESS= 79 | ALERT_NOTIFICATION_MAIL_EMERGENCY_TO_ADDRESS= 80 | ALERT_NOTIFICATION_CACHE_DRIVER=file 81 | ALERT_NOTIFICATION_MICROSOFT_TEAMS_WEBHOOK= 82 | ALERT_NOTIFICATION_SLACK_WEBHOOK= 83 | ALERT_NOTIFICATION_CURL_PROXY= 84 | ``` 85 | 86 | ### Usage 87 | 88 | ```php 89 | new AlertDispatcher( 90 | Exception $e 91 | [, array $dontAlertExceptions = []] // Exceptions that shouldn't trigger notifications 92 | [, array $notificationLevelsMapping = []] // [Exception class => Notification level] mapping 93 | [, array $exceptionContext = []] // Array of context data 94 | ) 95 | ``` 96 | 97 | In **app/Exceptions/Handler.php**. It is better to use a try catch to prevent loop. 98 | 99 | ```php 100 | use Kevincobain2000\LaravelAlertNotifications\Dispatcher\AlertDispatcher; 101 | 102 | class Handler extends ExceptionHandler 103 | { 104 | private $exceptionLogLevels = [ 105 | DebugLevelException::class => LogLevel::DEBUG, 106 | WarningLevelException::class => LogLevel::WARNING, 107 | ErrorLevelException::class => LogLevel::ERROR, 108 | ]; 109 | 110 | protected $dontReport = [ 111 | // 112 | ]; 113 | 114 | public function report(Throwable $exception) 115 | { 116 | try { 117 | $dontReport = array_merge($this->dontReport, $this->internalDontReport); 118 | $alertDispatcher = new AlertDispatcher($exception, $dontReport, $this->exceptionLogLevels); 119 | $alertDispatcher->notify(); 120 | } catch (Throwable $e) { 121 | // log any unexpected exceptions or do nothing 122 | } 123 | parent::report($exception); 124 | } 125 | } 126 | ``` 127 | 128 | ### Config 129 | 130 | | config/env key | purpose | 131 | | :---------- | :-------------- | 132 | | throttle_enabled | (default true) If false then library will send alerts without any throttling | 133 | | throttle_duration_minutes | (default 5 mins) If an exception has been notified | 134 | | | This will next notify after 5 mins when same exception occurs | 135 | | cache_prefix | This is a prefix for cache key. Your cache key will look like | 136 | | | ``laravel-alert-notifications-ExceptionClass-ExceptionCode`` | 137 | | ALERT_NOTIFICATION_CURL_PROXY | If your slack/MS teams require proxy, then set it up accordingly | 138 | | default_notification_level | Default notification level | 139 | | exclude_notification_levels | Do not send notification if it is of one of the listed level | 140 | | mail | E-mail config array: | 141 | | mail.enabled | (default true), false will not notify to email | 142 | | mail.fromAddress | (default null), null will not notify to email | 143 | | mail.toAddress | Default recipient e-mail address | 144 | | mail.subject | Default e-mail subject. May contain placeholders replaced afterwards with | 145 | | | correspondent exception data: | 146 | | | ``%ExceptionMessage%`` => ``$e->getMessage()`` | 147 | | | ``%ExceptionCode%`` => ``$e->getCode()`` | 148 | | | ``%ExceptionType%`` => ``$e->getType()`` | 149 | | | ``%ExceptionLevel%`` => ``current notification level`` | 150 | | | ex. ``'subject' => 'Exception [%ExceptionType%] has ocurred``' | | 151 | | mail.#level# | Configs for each notification level | 152 | | | notification levels refer to those defined in ``\Psr\Log\LogLevel`` | 153 | | mail.#level#.toAddress | (default mail.to_address), #level# notification recipient e-mail | 154 | | mail.#level#.subject | #level# notification e-mail subject | 155 | | microsoft_teams.enabled | (default true), false will not notify to teams | 156 | | microsoft_teams.webhook | (default null), null will not notify to teams | 157 | | slack.enabled | (default true), false will not notify to slack | 158 | | slack.webhook | (default null), null will not notify to slack | 159 | 160 | 161 | ### Samples 162 | 163 | #### Email 164 | 165 | Email 166 | 167 | #### Teams 168 | 169 | Teams 170 | 171 | #### Slack 172 | 173 | Slack 174 | 175 | ### References 176 | 177 | 1. https://qiita.com/kidatti/items/8732114ec4d1727844b8 178 | 2. https://laravel-news.com/email-on-error-exceptions 179 | 180 | ## Contributors ✨ 181 | 182 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 |

Aaron Brigham

⚠️ 💻

Alexander Hupe

👀 ⚠️ 💻

Kit Loong

👀 ⚠️ 💻

Andrew Miller

👀 ⚠️ 💻

Pulkit Kathuria

👀 ⚠️ 💻

Masashi

⚠️
197 | 198 | 199 | 200 | 201 | 202 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 203 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kevincobain2000/laravel-alert-notifications", 3 | "description": "Alert notifications of exceptions from your laravel application", 4 | "license": "MIT", 5 | "keywords": [ 6 | "email", 7 | "exception", 8 | "laravel", 9 | "laravel-package", 10 | "exception-handling", 11 | "debugging" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Pulkit Kathuria", 16 | "email": "kevincobain2000@gmail.com" 17 | } 18 | ], 19 | "minimum-stability": "stable", 20 | "require": { 21 | "php": "^7.3|^8.0", 22 | "guzzlehttp/guzzle": "^6.3|^7.0", 23 | "illuminate/support": "^7.0|^8.79|^9.33|^10.0|^11.0" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^9.0|^10.0", 27 | "squizlabs/php_codesniffer": "^3.0", 28 | "orchestra/testbench": "^5.0|^6.0|^7.0|^8.0|^9.0", 29 | "mockery/mockery": "^1.3.2" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Kevincobain2000\\LaravelAlertNotifications\\": "src/" 34 | } 35 | }, 36 | "scripts": { 37 | "test": "./vendor/bin/phpunit" 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Kevincobain2000\\LaravelAlertNotifications\\Tests\\": "tests/" 42 | } 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Kevincobain2000\\LaravelAlertNotifications\\AlertNotificationsServiceProvider" 48 | ] 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Standard Based on PSR2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | ./src/Dispatcher 14 | 15 | 16 | 17 | 18 | ./tests 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/AlertNotificationsServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadViewsFrom(__DIR__.'/views', 'laravel_alert_notifications'); 24 | 25 | $this->publishes([ 26 | self::CONFIG_PATH => base_path('config/laravel_alert_notifications.php'), 27 | ], 'config'); 28 | $this->publishes([ 29 | __DIR__.'/views' => resource_path('views/vendor/laravel_alert_notifications'), 30 | ], 'views'); 31 | 32 | if (app() instanceof \Laravel\Lumen\Application) { 33 | app()->configure('laravel_alert_notifications'); 34 | } 35 | } 36 | 37 | /** 38 | * Register the application services. 39 | */ 40 | public function register() 41 | { 42 | $this->mergeConfigFrom( 43 | self::CONFIG_PATH, 44 | 'laravel_alert_notifications' 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Dispatcher/AlertDispatcher.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 27 | $this->exceptionContext = $exceptionContext; 28 | $this->dontAlertExceptions = $dontAlertExceptions; 29 | $this->notificationLevel = $notificationLevelsMapping[get_class($exception)] 30 | ?? config('laravel_alert_notifications.default_notification_level'); 31 | } 32 | 33 | public function notify() 34 | { 35 | if ($this->shouldAlert()) { 36 | return $this->dispatch(); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | protected function shouldAlert() 43 | { 44 | $levelsNotToNotify = config('laravel_alert_notifications.exclude_notification_levels') ?? []; 45 | if ($this->isDonotAlertException() || in_array($this->notificationLevel, $levelsNotToNotify)) { 46 | return false; 47 | } 48 | 49 | if (! config('laravel_alert_notifications.throttle_enabled')) { 50 | return true; 51 | } 52 | 53 | return ! ThrottleControl::isThrottled($this->exception); 54 | } 55 | 56 | protected function dispatch() 57 | { 58 | if ($this->shouldMail()) { 59 | Mail::send(new ExceptionOccurredMail($this->exception, $this->notificationLevel, $this->exceptionContext)); 60 | } 61 | 62 | if ($this->shouldMicrosoftTeams()) { 63 | Teams::send(new ExceptionOccurredCard($this->exception, $this->exceptionContext)); 64 | } 65 | 66 | if ($this->shouldSlack()) { 67 | Slack::send(new ExceptionOccurredPayload($this->exception, $this->exceptionContext)); 68 | } 69 | 70 | return true; 71 | } 72 | 73 | protected function isDonotAlertException(): bool 74 | { 75 | return in_array(get_class($this->exception), $this->dontAlertExceptions); 76 | } 77 | 78 | protected function shouldMail(): bool 79 | { 80 | return config('laravel_alert_notifications.mail.enabled') 81 | && config('laravel_alert_notifications.mail.toAddress'); 82 | } 83 | 84 | protected function shouldMicrosoftTeams(): bool 85 | { 86 | return config('laravel_alert_notifications.microsoft_teams.enabled') 87 | && config('laravel_alert_notifications.microsoft_teams.webhook'); 88 | } 89 | 90 | protected function shouldSlack(): bool 91 | { 92 | return config('laravel_alert_notifications.slack.enabled') 93 | && config('laravel_alert_notifications.slack.webhook'); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Dispatcher/ThrottleControl.php: -------------------------------------------------------------------------------- 1 | has($key)) { 18 | return true; 19 | } 20 | 21 | Cache::store($driver)->put($key, true, self::nowAddMinutes()); 22 | 23 | return false; 24 | } 25 | 26 | public static function getThrottleCacheKey(Throwable $exception) 27 | { 28 | $key = config('laravel_alert_notifications.cache_prefix') 29 | .get_class($exception) 30 | .'-' 31 | .$exception->getCode(); 32 | 33 | return $key; 34 | } 35 | 36 | private static function nowAddMinutes(): DateTime 37 | { 38 | $dateTime = new DateTime(); 39 | $minutesToAdd = config('laravel_alert_notifications.throttle_duration_minutes'); 40 | $dateTime->modify("+{$minutesToAdd} minutes"); 41 | 42 | return $dateTime; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Dispatcher/Webhook.php: -------------------------------------------------------------------------------- 1 | request($method, $url, [ 13 | 'headers' => [ 14 | 'Content-Type' => 'application/json', 15 | ], 16 | 'proxy' => $proxy, 17 | 'connect_timeout' => 5.0, 18 | 'timeout' => 5.0, 19 | 'body' => json_encode($body), 20 | ]); 21 | 22 | return $result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Mail/ExceptionOccurredMail.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 25 | $this->exceptionContext = $exceptionContext; 26 | $this->notificationLevel = $notificationLevel; 27 | } 28 | 29 | public function build() 30 | { 31 | $configPrefix = 'laravel_alert_notifications.mail.'.$this->notificationLevel.'.'; 32 | 33 | $from = config('laravel_alert_notifications.mail.fromAddress'); 34 | $to = config($configPrefix.'toAddress') ?? config('laravel_alert_notifications.mail.toAddress'); 35 | $subject = config($configPrefix.'subject') ?? config('laravel_alert_notifications.mail.subject'); 36 | $subject = $this->replaceSubjectPlaceholders($subject); 37 | 38 | $data = [ 39 | 'exception' => $this->exception, 40 | 'context' => $this->exceptionContext 41 | ]; 42 | 43 | return $this->subject($subject)->from($from)->to($to)->with($data); 44 | } 45 | 46 | protected function replaceSubjectPlaceholders(string $subject): string 47 | { 48 | $subject = str_replace('%ExceptionType%', get_class($this->exception), $subject); 49 | $subject = str_replace('%ExceptionMessage%', $this->exception->getMessage(), $subject); 50 | $subject = str_replace('%ExceptionCode%', $this->exception->getCode(), $subject); 51 | $subject = str_replace('%ExceptionLevel%', ucfirst($this->notificationLevel), $subject); 52 | 53 | return $subject; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/MicrosoftTeams/ExceptionOccurredCard.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 15 | $this->exceptionContext = $exceptionContext; 16 | } 17 | 18 | public function getCard() 19 | { 20 | return [ 21 | 'type' => 'message', 22 | 'attachments' => [ 23 | [ 24 | 'contentType' => 'application/vnd.microsoft.card.adaptive', 25 | 'contentUrl' => null, 26 | 'content' => [ 27 | '\$schema' => 'http://adaptivecards.io/schemas/adaptive-card.json', 28 | 'type' => 'AdaptiveCard', 29 | 'version' => '1.4', 30 | 'accentColor' => 'bf0000', 31 | 'body' => [ 32 | [ 33 | 'type' => 'TextBlock', 34 | 'text' => config('laravel_alert_notifications.microsoft_teams.cardSubject'), 35 | 'id' => 'title', 36 | 'size' => 'large', 37 | 'weight' => 'bolder', 38 | 'color' => 'accent', 39 | ], 40 | [ 41 | 'type' => 'FactSet', 42 | 'facts' => [ 43 | [ 44 | 'title' => 'Environment:', 45 | 'value' => config('app.env'), 46 | ], 47 | [ 48 | 'title' => 'Server:', 49 | 'value' => Request::server('SERVER_NAME'), 50 | ], 51 | [ 52 | 'title' => 'Request Url:', 53 | 'value' => Request::fullUrl(), 54 | ], 55 | [ 56 | 'title' => 'Exception:', 57 | 'value' => get_class($this->exception), 58 | ], 59 | [ 60 | 'title' => 'Message:', 61 | 'value' => $this->exception->getMessage(), 62 | ], 63 | [ 64 | 'title' => 'Exception Code:', 65 | 'value' => $this->exception->getCode(), 66 | ], 67 | [ 68 | 'title' => 'In File:', 69 | 'value' => $this->exception->getFile() .' on line '.$this->exception->getLine(), 70 | ], 71 | ], 72 | 'id' => 'acFactSet', 73 | ], 74 | [ 75 | 'type' => 'CodeBlock', 76 | 'codeSnippet' => $this->exception->getTraceAsString(), 77 | 'fontType' => 'monospace', 78 | 'wrap' => true, 79 | ], 80 | ], 81 | 'msteams' => [ 82 | 'width' => 'Full', 83 | ], 84 | ], 85 | ], 86 | ], 87 | ]; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/MicrosoftTeams/Teams.php: -------------------------------------------------------------------------------- 1 | getCard(); 14 | 15 | return Webhook::send($url, $body, $proxy); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/PagerDuty/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevincobain2000/laravel-alert-notifications/2981b85a21fdc60da533cc5cdb7672e239b40ce2/src/PagerDuty/.gitkeep -------------------------------------------------------------------------------- /src/Slack/ExceptionOccurredPayload.php: -------------------------------------------------------------------------------- 1 | exception = $exception; 15 | $this->exceptionContext = $exceptionContext; 16 | } 17 | 18 | public function getCard() 19 | { 20 | return [ 21 | 'username' => config('laravel_alert_notifications.slack.username'), 22 | 'channel' => config('laravel_alert_notifications.slack.channel'), 23 | 'icon_emoji' => config('laravel_alert_notifications.slack.emoji'), 24 | 'attachments' => [ 25 | [ 26 | 'text' => '*Environment:* '.config('app.env') 27 | .' '.config('laravel_alert_notifications.slack.subject') 28 | .'\n'.'Server: '.Request::server('SERVER_NAME') 29 | .'\n'.'Request Url: '.Request::fullUrl() 30 | .'\n'.'Message: '.$this->exception->getMessage() 31 | .'\n'.'Exception: '.get_class($this->exception) 32 | .'\n'.'Exception Code: '.$this->exception->getCode() 33 | .'\n'.'In File: *'.$this->exception->getFile().'* on line '.$this->exception->getLine() 34 | .'\n'.'Context: '.'```\$context = '.var_export($this->exceptionContext, true).';```' 35 | .'\n'.'Stack Trace: '.'```'.$this->exception->getTraceAsString().'```', 'mrkdwn' => true, 36 | ], 37 | ], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Slack/Slack.php: -------------------------------------------------------------------------------- 1 | getCard(); 14 | 15 | return Webhook::send($url, $body, $proxy); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/config/laravel_alert_notifications.php: -------------------------------------------------------------------------------- 1 | true, 5 | 'throttle_duration_minutes' => 5, 6 | 'cache_driver' => env('ALERT_NOTIFICATION_CACHE_DRIVER', 'file'), 7 | 'cache_prefix' => env('ALERT_NOTIFICATION_CACHE_PREFIX', 'laravel-alert-notifications'), 8 | 'default_notification_level' => 'error', 9 | 'exclude_notification_levels' => ['debug', 'info'], 10 | 'mail' => [ 11 | 'enabled' => true, 12 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_TO_ADDRESS'), 13 | 'fromAddress' => env('ALERT_NOTIFICATION_MAIL_FROM_ADDRESS'), 14 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 15 | 'notice' => [ 16 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_NOTICE_TO_ADDRESS'), 17 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 18 | ], 19 | 'warning' => [ 20 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_WARNING_TO_ADDRESS'), 21 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 22 | ], 23 | 'error' => [ 24 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_ERROR_TO_ADDRESS'), 25 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 26 | ], 27 | 'critical' => [ 28 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_CRITICAL_TO_ADDRESS'), 29 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 30 | ], 31 | 'alert' => [ 32 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_ALERT_TO_ADDRESS'), 33 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 34 | ], 35 | 'emergency' => [ 36 | 'toAddress' => env('ALERT_NOTIFICATION_MAIL_EMERGENCY_TO_ADDRESS'), 37 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 38 | ], 39 | ], 40 | 'microsoft_teams' => [ 41 | 'enabled' => true, 42 | 'proxy' => env('ALERT_NOTIFICATION_CURL_PROXY', null), 43 | 'themeColor' => 'ff5864', 44 | 'cardSubject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 45 | 'webhook' => env('ALERT_NOTIFICATION_MICROSOFT_TEAMS_WEBHOOK'), 46 | ], 47 | 'slack' => [ 48 | 'enabled' => true, 49 | 'proxy' => env('ALERT_NOTIFICATION_CURL_PROXY', null), 50 | 'subject' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 51 | 'username' => '['.env('APP_ENV').'] ['.trim(`hostname`).'] '.env('APP_NAME'), 52 | 'emoji' => ':slack:', 53 | 'webhook' => env('ALERT_NOTIFICATION_SLACK_WEBHOOK', null), 54 | 'channel' => env('ALERT_NOTIFICATION_SLACK_CHANNEL', null), 55 | 'image' => null, 56 | ], 57 | 'pager_duty' => [ 58 | 'enabled' => true, 59 | ], 60 | ]; 61 | -------------------------------------------------------------------------------- /src/views/mail.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 99 | 100 |
12 | 13 | 14 | 96 | 97 |
15 |

There has been an exception thrown on {{ config('app.name') }}

16 | 17 | 18 | 21 | 24 | 25 | 26 | 29 | 32 | 33 | 34 | 37 | 40 | 41 | 42 | 45 | 48 | 49 | 50 | 53 | 56 | 57 | 58 | 61 | 64 | 65 |
19 | Environment: 20 | 22 | {{ config('app.env') }} 23 |
27 | Server: 28 | 30 | {{ \Illuminate\Support\Facades\Request::server('SERVER_NAME') }} 31 |
35 | Exception Url: 36 | 38 | {!! \Illuminate\Support\Facades\Request::fullUrl() !!} 39 |
43 | Exception Class: 44 | 46 | {{ get_class($exception) }} 47 |
51 | Exception Message: 52 | 54 | {!! htmlspecialchars($exception->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') !!} 55 |
59 | Exception Code: 60 | 62 | {{ $exception->getCode() }} 63 |
66 | 67 | 68 | 71 | 72 |
69 | In File {{ $exception->getFile() }} on line {{ $exception->getLine() }} 70 |
73 | 74 | 75 | 78 | 79 | 80 | 83 | 84 | 85 | 88 | 89 | 90 | 93 | 94 |
76 | Context: 77 |
81 |
$context = {{ var_export($context, true) }};
82 |
86 | Stack Trace: 87 |
91 |
{!! htmlspecialchars($exception->getTraceAsString(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') !!}
92 |
95 |
98 |
101 | 102 | -------------------------------------------------------------------------------- /tests/AlertDispatcherTest.php: -------------------------------------------------------------------------------- 1 | true, 17 | 'laravel_alert_notifications.throttle_duration_minutes' => 100, 18 | 'laravel_alert_notifications.mail.enabled' => true, 19 | 'laravel_alert_notifications.mail.toAddress' => 'test@test.com', 20 | 'laravel_alert_notifications.mail.fromAddress' => 'test@test.com', 21 | 'laravel_alert_notifications.mail.subject' => 'test', 22 | 'laravel_alert_notifications.microsoft_teams.enabled' => true, 23 | 'laravel_alert_notifications.microsoft_teams.webhook' => 'http://test', 24 | 'laravel_alert_notifications.slack.enabled' => true, 25 | 'laravel_alert_notifications.slack.webhook' => 'http://test', 26 | 'laravel_alert_notifications.cache_prefix' => 'laravel-alert-notifications-test-', 27 | ]; 28 | 29 | protected function setUp(): void 30 | { 31 | parent::setUp(); 32 | 33 | $this->alertHandlerMock = Mockery::mock( 34 | AlertDispatcher::class 35 | )->makePartial()->shouldAllowMockingProtectedMethods(); 36 | } 37 | 38 | public function testAlert() 39 | { 40 | $exception = new Exception('Test Exception'); 41 | $this->alertHandlerMock->exception = $exception; 42 | $this->alertHandlerMock->dontAlertExceptions = []; 43 | 44 | config($this->config); 45 | 46 | Mail::fake(); 47 | 48 | $actual = $this->alertHandlerMock->shouldMail(); 49 | $this->assertTrue($actual); 50 | 51 | $actual = $this->alertHandlerMock->shouldMicrosoftTeams(); 52 | $this->assertTrue($actual); 53 | 54 | $actual = $this->alertHandlerMock->shouldSlack(); 55 | $this->assertTrue($actual); 56 | 57 | $this->alertHandlerMock->dontAlertExceptions = [Exception::class]; 58 | $actual = $this->alertHandlerMock->isDonotAlertException(); 59 | $this->assertTrue($actual); 60 | } 61 | 62 | public function testShouldAlert() 63 | { 64 | $exception = new Exception('Test Exception'); 65 | $this->alertHandlerMock->exception = $exception; 66 | $this->alertHandlerMock->dontAlertExceptions = []; 67 | 68 | config($this->config); 69 | 70 | Mail::fake(); 71 | 72 | $actual = $this->alertHandlerMock->shouldAlert(); 73 | $this->assertTrue($actual); 74 | } 75 | 76 | public function testBasic() 77 | { 78 | $alert = new AlertDispatcher(new Exception(), []); 79 | $this->assertTrue(true); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/NotificationLevelTest.php: -------------------------------------------------------------------------------- 1 | true, 18 | 'laravel_alert_notifications.mail.toAddress' => 'test@test.com', 19 | 'laravel_alert_notifications.mail.fromAddress' => 'test@test.com', 20 | 'laravel_alert_notifications.mail.subject' => 'test', 21 | 'laravel_alert_notifications.mail.default_error_level' => 'error', 22 | 'laravel_alert_notifications.mail.warning.toAddress' => 'warning_test@test.com', 23 | 'laravel_alert_notifications.mail.error.toAddress' => 'error_test@test.com', 24 | 'laravel_alert_notifications.exclude_notification_levels' => ['debug'], 25 | ]; 26 | 27 | protected function setUp(): void 28 | { 29 | parent::setUp(); 30 | 31 | $this->exception = new Exception('Test Exception'); 32 | config($this->config); 33 | } 34 | 35 | public function testWarningLevelMail() 36 | { 37 | $notificationLevelsMapping = [ 38 | Exception::class => LogLevel::WARNING, 39 | ]; 40 | 41 | $alertHandler = new AlertDispatcher($this->exception, [], $notificationLevelsMapping); 42 | 43 | $mailable = new ExceptionOccurredMail($this->exception, $alertHandler->notificationLevel, []); 44 | $mailableInstance = $mailable->build(); 45 | 46 | $this->assertEquals( 47 | $mailableInstance->to[0]['address'], 48 | $this->config['laravel_alert_notifications.mail.warning.toAddress'] 49 | ); 50 | } 51 | 52 | public function testDefaultLevelMail() 53 | { 54 | $notificationLevelsMapping = [ 55 | Exception::class => LogLevel::EMERGENCY, 56 | ]; 57 | 58 | $alertHandler = new AlertDispatcher($this->exception, [], $notificationLevelsMapping); 59 | 60 | $mailable = new ExceptionOccurredMail($this->exception, $alertHandler->notificationLevel, []); 61 | $mailableInstance = $mailable->build(); 62 | 63 | $this->assertEquals( 64 | $mailableInstance->to[0]['address'], 65 | $this->config['laravel_alert_notifications.mail.toAddress'] 66 | ); 67 | } 68 | 69 | public function testDoNotSendMail() 70 | { 71 | $notificationLevelsMapping = [ 72 | Exception::class => LogLevel::DEBUG, 73 | ]; 74 | 75 | $alertHandler = new AlertDispatcher($this->exception, [], $notificationLevelsMapping); 76 | 77 | Mail::fake(); 78 | 79 | $alertHandler->notify(); 80 | 81 | Mail::assertNothingSent(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/ThrottleControlTest.php: -------------------------------------------------------------------------------- 1 | true, 16 | 'laravel_alert_notifications.throttle_duration_minutes' => 1, 17 | 'laravel_alert_notifications.cache_prefix' => 'laravel-alert-notifications-test-', 18 | ]; 19 | 20 | protected function setUp(): void 21 | { 22 | parent::setUp(); 23 | } 24 | 25 | public function testIsThrottled() 26 | { 27 | config($this->config); 28 | $exception = new Exception('message'); 29 | $actual = ThrottleControl::isThrottled($exception); 30 | $this->assertFalse($actual); 31 | 32 | $actual = ThrottleControl::isThrottled($exception); 33 | $this->assertTrue($actual); 34 | 35 | $differentException = new BadMethodCallException('message'); 36 | $actual = ThrottleControl::isThrottled($differentException); 37 | $this->assertFalse($actual); 38 | 39 | // crud: do it again 40 | $exception = new Exception('message'); 41 | $actual = ThrottleControl::isThrottled($exception); 42 | $this->assertTrue($actual); 43 | } 44 | 45 | public function testGetThrottleCacheKey() 46 | { 47 | config($this->config); 48 | $exception = new Exception('message', $code = 123); 49 | $actual = ThrottleControl::getThrottleCacheKey($exception); 50 | $this->assertSame($actual, 'laravel-alert-notifications-test-Exception-123'); 51 | } 52 | } 53 | --------------------------------------------------------------------------------