├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── lern.php ├── migrations ├── 2016_03_17_000000_create_lern_tables.php ├── 2016_03_27_000000_add_user_data_and_url_to_lern_tables.php └── 2017_09_23_000000_add_ip_to_lern_tables.php ├── src ├── Components │ ├── Component.php │ ├── Notifier.php │ └── Recorder.php ├── Exceptions │ ├── NotifierFailedException.php │ └── RecorderFailedException.php ├── Facades │ └── LERN.php ├── LERN.php ├── LERNServiceProvider.php └── Models │ └── ExceptionModel.php └── views └── exceptions └── default.blade.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `LERN` will be documented in this file. 4 | 5 | ### Unreleased 6 | - Updated for Laravel 7 and 8 7 | - Add support for PHP 7.3, 7.4, 8.0 8 | - Removed support for Laravel 5.5, 5.6, 5.7, 5.8 9 | - Removed support for PHP 7.0, 7.1 10 | - Removed deprecated RavenHandler handler, use sentry/sentry 3.x and their Sentry\Monolog\Handler instead 11 | - Removed deprecated HipChat handler, migrate to Slack and use SlackWebhookHandler or SlackHandler instead 12 | 13 | ### 5.0.0 14 | - Updated for Laravel 6 15 | 16 | ### 4.5.1 17 | - Fixed issues with `PDOException` 18 | 19 | ### 4.5.0 20 | - Added rate limiting of 1 second per Exception class for recording in the database 21 | - Added rate limiting of 1 second per Exception class for sending a notification 22 | - Made internal exception `Tylercd100\LERN\Exceptions\RecorderFailedException` notifiable. (previously it would be ignored and not sent on any notification channels) 23 | - Made internal exception `Tylercd100\LERN\Exceptions\NotifierFailedException` recordable. (previously it would be ignored and not be recorded in the database) 24 | 25 | ### 4.4.0 26 | - Added ability to use a custom Model 27 | - Added ability to change database connection for the default Model 28 | - Added `lern.recorder.model` and `lern.recorder.connection` to the config 29 | 30 | ### 4.3.0 31 | - Added setLogLevel and getLogLevel functions 32 | 33 | ### 4.2.2 34 | - Backwards compatibility fix for custom Recorder/Notifier classes 35 | 36 | ### 4.2.1 37 | - Fixed Auto Package Discovery 38 | 39 | ### 4.2.0 40 | - Added the ability to use Custom Recorder and Notifier classes 41 | - Added IP option to the config to collect IP addresses 42 | 43 | ### 4.1.1 44 | - Small typo 45 | 46 | ### 4.1.0 47 | - Auto Package Discovery 48 | 49 | ### 4.0.0 50 | - Added support for Laravel 5.5 51 | - Removed support for Laravel 5.4, 5.3, 5.2, 5.1 (Please use 3.x) 52 | 53 | ### 3.8.2 54 | - Lowered version requirement for php to 5.5.9 55 | 56 | ### 3.8.1 57 | - Lowered version requirement for orchestra/testbench which was preventing laravel 5.1 from installing 58 | 59 | ### 3.8.0 60 | - Fixes [#44](https://github.com/tylercd100/lern/issues/44) 61 | - Support for Laravel 5.1 62 | 63 | ### 3.7.5 64 | - Fixed [#42](https://github.com/tylercd100/lern/issues/42) 65 | 66 | ### 3.7.4 67 | - Updated to tylercd100/laravel-notify@^1.8.5 which sets the content type of SMTP emails to text/html instead of text/plain 68 | 69 | ### 3.7.3 70 | - Changed minimum phpunit version requirement 71 | 72 | ### 3.7.2 73 | - Fixed a typo in one of the unit tests 74 | 75 | ### 3.7.1 76 | - Quick fix! The system would seize with no exception 77 | 78 | ### 3.7.0 79 | - Added the ability to use a blade template 80 | - Blade templates are now the default way to style your exception notification 81 | 82 | ### 3.6.6 83 | - Updated to tylercd100/laravel-notify@^1.8.4 which allows newline characters in Fleephook, Hipchat, Pushover, Raven, and Slack. 84 | 85 | ### 3.6.5 86 | - Updated to tylercd100/laravel-notify@^1.8.2 which sets the content type of emails to text/html instead of text/plain 87 | 88 | ### 3.6.4 89 | - Updated to tylercd100/laravel-notify@^1.8.1 which allows newline characters in emails 90 | 91 | ### 3.6.3 92 | - Ignore columns that are null 93 | 94 | ### 3.6.2 95 | - Fixed Tests for Laravel 5.2 96 | 97 | ### 3.6.1 98 | - Fixed Tests for Laravel 5.3 99 | 100 | ### 3.6.0 101 | - Added a log_level option in the config to set the desired log level 102 | 103 | ### 3.5.0 104 | - Added Mailgun support 105 | 106 | ### 3.4.0 107 | - Set context using a callback/closure (Thanks to [@qodeboy](https://github.com/qodeboy) for suggestion) 108 | 109 | ### 3.3.1 110 | - Added default config values for Raven/Sentry 111 | 112 | ### 3.3.0 113 | - Extracted notification functions into its [own package](https://github.com/tylercd100/laravel-notify) 114 | 115 | ### 3.2.2 116 | - Accidently forgot to merge PR for 3.2.1, this release has the 3.2.1 fixes 117 | 118 | ### 3.2.1 119 | - Fixed issue when trying to store an exception code that is not an integer 120 | 121 | ### 3.2.0 122 | - Added option to remove certain keys from the input data. Please look at the excludeKeys options in the new [config file](https://github.com/tylercd100/lern/blob/3.2.0/config/lern.php) 123 | 124 | ### 3.1.4 125 | - Check if the exception code is an integer 126 | 127 | ### 3.1.3 128 | - Use Attribute Casting in Exception Model 129 | 130 | ### 3.1.2 131 | - Check to make sure 'smtp' config value is set before checking its value 132 | 133 | ### 3.1.1 134 | - Added Try/Catch statements to prevent infinite loops 135 | 136 | ### 3.1.0 137 | - Added SMTP support 138 | 139 | ### 3.0.2 140 | - Fixed issue with rolling back migration files 141 | 142 | ### 3.0.1 143 | - Fixed Pushover sounds 144 | 145 | ### 3.0.0 146 | - When enabled in the config file you can now collect: 147 | - user_id - The id of the currently logged in user. 148 | - method - Then method of the request: GET, POST, DELETE, PUT, etc... 149 | - url - The full URL of the request. 150 | - data - The input data of the request, if any. 151 | 152 | *__Reason for Major release: 3.0.0 introduces a new migration file and structure changes that could cause issues for 2.x users__* 153 | 154 | ### 2.3.0 155 | - Added support for [Twilio](https://www.twilio.com/) an SMS messaging service. 156 | 157 | ### 2.2.2 158 | - Fixed config for Plivo 159 | 160 | ### 2.2.1 161 | - Changed dependancy from `tylercd100/monolog-plivo` to `tylercd100/monolog-sms` which is the same package with a different name 162 | 163 | ### 2.2.0 164 | - Added support for [Plivo](https://www.plivo.com/) an SMS messaging service. 165 | 166 | ### 2.1.3 167 | - Scutinizer Code Coverage 168 | - Fixed Docblocks 169 | - Added more Unit Tests (60% -> 94% Coverage) 170 | - Changed dev dependencies 171 | 172 | ### 2.1.2 173 | - Improved config file with env functions and comments. 174 | - Fixed some handlers by increasing the log level from ERROR to CRITICAL 175 | 176 | ### 2.1.1 177 | - Fixed HipChat by making it use api v2 178 | 179 | ### 2.1.0 180 | - Added Hipchat, Flowdock and Fleephook support 181 | 182 | ### 2.0.0 183 | - Added a `LERN` facade 184 | - Monolog\Logger Support 185 | - With the ability to use custom Logger and Handler instances 186 | - Custom table name for migration (see the new config file) 187 | 188 | ### 1.1.0 189 | - Added Slack 190 | 191 | ### 1.0.0 192 | - Initial release and connected with packagist 193 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2016 tylercd100 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 | # LERN (Laravel Exception Recorder and Notifier) 2 | [![Latest Version](https://img.shields.io/github/release/tylercd100/lern.svg?style=flat-square)](https://github.com/tylercd100/lern/releases) 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 4 | [![Build Status](https://travis-ci.org/tylercd100/lern.svg?branch=master)](https://travis-ci.org/tylercd100/lern) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/tylercd100/lern/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/tylercd100/lern/?branch=master) 6 | [![Code Coverage](https://scrutinizer-ci.com/g/tylercd100/lern/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/tylercd100/lern/?branch=master) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/tylercd100/lern.svg?style=flat-square)](https://packagist.org/packages/tylercd100/lern) 8 | 9 | **_LERN from your mistakes_** 10 | 11 | LERN is a Laravel 5 package that will record exceptions into a database and will send you a notification. 12 | 13 | Currently supported notification channels via [Monolog](https://github.com/Seldaek/monolog) 14 | - Email 15 | - [Pushover](https://pushover.net/) 16 | - [Slack](https://slack.com/) 17 | - [Fleephook](https://fleep.io/) 18 | - [Flowdock](https://www.flowdock.com/) 19 | - [Plivo](https://www.plivo.com/) an SMS messaging service. 20 | - [Twilio](https://www.twilio.com/) an SMS messaging service. 21 | - [Sentry](https://getsentry.com) via [Sentry SDK for PHP](https://github.com/getsentry/sentry-php) 22 | - [Mailgun](https://mailgun.com) 23 | 24 | ## Version Compatibility 25 | 26 | Laravel | LERN 27 | :---------|:---------- 28 | 5.1.x | 3.x 29 | 5.2.x | 3.x 30 | 5.3.x | 3.x 31 | 5.4.x | 3.x 32 | 5.5.x | 4.x 33 | 5.6.x | 4.x 34 | 6.x | 5.x and 6.x 35 | 7.x | 6.x 36 | 8.x | 6.x 37 | 38 | ## Migrating from `3.x` to `4.x` 39 | Make sure that the config file now includes the new `lern.notify.class` and `lern.record.class` settings. Check the [config file](https://github.com/tylercd100/lern/blob/master/config/lern.php) to see how they are used. 40 | 41 | ## Migrating from `2.x` to `3.x` 42 | Version 3.x introduces the ability to collect more information from the error such as the user_id, url, method, and input data. In order to use 3.x you will need to copy over the new [config file](https://github.com/tylercd100/lern/blob/master/config/lern.php), the migration file and then migrate it. 43 | ```php 44 | # This will only copy over the migration file. For the config file you can either include the --force flag (Which will overwrite it) or copy it manually from github 45 | php artisan vendor:publish --provider="Tylercd100\LERN\LERNServiceProvider" 46 | php artisan migrate 47 | ``` 48 | 49 | ## Installation 50 | 51 | Version 4.x uses [Package Discovery](https://laravel.com/docs/5.5/packages#package-discovery). If you are using 3.x you will need to follow these [instructions.](https://github.com/tylercd100/lern/tree/3.8.2) 52 | 53 | Install via [composer](https://getcomposer.org/) - In the terminal: 54 | ```bash 55 | composer require tylercd100/lern 56 | ``` 57 | 58 | Then you will need to run these commands in the terminal in order to copy the config and migration files 59 | ```bash 60 | php artisan vendor:publish --provider="Tylercd100\LERN\LERNServiceProvider" 61 | ``` 62 | 63 | Before you run the migration you may want to take a look at `config/lern.php` and change the `table` property to a table name that you would like to use. After that run the migration 64 | ```bash 65 | php artisan migrate 66 | ``` 67 | 68 | ## Usage 69 | To use LERN modify the report method in the `app/Exceptions/Handler.php` file 70 | ```php 71 | public function report(Throwable $e) 72 | { 73 | if ($this->shouldReport($e)) { 74 | 75 | //Check to see if LERN is installed otherwise you will not get an exception. 76 | if (app()->bound("lern")) { 77 | app()->make("lern")->handle($e); //Record and Notify the Exception 78 | 79 | /* 80 | OR... 81 | app()->make("lern")->record($e); //Record the Exception to the database 82 | app()->make("lern")->notify($e); //Notify the Exception 83 | */ 84 | } 85 | } 86 | 87 | return parent::report($e); 88 | } 89 | ``` 90 | 91 | Dont forget to add this to the top of the file 92 | ```php 93 | //If you updated your aliases array in "config/app.php" 94 | use LERN; 95 | use Throwable; 96 | //or if you didnt... 97 | use Tylercd100\LERN\Facades\LERN; 98 | use Throwable; 99 | ``` 100 | 101 | ### Recording 102 | You can call `LERN::record($exception);` to record an Exception to the database. 103 | To query any Exception that has been recorded you can use `ExceptionModel` which is an Eloquent Model 104 | ```php 105 | use Tylercd100\LERN\Models\ExceptionModel; 106 | $mostRecentException = ExceptionModel::orderBy('created_at','DESC')->first(); 107 | ``` 108 | 109 | To change what is recorded in to the database take a look at `config/lern.php` 110 | ```php 111 | 'record'=>[ 112 | /** 113 | * The Model to use 114 | */ 115 | 'model' => \Tylercd100\LERN\Models\ExceptionModel::class, 116 | 117 | /** 118 | * Database connection to use. Null is the default connection. 119 | */ 120 | 'connection'=>null, 121 | 122 | /** 123 | * Database table to use 124 | */ 125 | 'table'=>'vendor_tylercd100_lern_exceptions', 126 | 127 | /** 128 | * Information to store 129 | */ 130 | 'collect'=>[ 131 | 'method'=>false, //When true it will collect GET, POST, DELETE, PUT, etc... 132 | 'data'=>false, //When true it will collect Input data 133 | 'status_code'=>true, 134 | 'user_id'=>false, 135 | 'url'=>false, 136 | 'ip'=>false, 137 | ], 138 | ], 139 | ``` 140 | Note: If you change `lern.recorder.model` then `lern.recorder.table` and `lern.recorder.connection` will be ignored unless you extend `\Tylercd100\LERN\Models\ExceptionModel::class` 141 | 142 | ### Notifications 143 | LERN uses the Monolog library to send notifications. If you need more than the supported notification channels, then you can add your own custom Monolog handlers. To start using any of the supported handlers just edit the provided config file `config/lern.php`. 144 | 145 | 146 | #### Changing the log level programmatically 147 | Some notification services support different log levels. If changing the config value `lern.notify.log_level` is not enough then try it this way: 148 | ```php 149 | // Change the log level. 150 | // Default is: critical 151 | // Options are: debug, info, notice, warning, error, critical, alert, emergency 152 | LERN::setLogLevel("emergency"); 153 | ``` 154 | 155 | #### Changing the subject line 156 | Some notification services support a subject line, this is how you change it. 157 | ```php 158 | //Change the subject 159 | LERN::setSubject("An Exception was thrown!"); 160 | ``` 161 | 162 | #### Changing the body of the notification 163 | LERN publishes a default blade template file that you can find at `resources/views/exceptions/default.blade.php`. 164 | The blade template file is compiled with these values: `$exception` `$url` `$method` `$data` `$user`. 165 | To specify a different blade template file, just edit the config file 166 | ```php 167 | 'notify'=>[ 168 | 'view'=>'exceptions.default', 169 | ], 170 | ``` 171 | ##### (deprecated) Using the `LERN::setMessage()` function 172 | Make sure that you set the view config value to null or the `LERN::setMessage()` will not work 173 | ```php 174 | 'notify'=>[ 175 | 'view'=>null, 176 | ], 177 | ``` 178 | 179 | #### Custom Monolog Handlers 180 | To use a custom Monolog Handler call the `pushHandler` method 181 | ```php 182 | use Monolog\Handler\SlackHandler; 183 | $handler = new SlackHandler($token, $channel); 184 | LERN::pushHandler($handler); 185 | LERN::notify($exception); 186 | ``` 187 | 188 | ## Further Reading and How-Tos 189 | - [Creating relationships between Exceptions and Users](https://github.com/tylercd100/lern/wiki/Creating-relationships-between-exceptions-and-users) 190 | 191 | ## Roadmap 192 | - Support more Monolog Handlers 193 | - Exception report page or command to easily identify your application's issues. 194 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tylercd100/lern", 3 | "description": "LERN (Laravel Exception Recorder and Notifier) is a Laravel 5 package that will record exceptions into a database and will notify you via Email, Pushover or Slack.", 4 | "keywords": [ 5 | "laravel", 6 | "exceptions", 7 | "monolog", 8 | "pushover", 9 | "slack", 10 | "LERN", 11 | "tylercd100" 12 | ], 13 | "type": "laravel-package", 14 | "homepage": "https://github.com/tylercd100/lern", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Tyler Arbon", 19 | "email": "tylercd100@gmail.com" 20 | } 21 | ], 22 | "autoload":{ 23 | "psr-4":{ 24 | "Tylercd100\\LERN\\":"src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "Tylercd100\\LERN\\Tests\\": "tests/" 30 | } 31 | }, 32 | "minimum-stability": "dev", 33 | "prefer-stable": true, 34 | "require": { 35 | "php": "^7.2|^8.0", 36 | "illuminate/support": "^6.0|^7.0|^8.0", 37 | "monolog/monolog": "^2.0", 38 | "tylercd100/laravel-notify": "^4.0" 39 | }, 40 | "require-dev": { 41 | "mockery/mockery": "~1.3.3|^1.4.2", 42 | "orchestra/testbench": "^4.0|^5.0|^6.0", 43 | "phpunit/phpunit": "^8.4|^9.3.3", 44 | "doctrine/dbal": "^2.6|^3.0" 45 | }, 46 | "suggest": { 47 | }, 48 | "extra": { 49 | "laravel": { 50 | "providers": [ 51 | "Tylercd100\\LERN\\LERNServiceProvider" 52 | ], 53 | "aliases": { 54 | "LERN": "Tylercd100\\LERN\\Facades\\LERN" 55 | } 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /config/lern.php: -------------------------------------------------------------------------------- 1 | 1, 12 | 13 | 'record'=>[ 14 | /** 15 | * The Recorder to use 16 | */ 17 | 'class' => \Tylercd100\LERN\Components\Recorder::class, 18 | 19 | /** 20 | * The Model to use 21 | */ 22 | 'model' => \Tylercd100\LERN\Models\ExceptionModel::class, 23 | 24 | /** 25 | * Database connection to use. Null is the default connection. 26 | */ 27 | 'connection'=>null, 28 | 29 | /** 30 | * Database table to use 31 | */ 32 | 'table'=>'vendor_tylercd100_lern_exceptions', 33 | 34 | /** 35 | * Information to store 36 | */ 37 | 'collect'=>[ 38 | 'method'=>false, //When true it will collect GET, POST, DELETE, PUT, etc... 39 | 'data'=>false, //When true it will collect Input data 40 | 'status_code'=>true, 41 | 'user_id'=>false, 42 | 'url'=>false, 43 | 'ip'=>false, 44 | ], 45 | 46 | /** 47 | * When record.collect.data is true, this will exclude certain data keys recursively 48 | */ 49 | 'excludeKeys' => [ 50 | 'password' 51 | ] 52 | ], 53 | 54 | 'notify'=>[ 55 | /** 56 | * The Notifier to use 57 | */ 58 | 'class' => \Tylercd100\LERN\Components\Notifier::class, 59 | 60 | /** 61 | * The view file to use 62 | */ 63 | 'view' => 'exceptions.default', 64 | 65 | /** 66 | * The default name of the monolog logger channel 67 | */ 68 | 'channel'=>'Tylercd100\LERN', 69 | 70 | /** 71 | * The log level to use when notifying 72 | */ 73 | 'log_level' => 'critical', //Options are: debug, info, notice, warning, error, critical, alert, emergency. 74 | 75 | /** 76 | * When using the default message body this will also include the stack trace 77 | */ 78 | 'includeExceptionStackTrace' => true, 79 | 80 | /** 81 | * mail, pushover, slack, etc... 82 | */ 83 | 'drivers'=>['mail'], 84 | 85 | /** 86 | * Mail settings 87 | */ 88 | 'mail'=>[ 89 | 'to' => 'to@address.com', 90 | 'from' => 'from@address.com', 91 | 'smtp' => true, 92 | ], 93 | 94 | /** 95 | * Mailgun settings 96 | */ 97 | 'mailgun'=>[ 98 | 'to' => env('MAILGUN_TO'), 99 | 'from' => env('MAILGUN_FROM'), 100 | 'token' => env('MAILGUN_APP_TOKEN'), 101 | 'domain'=> env('MAILGUN_DOMAIN'), 102 | ], 103 | 104 | /** 105 | * Pushover settings 106 | */ 107 | 'pushover'=>[ 108 | 'token' => env('PUSHOVER_APP_TOKEN'), 109 | 'users' => env('PUSHOVER_USER_KEY'), 110 | 'sound' => env('PUSHOVER_SOUND_ERROR', 'siren'), // https://pushover.net/api#sounds 111 | ], 112 | 113 | /** 114 | * Slack settings 115 | */ 116 | 'slack'=>[ 117 | 'token' => env('SLACK_APP_TOKEN'), //https://api.slack.com/web#auth 118 | 'channel' => env('SLACK_CHANNEL', '#exceptions'), //Dont forget the '#' 119 | 'username'=> env('SLACK_USERNAME', 'LERN'), //The 'from' name 120 | ], 121 | 122 | /** 123 | * Flowdock settings 124 | */ 125 | 'flowdock'=>[ 126 | 'token' => env('FLOWDOCK_APP_TOKEN'), 127 | ], 128 | 129 | /** 130 | * Sentry settings 131 | */ 132 | 'sentry'=>[ 133 | 'dsn' => env('SENTRY_DSN'), 134 | ], 135 | 136 | /** 137 | * Fleephook settings 138 | */ 139 | 'fleephook'=>[ 140 | 'token' => env('FLEEPHOOK_APP_TOKEN'), 141 | ], 142 | 143 | /** 144 | * Plivo settings 145 | */ 146 | 'plivo'=>[ 147 | 'auth_id' => env('PLIVO_AUTH_ID'), 148 | 'token' => env('PLIVO_AUTH_TOKEN'), 149 | 'to' => env('PLIVO_TO'), 150 | 'from' => env('PLIVO_FROM'), 151 | ], 152 | 153 | /** 154 | * Twilio settings 155 | */ 156 | 'twilio'=>[ 157 | 'sid' => env('TWILIO_AUTH_SID'), 158 | 'secret' => env('TWILIO_AUTH_SECRET'), 159 | 'to' => env('TWILIO_TO'), 160 | 'from' => env('TWILIO_FROM'), 161 | ] 162 | ], 163 | 164 | ]; 165 | -------------------------------------------------------------------------------- /migrations/2016_03_17_000000_create_lern_tables.php: -------------------------------------------------------------------------------- 1 | increments('id')->unsigned(); 17 | $table->string('class'); 18 | $table->string('file'); 19 | $table->integer('code'); 20 | $table->integer('status_code')->default(0); 21 | $table->integer('line'); 22 | $table->text('message'); 23 | $table->mediumText('trace'); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::drop(config('lern.record.table')); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /migrations/2016_03_27_000000_add_user_data_and_url_to_lern_tables.php: -------------------------------------------------------------------------------- 1 | integer('user_id')->nullable(); 17 | $table->text('data')->nullable(); 18 | $table->string('url')->nullable(); 19 | $table->string('method')->nullable(); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table(config('lern.record.table'), function(Blueprint $table) { 31 | $table->dropColumn('user_id'); 32 | $table->dropColumn('data'); 33 | $table->dropColumn('url'); 34 | $table->dropColumn('method'); 35 | }); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /migrations/2017_09_23_000000_add_ip_to_lern_tables.php: -------------------------------------------------------------------------------- 1 | string('ip')->nullable(); 17 | }); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | * 23 | * @return void 24 | */ 25 | public function down() 26 | { 27 | Schema::table(config('lern.record.table'), function(Blueprint $table) { 28 | $table->dropColumn('ip'); 29 | }); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/Components/Component.php: -------------------------------------------------------------------------------- 1 | dontHandle, $this->absolutelyDontHandle); 31 | 32 | foreach ($dontHandle as $type) { 33 | if ($e instanceof $type) { 34 | return true; 35 | } 36 | } 37 | 38 | $sent_at = Cache::get($this->getCacheKey($e)); 39 | if (empty($sent_at) || $sent_at->addSeconds(config('lern.ratelimit', 1))->lte(Carbon::now())) { 40 | return false; // The cache is empty or enough time has passed, so lets continue 41 | } else { 42 | return true; 43 | } 44 | } 45 | 46 | /** 47 | * Returns the cache key for the exception with the current component 48 | * 49 | * @param \Throwable $e 50 | * @return string 51 | */ 52 | protected function getCacheKey(Throwable $e) 53 | { 54 | return "LERN::".static::class."::".get_class($e); 55 | } 56 | } -------------------------------------------------------------------------------- /src/Components/Notifier.php: -------------------------------------------------------------------------------- 1 | config = config('lern.notify'); 42 | $this->log = $log; 43 | } 44 | 45 | /** 46 | * Transforms a value into a closure that returns itself when called 47 | * @param callable|string $cb The value that you want to wrap in a closure 48 | * @return callable 49 | */ 50 | private function wrapValueInClosure($cb) 51 | { 52 | if (is_callable($cb)) { 53 | return $cb; 54 | } else { 55 | return function () use ($cb) { return $cb; }; 56 | } 57 | } 58 | 59 | /** 60 | * Set a string or a closure to be called that will generate the message body for the notification 61 | * @param callable|string $cb A closure or string that will be set for the message 62 | * @return $this 63 | */ 64 | public function setMessage($cb) 65 | { 66 | $this->messageCb = $this->wrapValueInClosure($cb); 67 | return $this; 68 | } 69 | 70 | /** 71 | * Returns the result of the message closure 72 | * @param Throwable $e The Throwable instance that you want to build the message around 73 | * @return string The message string 74 | */ 75 | public function getMessage(Throwable $e) 76 | { 77 | $msg = $this->getMessageViaView($e); 78 | 79 | if ($msg === false) { 80 | $msg = $this->getMessageViaCallback($e); 81 | } 82 | 83 | if ($msg === false) { 84 | $msg = $this->getMessageViaDefault($e); 85 | } 86 | 87 | return $msg; 88 | } 89 | 90 | /** 91 | * Gets a basic Throwable message 92 | * @param Throwable $e The Throwable instance that you want to build the message around 93 | * @return String Returns the message string 94 | */ 95 | public function getMessageViaDefault(Throwable $e) 96 | { 97 | $msg = get_class($e)." was thrown! \n".$e->getMessage(); 98 | if ($this->config['includeExceptionStackTrace'] === true) { 99 | $msg .= "\n\n".$e->getTraceAsString(); 100 | } 101 | return $msg; 102 | } 103 | 104 | /** 105 | * Gets the Throwable message using a callback if it is set 106 | * @param Throwable $e The Throwable instance that you want to build the message around 107 | * @return String|false Returns the message string or false 108 | */ 109 | public function getMessageViaCallback(Throwable $e) 110 | { 111 | if (is_callable($this->messageCb)) { 112 | return $this->messageCb->__invoke($e); 113 | } 114 | return false; 115 | } 116 | 117 | /** 118 | * Gets the Throwable message using a Laravel view file 119 | * @param Throwable $e The Throwable instance that you want to build the message around 120 | * @return String|false Returns the message string or false 121 | */ 122 | public function getMessageViaView(Throwable $e) 123 | { 124 | $path = @$this->config["view"]; 125 | if (!empty($path) && View::exists($path)) { 126 | return View::make($path, [ 127 | "exception" => $e, 128 | "url" => Request::url(), 129 | "method" => Request::method(), 130 | "input" => Request::all(), 131 | "user" => Auth::user(), 132 | ])->render(); 133 | } 134 | return false; 135 | } 136 | 137 | /** 138 | * Set a string or a closure to be called that will generate the subject line for the notification 139 | * @param callable|string $cb A closure or string that will be set for the subject line 140 | * @return $this 141 | */ 142 | public function setSubject($cb) 143 | { 144 | $this->subjectCb = $this->wrapValueInClosure($cb); 145 | return $this; 146 | } 147 | 148 | /** 149 | * Returns the result of the subject closure 150 | * @param Throwable $e The Throwable instance that you want to build the subject around 151 | * @return string The subject string 152 | */ 153 | public function getSubject(Throwable $e) 154 | { 155 | if (is_callable($this->subjectCb)) { 156 | return $this->subjectCb->__invoke($e); 157 | } else { 158 | return get_class($e); 159 | } 160 | } 161 | 162 | /** 163 | * Set an array or a closure to be called that will generate the context array for the notification 164 | * @param callable|array $cb A closure or array that will be set for the context 165 | * @return $this 166 | */ 167 | public function setContext($cb) 168 | { 169 | $this->contextCb = $this->wrapValueInClosure($cb); 170 | return $this; 171 | } 172 | 173 | /** 174 | * Returns the result of the context closure 175 | * @param Throwable $e The Throwable instance that you want to build the context around 176 | * @return array The context array 177 | */ 178 | public function getContext(Throwable $e, $context = []) 179 | { 180 | //This needs a better solution. How do I set specific context needs for different drivers? 181 | if (in_array('pushover', $this->config['drivers'])) { 182 | $context['sound'] = $this->config['pushover']['sound']; 183 | } 184 | 185 | // Call the callback or return the default 186 | if (is_callable($this->contextCb)) { 187 | return $this->contextCb->__invoke($e, $context); 188 | } else { 189 | return $context; 190 | } 191 | } 192 | 193 | /** 194 | * Get the log level 195 | * @return string 196 | */ 197 | public function getLogLevel() 198 | { 199 | return $this->config['log_level']; 200 | } 201 | 202 | /** 203 | * Set the log level 204 | * @param string $level The log level 205 | * @return \Tylercd100\LERN\LERN 206 | */ 207 | public function setLogLevel($level) 208 | { 209 | $this->config['log_level'] = $level; 210 | return $this; 211 | } 212 | 213 | /** 214 | * Pushes on another Monolog Handler 215 | * @param HandlerInterface $handler The handler instance to add on 216 | * @return Notifier Returns this 217 | */ 218 | public function pushHandler(HandlerInterface $handler) 219 | { 220 | $this->log->pushHandler($handler); 221 | return $this; 222 | } 223 | 224 | /** 225 | * Triggers the Monolog Logger instance to log an error to all handlers 226 | * @param Throwable $e The exception to use 227 | * @param array $context Additional information that you would like to pass to Monolog 228 | * @return bool 229 | * @throws NotifierFailedException 230 | */ 231 | public function send(Throwable $e, array $context = []) 232 | { 233 | if ($this->shouldntHandle($e)) { 234 | return false; 235 | } 236 | 237 | $message = $this->getMessage($e); 238 | $subject = $this->getSubject($e); 239 | $context = $this->getContext($e, $context); 240 | 241 | try { 242 | $notify = new Notify($this->config, $this->log, $subject); 243 | 244 | $level = (array_key_exists('log_level', $this->config) && !empty($this->config['log_level'])) 245 | ? $this->config['log_level'] 246 | : 'critical'; 247 | 248 | $notify->{$level}($message, $context); 249 | 250 | Cache::forever($this->getCacheKey($e), Carbon::now()); 251 | 252 | return true; 253 | } catch (Throwable $e) { 254 | $code = (is_int($e->getCode()) ? $e->getCode() : 0); 255 | throw new NotifierFailedException($e->getMessage(), $code, $e); 256 | } 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /src/Components/Recorder.php: -------------------------------------------------------------------------------- 1 | config = config('lern.record'); 34 | } 35 | 36 | /** 37 | * Records an Exception to the database 38 | * @param Throwable $e The exception you want to record 39 | * @return false|ExceptionModel 40 | * @throws RecorderFailedException 41 | */ 42 | public function record(Throwable $e) 43 | { 44 | if ($this->shouldntHandle($e)) { 45 | return false; 46 | } 47 | 48 | $opts = [ 49 | 'class' => get_class($e), 50 | 'file' => $e->getFile(), 51 | 'line' => $e->getLine(), 52 | 'code' => (is_int($e->getCode()) ? $e->getCode() : 0), 53 | 'message' => $e->getMessage(), 54 | 'trace' => $e->getTraceAsString(), 55 | ]; 56 | 57 | $configDependant = array_keys($this->config['collect']); 58 | 59 | foreach ($configDependant as $key) { 60 | if ($this->canCollect($key)) { 61 | $value = $this->collect($key, $e); 62 | if ($value !== null) { 63 | $opts[$key] = $value; 64 | } 65 | } 66 | } 67 | 68 | $class = config('lern.record.model'); 69 | $class = !empty($class) ? $class : ExceptionModel::class; 70 | 71 | $model = new $class(); 72 | foreach($opts as $key => $value) { 73 | $model->{$key} = $value; 74 | } 75 | 76 | $model->save(); 77 | 78 | Cache::forever($this->getCacheKey($e), Carbon::now()); 79 | 80 | return $model; 81 | } 82 | 83 | /** 84 | * Checks the config to see if you can collect certain information 85 | * @param string $type the config value you want to check 86 | * @return boolean 87 | */ 88 | private function canCollect($type) { 89 | if (!empty($this->config) && !empty($this->config['collect']) && !empty($this->config['collect'][$type])) { 90 | return $this->config['collect'][$type] === true; 91 | } 92 | return false; 93 | } 94 | 95 | /** 96 | * @param string $key 97 | * @param Throwable $e 98 | * @return array|int|null|string 99 | * @throws Throwable 100 | */ 101 | protected function collect($key, Throwable $e = null) { 102 | switch ($key) { 103 | case 'user_id': 104 | return $this->getUserId(); 105 | case 'method': 106 | return $this->getMethod(); 107 | case 'url': 108 | return $this->getUrl(); 109 | case 'data': 110 | return $this->getData(); 111 | case 'ip': 112 | return $this->getIp(); 113 | case 'status_code': 114 | if ($e === null) { 115 | return 0; 116 | } 117 | return $this->getStatusCode($e); 118 | default: 119 | ddd();// what to do here ?????????? 120 | // throw new Exception("{$key} is not supported! Therefore it cannot be collected!"); 121 | } 122 | } 123 | 124 | /** 125 | * Gets the ID of the User that is logged in 126 | * @return integer|null The ID of the User or Null if not logged in 127 | */ 128 | protected function getUserId() { 129 | $user = Auth::user(); 130 | if (is_object($user) && !empty($user->id)) { 131 | return $user->id; 132 | } else { 133 | return null; 134 | } 135 | } 136 | 137 | /** 138 | * Gets the Method of the Request 139 | * @return string|null Possible values are null or GET, POST, DELETE, PUT, etc... 140 | */ 141 | protected function getMethod() { 142 | $method = Request::method(); 143 | if (!empty($method)) { 144 | return $method; 145 | } else { 146 | return null; 147 | } 148 | } 149 | 150 | /** 151 | * Gets the input data of the Request 152 | * @return array|null The Input data or null 153 | */ 154 | protected function getData() { 155 | $data = Request::all(); 156 | if (is_array($data)) { 157 | return $this->excludeKeys($data); 158 | } else { 159 | return null; 160 | } 161 | } 162 | 163 | /** 164 | * Gets the URL of the Request 165 | * @return string|null Returns a URL string or null 166 | */ 167 | protected function getUrl() { 168 | $url = Request::url(); 169 | if (is_string($url)) { 170 | return $url; 171 | } else { 172 | return null; 173 | } 174 | } 175 | 176 | /** 177 | * Returns the IP from the request 178 | * 179 | * @return string 180 | */ 181 | protected function getIp() { 182 | return Request::ip(); 183 | } 184 | 185 | /** 186 | * Gets the status code of the Throwable 187 | * @param Throwable $e The Throwable to check 188 | * @return string|integer The status code value 189 | */ 190 | protected function getStatusCode(Throwable $e) { 191 | if ($e instanceof HttpExceptionInterface) { 192 | return $e->getStatusCode(); 193 | } else { 194 | return 0; 195 | } 196 | } 197 | 198 | /** 199 | * This function will remove all keys from an array recursively as defined in the config file 200 | * @param array $data The array to remove keys from 201 | * @return array $data 202 | */ 203 | protected function excludeKeys(array $data) { 204 | $keys = isset($this->config['excludeKeys']) ? $this->config['excludeKeys'] : []; 205 | foreach ($data as $key => &$value) { 206 | if (in_array($key, $keys)) { 207 | unset($data[$key]); 208 | } else if (is_array($value)) { 209 | $value = $this->excludeKeys($value); 210 | } 211 | } 212 | 213 | return $data; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /src/Exceptions/NotifierFailedException.php: -------------------------------------------------------------------------------- 1 | notifier = $this->buildNotifier($notifier); 39 | $this->recorder = $this->buildRecorder($recorder); 40 | } 41 | 42 | /** 43 | * Will execute record and notify methods 44 | * @param Throwable $e The exception to use 45 | * @return ExceptionModel the recorded Eloquent Model 46 | */ 47 | public function handle(Throwable $e) 48 | { 49 | $this->exception = $e; 50 | $this->notify($e); 51 | return $this->record($e); 52 | } 53 | 54 | /** 55 | * Stores the exception in the database 56 | * @param Throwable $e The exception to use 57 | * @return \Tylercd100\LERN\Models\ExceptionModel|false The recorded Exception as an Eloquent Model 58 | */ 59 | public function record(Throwable $e) 60 | { 61 | $this->exception = $e; 62 | return $this->recorder->record($e); 63 | } 64 | 65 | /** 66 | * Will send the exception to all monolog handlers 67 | * @param Throwable $e The exception to use 68 | * @return void 69 | */ 70 | public function notify(Throwable $e) 71 | { 72 | $this->exception = $e; 73 | $this->notifier->send($e); 74 | } 75 | 76 | /** 77 | * Pushes on another Monolog Handler 78 | * @param HandlerInterface $handler The handler instance to add on 79 | * @return $this 80 | */ 81 | public function pushHandler(HandlerInterface $handler) { 82 | $this->notifier->pushHandler($handler); 83 | return $this; 84 | } 85 | 86 | /** 87 | * Get Notifier 88 | * @return \Tylercd100\LERN\Components\Notifier 89 | */ 90 | public function getNotifier() 91 | { 92 | return $this->notifier; 93 | } 94 | 95 | /** 96 | * Set Notifier 97 | * @param \Tylercd100\LERN\Components\Notifier $notifier A Notifier instance to use 98 | * @return \Tylercd100\LERN\LERN 99 | */ 100 | public function setNotifier(Notifier $notifier) 101 | { 102 | $this->notifier = $notifier; 103 | return $this; 104 | } 105 | 106 | /** 107 | * Get Recorder 108 | * @return \Tylercd100\LERN\Components\Recorder 109 | */ 110 | public function getRecorder() 111 | { 112 | return $this->recorder; 113 | } 114 | 115 | /** 116 | * Set Recorder 117 | * @param \Tylercd100\LERN\Components\Recorder $recorder A Recorder instance to use 118 | * @return \Tylercd100\LERN\LERN 119 | */ 120 | public function setRecorder(Recorder $recorder) 121 | { 122 | $this->recorder = $recorder; 123 | return $this; 124 | } 125 | 126 | /** 127 | * Get the log level 128 | * @return string 129 | */ 130 | public function getLogLevel() 131 | { 132 | return $this->notifier->getLogLevel(); 133 | } 134 | 135 | /** 136 | * Set the log level 137 | * @param string $level The log level 138 | * @return \Tylercd100\LERN\LERN 139 | */ 140 | public function setLogLevel($level) 141 | { 142 | $this->notifier->setLogLevel($level); 143 | return $this; 144 | } 145 | 146 | /** 147 | * Set a string or a closure to be called that will generate the message body for the notification 148 | * @param function|string $cb This closure function will be passed an Throwable and must return a string 149 | * @return $this 150 | */ 151 | public function setMessage($cb) 152 | { 153 | $this->notifier->setMessage($cb); 154 | return $this; 155 | } 156 | 157 | /** 158 | * Set a string or a closure to be called that will generate the subject line for the notification 159 | * @param function|string $cb This closure function will be passed an Throwable and must return a string 160 | * @return $this 161 | */ 162 | public function setSubject($cb) 163 | { 164 | $this->notifier->setSubject($cb); 165 | return $this; 166 | } 167 | 168 | /** 169 | * Constructs a Notifier 170 | * 171 | * @param Notifier $notifier 172 | * @return Notifier 173 | */ 174 | protected function buildNotifier(Notifier $notifier = null) 175 | { 176 | $class = config('lern.notify.class'); 177 | $class = !empty($class) ? $class : Notifier::class; 178 | if (empty($notifier)) { 179 | $notifier = new $class(); 180 | } 181 | if ($notifier instanceof Notifier) { 182 | return $notifier; 183 | } else { 184 | throw new NotifierFailedException("LERN was expecting an instance of ".Notifier::class); 185 | } 186 | } 187 | 188 | /** 189 | * Constructs a Recorder 190 | * 191 | * @param Recorder $recorder 192 | * @return Recorder 193 | */ 194 | protected function buildRecorder(Recorder $recorder = null) 195 | { 196 | $class = config('lern.record.class'); 197 | $class = !empty($class) ? $class : Recorder::class; 198 | if (empty($recorder)) { 199 | $recorder = new $class(); 200 | } 201 | if ($recorder instanceof Recorder) { 202 | return $recorder; 203 | } else { 204 | throw new RecorderFailedException("LERN was expecting an instance of ".Recorder::class); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/LERNServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(__DIR__.'/../config/lern.php', 'lern'); 12 | 13 | $this->handleDeprecatedConfigValues(); 14 | 15 | $this->app->singleton('lern', function () { 16 | return new LERN; 17 | }); 18 | } 19 | 20 | public function boot() 21 | { 22 | $this->publishes([ 23 | __DIR__.'/../views/exceptions/default.blade.php' => base_path('resources/views/exceptions/default.blade.php'), 24 | __DIR__.'/../migrations/2016_03_17_000000_create_lern_tables.php' => base_path('database/migrations/2016_03_17_000000_create_lern_tables.php'), 25 | __DIR__.'/../migrations/2016_03_27_000000_add_user_data_and_url_to_lern_tables.php' => base_path('database/migrations/2016_03_27_000000_add_user_data_and_url_to_lern_tables.php'), 26 | __DIR__.'/../migrations/2017_09_23_000000_add_ip_to_lern_tables.php' => base_path('database/migrations/2017_09_23_000000_add_ip_to_lern_tables.php'), 27 | __DIR__.'/../config/lern.php' => base_path('config/lern.php'), 28 | ]); 29 | } 30 | 31 | protected function handleDeprecatedConfigValues() 32 | { 33 | $renamedConfigValues = [ 34 | [ 35 | 'oldName' => 'lern.notify.pushover.user', 36 | 'newName' => 'lern.notify.pushover.users', 37 | ], 38 | ]; 39 | 40 | foreach ($renamedConfigValues as $renamedConfigValue) { 41 | if (config($renamedConfigValue['oldName'])) { 42 | config([$renamedConfigValue['newName'] => config($renamedConfigValue['oldName'])]); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Models/ExceptionModel.php: -------------------------------------------------------------------------------- 1 | 'array' 14 | ); 15 | 16 | public function __construct(array $attributes = []) 17 | { 18 | $this->connection = config('lern.record.connection'); 19 | $this->table = config('lern.record.table'); 20 | parent::__construct($attributes); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /views/exceptions/default.blade.php: -------------------------------------------------------------------------------- 1 | id}".PHP_EOL; 10 | } 11 | 12 | //Exception 13 | echo get_class($exception).":{$exception->getFile()}:{$exception->getLine()} {$exception->getMessage()}".PHP_EOL; 14 | 15 | //Input 16 | if (!empty($input)) { 17 | echo "Data: ".json_encode($input).PHP_EOL; 18 | } 19 | 20 | //Trace 21 | echo PHP_EOL."Trace: {$exception->getTraceAsString()}"; 22 | --------------------------------------------------------------------------------