Laravel Exception結合自定義Log服務的使用
第一部分:laravel關於錯誤和異常的部分原始碼
第二部分:自定義異常的使用(結合serviceprovider monolog elasticsearch)
過程中涉及到的重要函式請自行檢視手冊
error_reporting set_error_handler set_exception_handler register_shutdown_function error_get_last
laravel v6.18.40
原始碼部分
我們來到http kernel檔案,處理請求部分
可以看到執行我們業務邏輯的sendRequestThroughRouter方法被try_catch包裹的嚴嚴實實
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Exception $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
} catch (Throwable $e) {
$this->reportException($e = new FatalThrowableError($e));
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
捕獲到異常後 框架做了哪些工作呢? reportException 記錄了異常 renderException 響應了異常
/**
* Report the exception to the exception handler.
*
* @param \Exception $e
* @return void
*/
protected function reportException(Exception $e)
{
# 從容器中解析ExceptionHandler, 繫結位於bootstrap/app.php中
# 執行的是App\Exceptions\Handler的report方法
$this->app[ExceptionHandler::class]->report($e);
}
# 跳轉到App\Exceptions\Handler的report方法
public function report(Exception $exception)
{
# 繼續跳轉到父類的report方法
parent::report($exception);
}
# 只看核心程式碼
Illuminate\Foundation\Exceptions\Handler::report()
/**
* Report or log an exception.
*
* @param \Exception $e
* @return void
*
* @throws \Exception
*/
public function report(Exception $e)
{
if ($this->shouldntReport($e)) {
return;
}
# 判斷傳遞進來的異常是否存在report方法,有就執行
if (is_callable($reportCallable = [$e, 'report'])) {
// 值得注意的是,此步驟通過容器呼叫
// 這意味著我們可以在自定義的異常類中肆無忌憚的向report方法中諸如依賴了!!!
// 後面自定義異常類的使用會提及
return $this->container->call($reportCallable);
}
# 從容器中解析log服務
# Illuminate\Log\LogManager例項
try {
$logger = $this->container->make(LoggerInterface::class);
} catch (Exception $ex) {
throw $e;
}
# 記錄日誌 基於monolog 後面自定義日誌服務會講解monolog的使用
$logger->error(
$e->getMessage(),
array_merge(
$this->exceptionContext($e),
$this->context(),
['exception' => $e]
)
);
}
# renderException方法請自行檢視
通過上面的程式碼,我們自然而然的認為這樣就可以捕獲應用中產生的所有異常了,其實不然
下面我們來看框架引導階段為處理錯誤和異常做的工作
邏輯同樣位於框架的boot階段
下面給出簡要介紹
Illuminate\Foundation\Bootstrap\HandleExceptions::bootstrap()
public function bootstrap(Application $app)
{
self::$reservedMemory = str_repeat('x', 10240);
$this->app = $app;
// 儘可能顯示所有錯誤, 甚至包括將來 PHP 可能加入的新的錯誤級別和常量
// -1 和 E_ALL | E_STRICT 的區別為是否向後相容
error_reporting(-1);
// 設定執行邏輯部分出現錯誤的回撥
// 預設錯誤級別為 E_ALL | E_STRICT
// 網上說此函式只有warning和notice級別的錯誤能夠觸發的說法不夠準確
// 個人拙見:可以觸發回撥的錯誤級別為執行時產生的錯誤
// 直接中斷指令碼執行的錯誤不能觸發此回撥 因為回撥還未註冊
// 為了更大範圍的抓取錯誤,需要配合register_shutdown_function 和 error_get_last 處理
set_error_handler([$this, 'handleError']);
// 設定捕獲執行邏輯部分未捕獲的異常回撥
set_exception_handler([$this, 'handleException']);
// 設定php指令碼結束前最後執行的回撥
register_shutdown_function([$this, 'handleShutdown']);
if (! $app->environment('testing')) {
ini_set('display_errors', 'Off');
}
}
# 將php錯誤轉化為異常丟擲
/**
* Convert PHP errors to ErrorException instances.
* @throws \ErrorException
*/
public function handleError($level, $message, $file = '', $line = 0, $context = [])
{
if (error_reporting() & $level) {
throw new ErrorException($message, 0, $level, $file, $line);
}
}
# 註釋已經非常清晰 致命錯誤異常不能按照普通異常處理 在此處直接記錄和返回響應
/**
* Handle an uncaught exception from the application.
*
* Note: Most exceptions can be handled via the try / catch block in
* the HTTP and Console kernels. But, fatal error exceptions must
* be handled differently since they are not normal exceptions.
*
* @param \Throwable $e
* @return void
*/
public function handleException($e)
{
if (! $e instanceof Exception) {
$e = new FatalThrowableError($e);
}
try {
self::$reservedMemory = null;
$this->getExceptionHandler()->report($e);
} catch (Exception $e) {
//
}
if ($this->app->runningInConsole()) {
$this->renderForConsole($e);
} else {
$this->renderHttpResponse($e);
}
}
/**
* Handle the PHP shutdown event.
*
* @return void
*/
public function handleShutdown()
{
// 生成的異常類是symfony封裝的異常類
// 例:可以在任意路由中來上一句不加分號的程式碼 看看測試效果
if (! is_null($error = error_get_last()) && $this->isFatal($error['type'])) {
$this->handleException($this->fatalExceptionFromError($error, 0));
}
}
使用部分
自定義異常的使用
方式一:直接丟擲一個異常 在相應方法中判斷 進行自定義的處理
1 建立一個自定義異常類
php artisan make:exception CustomException
2 在業務邏輯中丟擲異常
Route::get('ex', function () {
throw new CustomException('thrown for caught');
});
3 擴充套件App\Exceptions\Handler類
public function report(Exception $exception)
{
if ($exception instanceof CustomException) {
Log::channel('daily')->error($exception->getMessage(), ['type' => 'myEx']);
}
parent::report($exception);
}
# 當然你也可以擴充套件render方法
4 訪問路由
# 檢視logs下的檔案
以上方法顯然不夠優雅,當異常變多的時候,需要配合大量的instanceof判斷,並且可能會記錄兩份相同內容的日誌
所以還可以使用第二種方式進行自定義異常的使用,利用框架自動呼叫report和render方法的特性,實現記錄和渲染異常響應
方式二:自定義一個exception 然後讓框架自動呼叫report方法 不進行render
1 建立自定義並編輯自定義異常
php artisan make:exception MyException
class MyException extends Exception
{
public function report()
{
Log::channel('daily')->error($this->getMessage(), array_merge($this->exceptionContext(), $this->context()));
}
// 其實是不必要的 這兩個方法可以在Handler中進行重寫,請自行檢視Handler的父類,根據需要進行擴充套件重寫
public function exceptionContext()
{
// return [$this->getTraceAsString()];
return [];
}
// 其實是不必要的 這兩個方法可以在Handler中進行重寫
public function context()
{
// return ['type' => 'myEx'];
return ['exception' => $this];
}
public function render()
{
return 'whatever you like';
}
}
2 丟擲異常
Route::get('myex', function () {
throw new MyException('thrown for caught');
});
3 執行並檢視日誌檔案 是不是發現和laravel原生的異常記錄長得很像呢
方式三:使用自定義的日誌服務記錄異常
上面提到異常例項的report是通過容器呼叫的,這意味著我們可以注入我們自定義的日誌服務
這裡使用神器monolog,因為laravel的日誌系統基於monolog,框架已經包含了此庫。如果使用其他框架請先確保安裝monolog
這裡使用elasticsearch作為日誌handler之一,所以請確保安裝了composer require elasticsearch/elasticsearch
使用monolog作為自定義日誌服務實現的原因是因為monolog本身具有替換性和通用性,其他框架稍加改動也可以使用
1 建立契約
<?php
namespace App\Contracts;
interface ExceptionLog
{
// 記錄異常
public function recordEx(\Exception $e);
}
2 建立日誌服務 並簡單介紹monolog使用
# 關於monolog的更多使用方法請檢視官方文件 https://github.com/Seldaek/monolog
<?php
namespace App\Services\Logs;
use App\Contracts\ExceptionLog;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\RotatingFileHandler;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder;
class MonoException implements ExceptionLog
{
protected $logger;
public function __construct()
{
// 建立本地檔案儲存handlers
$streamHandler = new StreamHandler(storage_path('logs/exception.log'), Logger::DEBUG);
// 建立本地日期切分儲存handler
$rotateHandler = new RotatingFileHandler(storage_path('logs/exception.log'), Logger::DEBUG);
// 建立es 客戶端 為了減小難度 就不在這裡注入elasticsearch客戶端了 其實是我懶 開心擼碼最重要
$esClient = ClientBuilder::create()->setHosts(config('es.hosts'))->build();
// es配置
$options = [
'index' => 'my_exception_index',
'type' => 'test_exception',
];
// 建立遠端elasticsearch日誌儲存
$esHandler = new ElasticsearchHandler($esClient, $options);
// 這裡沒有阻止handlers的堆疊冒泡,一條日誌會逐個經過es、rotate、stream日誌處理器
// 更多的日誌儲存handler請檢視文件(為了效能考量,monolog甚至為你提供了非同步方式記錄日誌)
// 建立logger 雖然叫logger但是他並沒有記錄日誌的能力
// 真正提供記錄日誌能力的是提前建立好的handlers
// monolog提供非常多開箱即用的handler 請檢視文件
// 並沒有設定processor等等 更多api請檢視官方文件
$logger = new Logger('exception', compact('streamHandler', 'rotateHandler', 'esHandler'));
$this->logger = $logger;
}
public function recordEx(\Exception $e)
{
$this->logger->error($e->getMessage());
}
}
3 建立服務提供者 因為我們建立的服務在異常中呼叫 所以使用單例和延遲繫結更加合適
php artisan make:provider LogServiceProvider
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\ExceptionLog;
use App\Services\Logs\MonoException;
use Illuminate\Contracts\Support\DeferrableProvider;
class LogServiceProvider extends ServiceProvider implements DeferrableProvider
{
/**
* Register services.
*
* @return void
*/
public function register()
{
$this->app->singleton(ExceptionLog::class, function () {
return new MonoException();
});
}
/**
* Bootstrap services.
*
* @return void
*/
public function boot()
{
//
}
public function provides()
{
return [ExceptionLog::class];
}
}
4 註冊服務 config/app.php
...
App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
// 註冊自定義日誌服務
App\Providers\LogServiceProvider::class,
4 建立自定義異常 並在其中使用自定義的日誌服務
<?php
namespace App\Exceptions;
use Exception;
use App\Contracts\ExceptionLog;
class CustomException extends Exception
{
// 注入我們的日誌服務
public function report(ExceptionLog $logger)
{
$logger->recordEx($this);
}
public function render()
{
return 'whatever you like';
}
}
5 測試異常的使用
Route::get('cex', function () {
throw new CustomException('thrown for caught!!!');
});
6 檢查es和storage/logs下是否存在我們的日誌吧
# 我們實現的日誌服務可以在應用的任何地方使用,這裡只是使用在了異常記錄的地方,希望大家能夠理解
方式四:有的道友可能吐槽,laravel好好的日誌服務不香嗎?折騰啥啊?
好的,那就通過laravel自帶的日誌服務實現和上面同樣的功能
config/logging.php
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Handler\ElasticsearchHandler;
use Elasticsearch\ClientBuilder;
use Monolog\Formatter\ElasticsearchFormatter;
...
'stack' => [
'driver' => 'stack',
// 'channels' => ['single'],
'channels' => ['daily', 'es'],
'ignore_exceptions' => false,
],
// 自定義es日誌處理器
// 一定要設定es的formatter!!!
'es' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => ElasticsearchHandler::class,
'formatter' => ElasticsearchFormatter::class,
'formatter_with' => [
'index' => 'lara_exception',
'type' => 'lara_exception'
],
'handler_with' => [
'client' => ClientBuilder::create()->setHosts(config('es.hosts'))->build()
]
],
# 這樣就新增了es作為日誌記錄的驅動了
# 測試一下吧 比如訪問這個路由 檢視你的本地檔案和es吧
Route::get('cex', function () {
// laravel會將其轉換成一個symfony的致命異常
aaa
});
其他方式:想要維持一個高效能的、功能強大的日誌服務的話,可以考慮新增一個非同步的日誌handler,其實monolog也已經提供了開箱即用的handler
更多用法請檢視laravel元件的日誌部分文件,感興趣的道友可以自行檢視laravel log和monolog的原始碼
今天沒有下集預告,發現錯誤歡迎指正,感謝!!!