日志
對于一個框架系統(tǒng)來說,日志和異常處理可以說是非常重要的一個功能組件。我們的項目不管大還是小,對于錯誤異常都應該是零容忍的狀態(tài)。異常處理機制可以幫助我們及時發(fā)現(xiàn)問題,并且將問題記錄到日志中。而日志可以幫助我們掌握系統(tǒng)的運行情況,查找問題原因??傊?,日志和異常處理是在系統(tǒng)的運維狀態(tài)中非常重要的兩個功能。
日志記錄
Laravel 中的日志功能的使用非常簡單,我們前面講過的門面就可以直接使用。它是基于 Monolog 來實現(xiàn)的,底層就是一套 Monolog ,如果有使用過這個日志框架的同學學起來會非常輕松。
\Illuminate\Support\Facades\Log::info("記錄一條日志");
一條簡單地日志就這樣記錄下來了,如果沒有進行別的配置,那么這條日志將記錄在 storage/logs/laravel.log 里面,輸出的是這樣子的:
[2021-09-14 00:52:47] local.INFO: 記錄一條日志
它還有第二個參數(shù),可以記錄一些上下文信息,我們也可以直接理解為記錄一些參數(shù)。
\Illuminate\Support\Facades\Log::info("記錄一條日志,加點參數(shù)", ['name'=>'ZyBlog']);
// [2021-09-14 00:52:47] local.INFO: 記錄一條日志,加點參數(shù) {"name":"ZyBlog"}
除了 info 之外,我們還可以定義 emergency、alert、critical、error、warning、notice、info 和 debug 等類型,這也是遵循 RFC 5424 specificationhttps://datatracker./doc/html/rfc5424 的日志標準格式。至于它們的使用的話,其實和 info() 方法都是一樣的,只是在日志中最后記錄的 local.INFO 這里的名稱不同。
$message = '記錄一條日志';
\Illuminate\Support\Facades\Log::emergency($message);
\Illuminate\Support\Facades\Log::alert($message);
\Illuminate\Support\Facades\Log::critical($message);
\Illuminate\Support\Facades\Log::error($message);
\Illuminate\Support\Facades\Log::warning($message);
\Illuminate\Support\Facades\Log::notice($message);
\Illuminate\Support\Facades\Log::debug($message);
// [2021-09-14 01:09:31] local.EMERGENCY: 記錄一條日志
// [2021-09-14 01:09:31] local.ALERT: 記錄一條日志
// [2021-09-14 01:09:31] local.CRITICAL: 記錄一條日志
// [2021-09-14 01:09:31] local.ERROR: 記錄一條日志
// [2021-09-14 01:09:31] local.WARNING: 記錄一條日志
// [2021-09-14 01:09:31] local.NOTICE: 記錄一條日志
// [2021-09-14 01:09:31] local.DEBUG: 記錄一條日志
日志通道
日志通道可以看成是日志的類型分類,比如說我們最常用的就是要將日志按天記錄,那么我們直接配置一個 daily 就可以了,這樣所記錄的日志就不會全部記錄在一個 laravel.log 文件中。首先,我們來看一下默認情況下 Laravel 的日志配置有哪些。
return [
'default' => env('LOG_CHANNEL', 'stack'),
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => env('LOG_LEVEL', 'critical'),
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => SyslogUdpHandler::class,
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
從配置文件中,我們可以看出,默認的日志通道是在 .env 文件中 LOG_CHANNEL 配置屬性定義的,下面提供了一些已經(jīng)配置好的默認日志通道。默認情況下,我們使用的是 stack 這個通道,其實它是一個堆棧的聚合通道,在它的配置里面還有一個 channels 屬性,這個里面可以配置其它通道。這樣的話,我們就可以在這一個通道中讓它配置在 channels 中的所能通道都有機會處理日志信息。比如說我們這樣配置一下。
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'daily', 'errorlog'],
'ignore_exceptions' => false,
],
也就是給 channels 增加了一個 daily 配置。然后運行上面的日志路由,你會發(fā)現(xiàn) storage/logs/ 目錄下多了一個 laravel-2021-09-17.log 文件,這個就是按天記錄的日志文件。然后在 laravel.log 和 laravel-2021-09-17.log 中都會記錄我們的日志信息。另外還有一個 errorlog ,這個通道走的是 MonoLog 的 ErrorLogHandler ,也就是會把我們的錯誤信息寫入到 PHP 的錯誤日志文件中,它就是你在 php.ini 中配置的那個錯誤日志路徑。大家可以自己嘗試一下,具體的 MonoLog 相關的知識不是我們今天學習的重點,所以就需要大家自己去查閱相關的資料咯。
| 名稱 | 描述 |
|---|
| stack | 一個便于創(chuàng)建『多通道』通道的包裝器 |
| single | 單個文件或者基于日志通道的路徑 (StreamHandler) |
| daily | 一個每天輪換的基于 Monolog 驅動的 RotatingFileHandler |
| slack | 一個基于 Monolog 驅動的 SlackWebhookHandler |
| papertrail | 一個基于 Monolog 驅動的 SyslogUdpHandler |
| syslog | 一個基于 Monolog 驅動的 SyslogHandler |
| errorlog | 一個基于 Monolog 驅動的 ErrorLogHandler |
| monolog | 一個可以使用任何支持 Monolog 處理程序的 Monolog 工廠驅動程序 |
| custom | 一個調(diào)用指定工廠創(chuàng)建通道的驅動程序 |
這個表格的內(nèi)容還是需要大家記住的。同時,我們也可以直接修改 .env 中的 LOG_CHANNEL 來單獨指定某個日志通道,比如我們在線上經(jīng)常就只會使用一個 daily 來進行日志記錄。同時,我們也可以在記錄日志時直接指定使用哪個日志通道。
\Illuminate\Support\Facades\Log::channel('errorlog')->info($message);
另外你也可以手動創(chuàng)建一個日志棧 stack 來進行日志處理。
\Illuminate\Support\Facades\Log::stack(['daily', 'errorlog'])->info($message);
自定義日志處理
我們可以直接使用上面介紹的那些日志處理通道進行組合搭配來實現(xiàn)自己的日志操作功能,同時也可以自己來定義一個自己的日志通道。
'custom'=>[
'driver'=>'custom',
'via'=>App\Logging\CreateCustomLogger::class,
'tap' => [App\Logging\CustomizeFormatter::class],
'path' => storage_path('logs/zyblog.log'),
]
指定 driver 類型為 custom ,就可以實現(xiàn)一個你自己完全控制和配置的 Monolog 日志操作通道,在這個配置中,必須要有的是一個 via 屬性,它指向將被調(diào)用以創(chuàng)建 Monolog 實例的工廠類。
namespace App\Logging;
use Monolog\Handler\StreamHandler;
use Monolog\Logger;
class CreateCustomLogger
{
public function __invoke(array $config)
{
return new Logger('ZyBlog', [new StreamHandler($config['path'])]);
}
}
在這個 CreateCustomLogger 類中,我們只需要實現(xiàn) __invoke() 這個魔術方法,讓它返回一個 Monolog 實例對象,就可以實現(xiàn)通道的指定類處理。在配置文件中的參數(shù)會通過 $config 變量注入進來。比如在這段代碼中,我們就是簡單地定義了一個 Logger 對象,使用的處理器是 StreamHandler ,并且讓它的路徑指定為我們在配置文件中配置好的路徑。
另外一個 tap 屬性是干什么的呢?它是在通道創(chuàng)建完成之后,對 Logger 對象進行自定義處理的。因此,它的 __invoke() 方法注入進來的就是一個 Logger 對象。
namespace App\Logging;
use Monolog\Formatter\LineFormatter;
class CustomizeFormatter
{
public function __invoke($logger)
{
foreach ($logger->getHandlers() as $handler) {
$handler->setFormatter(new LineFormatter(
'ZYBLOG [%datetime%] %channel%.%level_name%: %message% %context% %extra%'
));
}
}
}
我們可以為這個 Logger 對象進行其它的屬性設置。在這里本身我們就是使用的自定義的通道,所以效果可能不明顯,這個 tap 屬性是可以放在其它系統(tǒng)默認提供的通道中的,比如說 single 或者 daily 中的。在這里我們只是修改了記錄的格式,使用的依然還是 LineFormatter ,但格式中有一些簡單的變化。
最后,我們看一下生成的日志,它被記錄在了 storage/logs/zyblog.log 文件中。
ZYBLOG [2021-09-23T00:58:17.965496+00:00] ZyBlog.INFO: 記錄一條日志custom [] []
日志記錄分析
日志功能其實也是走的門面模式,這個已經(jīng)不用多說了,大家可以一路找到門面最后實現(xiàn)的對象,也就是 vendor/laravel/framework/src/Illuminate/Log/LogManager.php 。我們以 daily 為例,看一下這個按天分割的日志處理器是怎么定義的。
在 LogManager 中,直接找到 info() 方法,也就是我們記錄的普通日志的方法,其它方法也是類似的,所以我們就從這個方法入手。
public function info($message, array $context = [])
{
$this->driver()->info($message, $context);
}
可以看到它調(diào)用了當前類中的 driver() 方法的 info() 方法。
public function driver($driver = null)
{
return $this->get($driver ?? $this->getDefaultDriver());
}
driver() 從名字就能看出是驅動的意思,接下來,它又調(diào)用了 get() 方法。
protected function get($name)
{
try {
return $this->channels[$name] ?? with($this->resolve($name), function ($logger) use ($name) {
return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events']));
});
} catch (Throwable $e) {
return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
$logger->emergency('Unable to create configured logger. Using emergency logger.', [
'exception' => $e,
]);
});
}
}
看著很復雜呀,其實主要就是 try 里面的內(nèi)容,如果當前類中的 channels 變量中已經(jīng)保存了當前指定的通道的話,那么就使用這個通道,否則的話使用 resolve() 方法去創(chuàng)建通道,接下來我們就進入到 resolve() 方法中。
protected function resolve($name)
{
$config = $this->configurationFor($name);
if (is_null($config)) {
throw new InvalidArgumentException("Log [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($config);
}
throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
}
protected function configurationFor($name)
{
return $this->app['config']["logging.channels.{$name}"];
}
configurationFor() 方法用于從配置文件中獲取指定的通道配置信息。接下來判斷是否是自定義的通道,如果不是的話,調(diào)用一個組合起來的方法名,也就是 createXXXDriver 這樣的方法。我們要看的是 daily 這個通道,所以組合起來的應該是 createDailyDriver 這個方法,繼續(xù)在文件中查找,果然,這個方法是存在的。
protected function createDailyDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(new RotatingFileHandler(
$config['path'], $config['days'] ?? 7, $this->level($config),
$config['bubble'] ?? true, $config['permission'] ?? null, $config['locking'] ?? false
), $config),
]);
}
重點需要關心的是通道的創(chuàng)建,在這里它使用的是 RotatingFileHandler() 這個通道,剩下的相信不用多說了吧,這是 Monolog 自帶的一個通道,每天創(chuàng)建一個文件,自動刪除超時的文件。整體看下來,是不是和我們自定義日志通道配置的處理流程是一樣一樣的。
總結
通過今天的學習,我們了解到了 Laravel 中日志相關處理的流程以及使用方式。這個東西吧,大家只要做了 Laravel 項目多少都會接觸到,只是平??赡芫褪呛唵蔚嘏渲靡幌?.env 文件就完事了,并沒有深入的了解。Monolog 很強大,而且也很實用,但如果你想用別的日志工具,其實也可以通過之前的文章去配置 服務提供者 和 門面 來進行方便地使用。
關于 Monolog 的內(nèi)容,將來我們再單獨開小系列的文章進行學習,今天日志相關的內(nèi)容就簡單地介紹到這里,下節(jié)課我們再了解一下 Laravel 的異常和錯誤處理機制。
參考文檔:
https:///docs/laravel/8.x/logging/9376