日志
對(duì)于一個(gè)框架系統(tǒng)來說,日志和異常處理可以說是非常重要的一個(gè)功能組件。我們的項(xiàng)目不管大還是小,對(duì)于錯(cuò)誤異常都應(yīng)該是零容忍的狀態(tài)。異常處理機(jī)制可以幫助我們及時(shí)發(fā)現(xiàn)問題,并且將問題記錄到日志中。而日志可以幫助我們掌握系統(tǒng)的運(yùn)行情況,查找問題原因??傊?,日志和異常處理是在系統(tǒng)的運(yùn)維狀態(tài)中非常重要的兩個(gè)功能。
日志記錄
Laravel 中的日志功能的使用非常簡(jiǎn)單,我們前面講過的門面就可以直接使用。它是基于 Monolog 來實(shí)現(xiàn)的,底層就是一套 Monolog ,如果有使用過這個(gè)日志框架的同學(xué)學(xué)起來會(huì)非常輕松。
\Illuminate\Support\Facades\Log::info("記錄一條日志");
一條簡(jiǎn)單地日志就這樣記錄下來了,如果沒有進(jìn)行別的配置,那么這條日志將記錄在 storage/logs/laravel.log 里面,輸出的是這樣子的:
[2021-09-14 00:52:47] local.INFO: 記錄一條日志
它還有第二個(gè)參數(shù),可以記錄一些上下文信息,我們也可以直接理解為記錄一些參數(shù)。
\Illuminate\Support\Facades\Log::info("記錄一條日志,加點(diǎn)參數(shù)", ['name'=>'ZyBlog']);
// [2021-09-14 00:52:47] local.INFO: 記錄一條日志,加點(diǎn)參數(shù) {"name":"ZyBlog"}
除了 info 之外,我們還可以定義 emergency、alert、critical、error、warning、notice、info 和 debug 等類型,這也是遵循 RFC 5424 specificationhttps://datatracker./doc/html/rfc5424 的日志標(biāo)準(zhǔn)格式。至于它們的使用的話,其實(shí)和 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: 記錄一條日志
日志通道
日志通道可以看成是日志的類型分類,比如說我們最常用的就是要將日志按天記錄,那么我們直接配置一個(gè) daily 就可以了,這樣所記錄的日志就不會(huì)全部記錄在一個(gè) laravel.log 文件中。首先,我們來看一下默認(rèn)情況下 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'),
],
],
];
從配置文件中,我們可以看出,默認(rèn)的日志通道是在 .env 文件中 LOG_CHANNEL 配置屬性定義的,下面提供了一些已經(jīng)配置好的默認(rèn)日志通道。默認(rèn)情況下,我們使用的是 stack 這個(gè)通道,其實(shí)它是一個(gè)堆棧的聚合通道,在它的配置里面還有一個(gè) channels 屬性,這個(gè)里面可以配置其它通道。這樣的話,我們就可以在這一個(gè)通道中讓它配置在 channels 中的所能通道都有機(jī)會(huì)處理日志信息。比如說我們這樣配置一下。
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'daily', 'errorlog'],
'ignore_exceptions' => false,
],
也就是給 channels 增加了一個(gè) daily 配置。然后運(yùn)行上面的日志路由,你會(huì)發(fā)現(xiàn) storage/logs/ 目錄下多了一個(gè) laravel-2021-09-17.log 文件,這個(gè)就是按天記錄的日志文件。然后在 laravel.log 和 laravel-2021-09-17.log 中都會(huì)記錄我們的日志信息。另外還有一個(gè) errorlog ,這個(gè)通道走的是 MonoLog 的 ErrorLogHandler ,也就是會(huì)把我們的錯(cuò)誤信息寫入到 PHP 的錯(cuò)誤日志文件中,它就是你在 php.ini 中配置的那個(gè)錯(cuò)誤日志路徑。大家可以自己嘗試一下,具體的 MonoLog 相關(guān)的知識(shí)不是我們今天學(xué)習(xí)的重點(diǎn),所以就需要大家自己去查閱相關(guān)的資料咯。
名稱 | 描述 |
---|
stack | 一個(gè)便于創(chuàng)建『多通道』通道的包裝器 |
single | 單個(gè)文件或者基于日志通道的路徑 (StreamHandler) |
daily | 一個(gè)每天輪換的基于 Monolog 驅(qū)動(dòng)的 RotatingFileHandler |
slack | 一個(gè)基于 Monolog 驅(qū)動(dòng)的 SlackWebhookHandler |
papertrail | 一個(gè)基于 Monolog 驅(qū)動(dòng)的 SyslogUdpHandler |
syslog | 一個(gè)基于 Monolog 驅(qū)動(dòng)的 SyslogHandler |
errorlog | 一個(gè)基于 Monolog 驅(qū)動(dòng)的 ErrorLogHandler |
monolog | 一個(gè)可以使用任何支持 Monolog 處理程序的 Monolog 工廠驅(qū)動(dòng)程序 |
custom | 一個(gè)調(diào)用指定工廠創(chuàng)建通道的驅(qū)動(dòng)程序 |
這個(gè)表格的內(nèi)容還是需要大家記住的。同時(shí),我們也可以直接修改 .env 中的 LOG_CHANNEL 來單獨(dú)指定某個(gè)日志通道,比如我們?cè)诰€上經(jīng)常就只會(huì)使用一個(gè) daily 來進(jìn)行日志記錄。同時(shí),我們也可以在記錄日志時(shí)直接指定使用哪個(gè)日志通道。
\Illuminate\Support\Facades\Log::channel('errorlog')->info($message);
另外你也可以手動(dòng)創(chuàng)建一個(gè)日志棧 stack 來進(jìn)行日志處理。
\Illuminate\Support\Facades\Log::stack(['daily', 'errorlog'])->info($message);
自定義日志處理
我們可以直接使用上面介紹的那些日志處理通道進(jìn)行組合搭配來實(shí)現(xiàn)自己的日志操作功能,同時(shí)也可以自己來定義一個(gè)自己的日志通道。
'custom'=>[
'driver'=>'custom',
'via'=>App\Logging\CreateCustomLogger::class,
'tap' => [App\Logging\CustomizeFormatter::class],
'path' => storage_path('logs/zyblog.log'),
]
指定 driver 類型為 custom ,就可以實(shí)現(xiàn)一個(gè)你自己完全控制和配置的 Monolog 日志操作通道,在這個(gè)配置中,必須要有的是一個(gè) via 屬性,它指向?qū)⒈徽{(diào)用以創(chuàng)建 Monolog 實(shí)例的工廠類。
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'])]);
}
}
在這個(gè) CreateCustomLogger 類中,我們只需要實(shí)現(xiàn) __invoke() 這個(gè)魔術(shù)方法,讓它返回一個(gè) Monolog 實(shí)例對(duì)象,就可以實(shí)現(xiàn)通道的指定類處理。在配置文件中的參數(shù)會(huì)通過 $config 變量注入進(jìn)來。比如在這段代碼中,我們就是簡(jiǎn)單地定義了一個(gè) Logger 對(duì)象,使用的處理器是 StreamHandler ,并且讓它的路徑指定為我們?cè)谂渲梦募信渲煤玫穆窂健?/p>
另外一個(gè) tap 屬性是干什么的呢?它是在通道創(chuàng)建完成之后,對(duì) Logger 對(duì)象進(jìn)行自定義處理的。因此,它的 __invoke() 方法注入進(jìn)來的就是一個(gè) Logger 對(duì)象。
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%'
));
}
}
}
我們可以為這個(gè) Logger 對(duì)象進(jìn)行其它的屬性設(shè)置。在這里本身我們就是使用的自定義的通道,所以效果可能不明顯,這個(gè) tap 屬性是可以放在其它系統(tǒng)默認(rèn)提供的通道中的,比如說 single 或者 daily 中的。在這里我們只是修改了記錄的格式,使用的依然還是 LineFormatter ,但格式中有一些簡(jiǎn)單的變化。
最后,我們看一下生成的日志,它被記錄在了 storage/logs/zyblog.log 文件中。
ZYBLOG [2021-09-23T00:58:17.965496+00:00] ZyBlog.INFO: 記錄一條日志custom [] []
日志記錄分析
日志功能其實(shí)也是走的門面模式,這個(gè)已經(jīng)不用多說了,大家可以一路找到門面最后實(shí)現(xiàn)的對(duì)象,也就是 vendor/laravel/framework/src/Illuminate/Log/LogManager.php 。我們以 daily 為例,看一下這個(gè)按天分割的日志處理器是怎么定義的。
在 LogManager 中,直接找到 info() 方法,也就是我們記錄的普通日志的方法,其它方法也是類似的,所以我們就從這個(gè)方法入手。
public function info($message, array $context = [])
{
$this->driver()->info($message, $context);
}
可以看到它調(diào)用了當(dāng)前類中的 driver() 方法的 info() 方法。
public function driver($driver = null)
{
return $this->get($driver ?? $this->getDefaultDriver());
}
driver() 從名字就能看出是驅(qū)動(dòng)的意思,接下來,它又調(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,
]);
});
}
}
看著很復(fù)雜呀,其實(shí)主要就是 try 里面的內(nèi)容,如果當(dāng)前類中的 channels 變量中已經(jīng)保存了當(dāng)前指定的通道的話,那么就使用這個(gè)通道,否則的話使用 resolve() 方法去創(chuàng)建通道,接下來我們就進(jìn)入到 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)用一個(gè)組合起來的方法名,也就是 createXXXDriver 這樣的方法。我們要看的是 daily 這個(gè)通道,所以組合起來的應(yīng)該是 createDailyDriver 這個(gè)方法,繼續(xù)在文件中查找,果然,這個(gè)方法是存在的。
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),
]);
}
重點(diǎn)需要關(guān)心的是通道的創(chuàng)建,在這里它使用的是 RotatingFileHandler() 這個(gè)通道,剩下的相信不用多說了吧,這是 Monolog 自帶的一個(gè)通道,每天創(chuàng)建一個(gè)文件,自動(dòng)刪除超時(shí)的文件。整體看下來,是不是和我們自定義日志通道配置的處理流程是一樣一樣的。
總結(jié)
通過今天的學(xué)習(xí),我們了解到了 Laravel 中日志相關(guān)處理的流程以及使用方式。這個(gè)東西吧,大家只要做了 Laravel 項(xiàng)目多少都會(huì)接觸到,只是平??赡芫褪呛?jiǎn)單地配置一下 .env 文件就完事了,并沒有深入的了解。Monolog 很強(qiáng)大,而且也很實(shí)用,但如果你想用別的日志工具,其實(shí)也可以通過之前的文章去配置 服務(wù)提供者 和 門面 來進(jìn)行方便地使用。
關(guān)于 Monolog 的內(nèi)容,將來我們?cè)賳为?dú)開小系列的文章進(jìn)行學(xué)習(xí),今天日志相關(guān)的內(nèi)容就簡(jiǎn)單地介紹到這里,下節(jié)課我們?cè)倭私庖幌?Laravel 的異常和錯(cuò)誤處理機(jī)制。
參考文檔:
https:///docs/laravel/8.x/logging/9376