├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── log-envelope.php ├── envelope-email.png ├── envelope-slack.jpg ├── envelope-telegram.jpg ├── envelope.png ├── resources ├── migrations │ └── create_exceptions_table.php.stub └── views │ ├── main.blade.php │ └── storage.blade.php └── src ├── Drivers ├── AbstractDriver.php ├── Database.php ├── DriverFactory.php ├── Dummy.php ├── Mail.php ├── Slack.php └── Telegram.php ├── Facade.php ├── LogEnvelope.php ├── Mail └── LogEmail.php ├── Models └── ExceptionModel.php ├── ServiceProvider.php └── SyntaxHighlight.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor/ 3 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Yaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Log Envelope 2 | 3 | Laravel 5-9 package for logging errors to your e-mail(s), telegram, slack and database! 4 | 5 | [](https://scrutinizer-ci.com/g/Cherry-Pie/LogEnvelope/?branch=master) 6 | [](https://scrutinizer-ci.com/g/Cherry-Pie/LogEnvelope/build-status/master) 7 | [](https://packagist.org/packages/yaro/log-envelope) 8 | 9 | ## Installation 10 | 11 | You can install the package through Composer. 12 | ```bash 13 | composer require yaro/log-envelope 14 | ``` 15 | You must install this service provider. Make this the very first provider in list. 16 | ```php 17 | // config/app.php 18 | 'providers' => [ 19 | // make this very first provider 20 | // so fatal exceptions can be catchable by envelope 21 | Yaro\LogEnvelope\ServiceProvider::class, 22 | //... 23 | ]; 24 | ``` 25 | 26 | Then publish the config and migration file of the package using artisan. 27 | ```bash 28 | php artisan vendor:publish --provider="Yaro\LogEnvelope\ServiceProvider" 29 | ``` 30 | 31 | Change your Exception Handler's (```/app/Exceptions/Handler.php``` by default) ```report``` method like this: 32 | ```php 33 | //... 34 | public function report(Exception $e) 35 | { 36 | $res = parent::report($e); 37 | 38 | \LogEnvelope::send($e); 39 | //... 40 | 41 | return $res; 42 | } 43 | //... 44 | ``` 45 | 46 | Change config ```yaro.log-envelope.php``` for your needs. You can choose to log your errors to your database or send them to your email/telegram/slack. Emails are preferable, cuz they contains more debug information, such as traceback. 47 | 48 | There is a ```censored_fields``` option which will change any fields value to `*****` if it is named in this array. For example by default it will change values for fields called `password` to `*****`. 49 | 50 | Also there is ```force_config``` option, where you can define which configs to override for LogEnvelope execution. E.g., if you using some smtp for mail sending and queue it, you can change configs to send LogEnvelope emails immediately and not via smtp: 51 | ``` 52 | 'force_config' => [ 53 | 'mail.driver' => 'sendmail', 54 | 'queue.default' => 'sync', 55 | ], 56 | ``` 57 | 58 | 59 | ## TODO 60 | - highlight traceback in emails 61 | - page with logs from database 62 | 63 | ## Results 64 | Something like this with other info for debugging. 65 | - - - 66 | Email: 67 | 68 |  69 | - - - 70 | Slack: 71 | 72 |  73 | - - - 74 | Telegram: 75 | 76 |  77 | 78 | 79 | ## License 80 | The MIT License (MIT). Please see [LICENSE](https://github.com/Cherry-Pie/LogEnvelope/blob/master/LICENSE) for more information. 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yaro/log-envelope", 3 | "description": "Laravel 5 email on error", 4 | "keywords": ["laravel", "log", "error", "mail on error"], 5 | "require": { 6 | "php": ">=5.4.0", 7 | "illuminate/support": "5.*|^6.0|^7.0|^8.0|^9.0|^10.0" 8 | }, 9 | "autoload": { 10 | "psr-4": { 11 | "Yaro\\LogEnvelope\\": "src/" 12 | } 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Yaro", 18 | "email": "12fcv4@gmail.com", 19 | "homepage": "http://cherry-pie.co", 20 | "role": "Owner" 21 | } 22 | ], 23 | "minimum-stability": "stable" 24 | } 25 | -------------------------------------------------------------------------------- /config/log-envelope.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'mail' => [ 10 | 'enabled' => false, 11 | 'to' => [ 12 | // 'email@example.com', 13 | ], 14 | 'from_name' => 'Log Envelope', 15 | 'from_email' => '', 16 | ], 17 | 18 | 'telegram' => [ 19 | 'enabled' => false, 20 | 'token' => env('LOGENVELOPE_TELEGRAM_TOKEN'), 21 | 'chats' => [ 22 | // telegram user id 23 | ], 24 | ], 25 | 26 | 'slack' => [ 27 | 'enabled' => false, 28 | 'username' => 'Log Envelope', 29 | 'channel' => '#logenvelope', // create channel 30 | 'token' => env('LOGENVELOPE_SLACK_TOKEN'), 31 | ], 32 | 33 | 'database' => [ 34 | 'enabled' => false, 35 | 'model' => Yaro\LogEnvelope\Models\ExceptionModel::class, 36 | ], 37 | ], 38 | 39 | /* 40 | * Change config for LogEnvelope execution. 41 | */ 42 | 'force_config' => [ 43 | // 'mail.driver' => 'sendmail', 44 | // 'queue.default' => 'sync', 45 | ], 46 | 47 | /* 48 | * How many lines to show before exception line and after. 49 | */ 50 | 'lines_count' => 6, 51 | 52 | /* 53 | * List of exceptions to skip sending. 54 | */ 55 | 'except' => [ 56 | //'Exception', 57 | 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException', 58 | 'Symfony\Component\Process\Exception\ProcessTimedOutException', 59 | ], 60 | 61 | /* 62 | * List of fields to censor 63 | */ 64 | 'censored_fields' => [ 65 | 'password', 66 | ], 67 | 68 | ]; 69 | -------------------------------------------------------------------------------- /envelope-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cherry-Pie/LogEnvelope/b7d153196e8a64fee413c623b6503fb998c1f51c/envelope-email.png -------------------------------------------------------------------------------- /envelope-slack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cherry-Pie/LogEnvelope/b7d153196e8a64fee413c623b6503fb998c1f51c/envelope-slack.jpg -------------------------------------------------------------------------------- /envelope-telegram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cherry-Pie/LogEnvelope/b7d153196e8a64fee413c623b6503fb998c1f51c/envelope-telegram.jpg -------------------------------------------------------------------------------- /envelope.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cherry-Pie/LogEnvelope/b7d153196e8a64fee413c623b6503fb998c1f51c/envelope.png -------------------------------------------------------------------------------- /resources/migrations/create_exceptions_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | 20 | $table->string('host'); 21 | $table->string('method'); 22 | $table->string('fullUrl'); 23 | $table->text('exception'); 24 | $table->text('error'); 25 | $table->string('line'); 26 | $table->string('file'); 27 | $table->string('class'); 28 | $table->text('storage'); 29 | $table->text('exegutor'); 30 | $table->text('file_lines'); 31 | 32 | $table->timestamps(); 33 | }); 34 | } 35 | } 36 | 37 | /** 38 | * Reverse the migrations. 39 | */ 40 | public function down() 41 | { 42 | if (Schema::hasTable('exceptions')) { 43 | Schema::drop('exceptions'); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/main.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 |{!! $lineInfo['wrap_left'] !!}{!! $lineInfo['line'] !!}{!! $lineInfo['wrap_right'] !!}29 |
{{ $error }}33 |
12 | {{ $key }}13 | |
14 |
15 | =>16 | |
17 |
18 | {{ print_r($val, true) }}19 | |
20 |
%s
line %s%s',
24 | urlencode($data['method']),
25 | urlencode($data['fullUrl']),
26 | urlencode("\n \n"),
27 | urlencode($data['class']),
28 | urlencode($data['exception']),
29 | urlencode("\n"),
30 | urlencode($data['file']),
31 | $data['line'],
32 | urlencode("\n \n")
33 | );
34 |
35 | $biggestNumberLength = strlen(max(array_keys($data['file_lines'])));
36 | foreach ($data['file_lines'] as $num => $line) {
37 | $num = str_pad($num, $biggestNumberLength, ' ', STR_PAD_LEFT);
38 | $num = ''. $num .'
';
39 | $text .= urlencode($num .'|'. htmlentities($line) .'
');
40 | }
41 |
42 | foreach ($this->config['chats'] as $idUser) {
43 | $url = 'https://api.telegram.org/'. $this->config['token'] .'/sendMessage?disable_web_page_preview=true&chat_id='. $idUser
44 | . '&parse_mode=HTML&text=';
45 |
46 | @file_get_contents($url . $text);
47 | }
48 | } // end send
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/Facade.php:
--------------------------------------------------------------------------------
1 | config['censored_fields'] = config('yaro.log-envelope.censored_fields', ['password']);
20 | $this->config['except'] = config('yaro.log-envelope.except', []);
21 | $this->config['count'] = config('yaro.log-envelope.lines_count', 6);
22 | $this->config['drivers'] = config('yaro.log-envelope.drivers', []);
23 | } // end __construct
24 |
25 | public function send($exception)
26 | {
27 | $this->onBefore();
28 |
29 | try {
30 | $data = $this->getExceptionData($exception);
31 |
32 | if ($this->isSkipException($data['class'])) {
33 | return;
34 | }
35 |
36 | foreach ($this->config['drivers'] as $driver => $driverConfig) {
37 | DriverFactory::create($driver, $data)->setConfig($driverConfig)->send();
38 | }
39 | } catch (Exception $e) {
40 | Log::error($e);
41 | }
42 |
43 | $this->onAfter();
44 | } // end send
45 |
46 | private function onBefore()
47 | {
48 | $this->cachedConfig = [];
49 | $forcedConfig = config('yaro.log-envelope.force_config', []);
50 | foreach ($forcedConfig as $configKey => $configValue) {
51 | $this->cachedConfig[$configKey] = config($configKey);
52 | }
53 | if ($forcedConfig) {
54 | config($forcedConfig);
55 | }
56 | } // end onBefore
57 |
58 | private function onAfter()
59 | {
60 | if ($this->cachedConfig) {
61 | config($this->cachedConfig);
62 | }
63 | } // end onAfter
64 |
65 | public function isSkipException($exceptionClass)
66 | {
67 | return in_array($exceptionClass, $this->config['except']);
68 | } // end isSkipException
69 |
70 | private function getExceptionData($exception)
71 | {
72 | $data = [];
73 |
74 | $data['host'] = Request::server('HTTP_HOST');
75 | $data['method'] = Request::method();
76 | $data['fullUrl'] = Request::fullUrl();
77 | if (php_sapi_name() === 'cli') {
78 | $data['host'] = parse_url(config('app.url'), PHP_URL_HOST);
79 | $data['method'] = 'CLI';
80 | }
81 | $data['exception'] = $exception->getMessage();
82 | $data['error'] = $exception->getTraceAsString();
83 | $data['line'] = $exception->getLine();
84 | $data['file'] = $exception->getFile();
85 | $data['class'] = get_class($exception);
86 | $data['storage'] = array(
87 | 'SERVER' => Request::server(),
88 | 'GET' => Request::query(),
89 | 'POST' => $_POST,
90 | 'FILE' => Request::file(),
91 | 'OLD' => Request::hasSession() ? Request::old() : [],
92 | 'COOKIE' => Request::cookie(),
93 | 'SESSION' => Request::hasSession() ? Session::all() : [],
94 | 'HEADERS' => Request::header(),
95 | );
96 |
97 | // Remove empty, false and null values
98 | $data['storage'] = array_filter($data['storage']);
99 |
100 | // Censor sensitive field values
101 | array_walk_recursive($data['storage'], self::censorSensitiveFields(...));
102 |
103 | $count = $this->config['count'];
104 |
105 | $data['exegutor'] = [];
106 | $data['file_lines'] = [];
107 |
108 | $file = new SplFileObject($data['file']);
109 | for ($i = -1 * abs($count); $i <= abs($count); $i++) {
110 | list($line, $exegutorLine) = $this->getLineInfo($file, $data['line'], $i);
111 | if (!$line && !$exegutorLine) {
112 | continue;
113 | }
114 | $data['exegutor'][] = $exegutorLine;
115 | $data['file_lines'][$data['line'] + $i] = $line;
116 | }
117 |
118 | // to make Symfony exception more readable
119 | if ($data['class'] == 'Symfony\Component\Debug\Exception\FatalErrorException') {
120 | preg_match("~^(.+)' in ~", $data['exception'], $matches);
121 | if (isset($matches[1])) {
122 | $data['exception'] = $matches[1];
123 | }
124 | }
125 |
126 | return $data;
127 | } // end getExceptionData
128 |
129 | /**
130 | * Set the value of specified fields to *****
131 | *
132 | * @param string $value
133 | * @param string $key
134 | * @return void
135 | */
136 | public function censorSensitiveFields(&$value, $key)
137 | {
138 | if (in_array($key, $this->config['censored_fields'], true)) {
139 | $value = '*****';
140 | }
141 | }
142 |
143 | /**
144 | * @param SplFileObject $file
145 | */
146 | private function getLineInfo($file, $line, $i)
147 | {
148 | $currentLine = $line + $i;
149 | // cuz array starts with 0, when file lines start count from 1
150 | $index = $currentLine - 1;
151 | if ($index < 0) {
152 | return [false, false];
153 | }
154 | $file->seek($index);
155 |
156 | if ($file->eof()) {
157 | return [false, false];
158 | }
159 |
160 | return [
161 | $file->current(),
162 | [
163 | 'line' => '' . $currentLine . '. ' . SyntaxHighlight::process($file->current()),
164 | 'wrap_left' => $i ? '' : '',
165 | 'wrap_right' => $i ? '' : '',
166 | ]
167 | ];
168 | } // end getLineInfo
169 | }
170 |
--------------------------------------------------------------------------------
/src/Mail/LogEmail.php:
--------------------------------------------------------------------------------
1 | data = $data;
19 | $this->config = $config;
20 | }
21 |
22 | /**
23 | * Build the message.
24 | *
25 | * @return $this
26 | */
27 | public function build()
28 | {
29 | $subject = sprintf('[%s] @ %s: %s', $this->data['class'], $this->data['host'], $this->data['exception']);
30 |
31 | // to protect from gmail's anchors automatic generating
32 | $this->withSwiftMessage(function ($message) {
33 | $message->setBody(
34 | preg_replace(
35 | ['~\.~', '~http~'],
36 | ['.', 'http'],
37 | $message->getBody()
38 | )
39 | );
40 | });
41 |
42 | return $this->view('log-envelope::main')
43 | ->with($this->data)
44 | ->to($this->config['to'])
45 | ->from($this->config['from_email'], $this->config['from_name'])
46 | ->subject($subject);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Models/ExceptionModel.php:
--------------------------------------------------------------------------------
1 | publishes([
19 | __DIR__ . '/../config/log-envelope.php' => config_path('yaro.log-envelope.php'),
20 | ]);
21 |
22 | /*
23 | * Publish migration if not published yet
24 | */
25 | if (!$this->migrationHasAlreadyBeenPublished()) {
26 | $timestamp = date('Y_m_d_His', time());
27 | $this->publishes([
28 | __DIR__ . '/../resources/migrations/create_exceptions_table.php.stub' => database_path('migrations/' . $timestamp . '_create_exceptions_table.php'),
29 | ], 'migrations');
30 | }
31 |
32 | $this->app['view']->addNamespace('log-envelope', __DIR__ . '/../resources/views');
33 |
34 | $loader = \Illuminate\Foundation\AliasLoader::getInstance();
35 | $loader->alias('LogEnvelope', 'Yaro\LogEnvelope\Facade');
36 | }
37 |
38 | /**
39 | * Register the service provider.
40 | */
41 | public function register()
42 | {
43 | config([
44 | 'config/yaro.log-envelope.php',
45 | ]);
46 |
47 | $this->app->singleton('yaro.log-envelope', function($app) {
48 | return new LogEnvelope();
49 | });
50 | }
51 |
52 | /**
53 | * @return bool
54 | */
55 | protected function migrationHasAlreadyBeenPublished()
56 | {
57 | $files = glob(database_path('/migrations/*_create_exceptions_table.php'));
58 | return count($files) > 0;
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/src/SyntaxHighlight.php:
--------------------------------------------------------------------------------
1 | highlight($s);
16 | } // end process
17 |
18 | public function highlight($s)
19 | {
20 | $s = htmlspecialchars($s, ENT_COMPAT);
21 |
22 | // Workaround for escaped backslashes
23 | $s = str_replace('\\\\', '\\\\