├── .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 | [](#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 |
166 |
167 | #### Teams
168 |
169 |
170 |
171 | #### Slack
172 |
173 |
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 |
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 |
12 |
13 |
14 |
15 | There has been an exception thrown on {{ config('app.name') }}
16 |
17 |
18 |
19 | Environment:
20 |
21 |
22 | {{ config('app.env') }}
23 |
24 |
25 |
26 |
27 | Server:
28 |
29 |
30 | {{ \Illuminate\Support\Facades\Request::server('SERVER_NAME') }}
31 |
32 |
33 |
34 |
35 | Exception Url:
36 |
37 |
38 | {!! \Illuminate\Support\Facades\Request::fullUrl() !!}
39 |
40 |
41 |
42 |
43 | Exception Class:
44 |
45 |
46 | {{ get_class($exception) }}
47 |
48 |
49 |
50 |
51 | Exception Message:
52 |
53 |
54 | {!! htmlspecialchars($exception->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') !!}
55 |
56 |
57 |
58 |
59 | Exception Code:
60 |
61 |
62 | {{ $exception->getCode() }}
63 |
64 |
65 |
66 |
67 |
68 |
69 | In File {{ $exception->getFile() }} on line {{ $exception->getLine() }}
70 |
71 |
72 |
73 |
74 |
75 |
76 | Context:
77 |
78 |
79 |
80 |
81 | $context = {{ var_export($context, true) }};
82 |
83 |
84 |
85 |
86 | Stack Trace:
87 |
88 |
89 |
90 |
91 | {!! htmlspecialchars($exception->getTraceAsString(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') !!}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
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 |
--------------------------------------------------------------------------------