看完了Laravel關於日誌部分的文件,小白感覺對Laravel日誌的使用方法還是比較模糊,主要是對日誌的配置及通道的使用還不清晰,所以決定透過學習原始碼的方式對Laravel的日誌系統一探究竟。
Laravel版本:v7.22.4
對原始碼分析沒興趣的小夥伴可以直接跳到最後的總結
部分,有關於日誌配置資訊的詳細介紹。
從Log
的方法開始
我們平時使用日誌的方式都是Log::error('xx')
,Log::deubg('xx')
等等,那就從這裡開始入手吧。
透過追蹤原始碼,我們發現Log
實際上是Laravel中的一個Facade
,關於Facade
的概念可以檢視文件。Facade
提供了對Laravel服務容器中物件的一種快捷訪問方式,透過查表我們知道,Log
這個Facade
對應的底層物件實際上是Illuminate\Log\LogManager
這個類的一個例項。我們呼叫Log
的靜態方法,Laravel會幫我們轉換成Illuminate\Log\LogManager
這個類物件的方法呼叫(至於是如何呼叫到的,這裡先不展開,展開又可以寫成另一篇文章了)。
獲取預設日誌通道名
以Log::error('xxx')
為例,我們依次追蹤到了Illuminate\Log\LogManager
類中三個方法的呼叫
error
方法呼叫
public function error($message, array $context = [])
{
$this->driver()->error($message, $context);
}
獲取日誌通道名稱
public function driver($driver = null)
{
return $this->get($driver ?? $this->getDefaultDriver());
}
由於上面的driver
引數為null
,所以要獲取預設通道
public function getDefaultDriver()
{
return $this->app['config']['logging.default'];
}
可以看到,這裡獲取的預設日誌通道名稱正是我們配置在config/logging.php
檔案中的default
配置項的值。
注意,不要被這裡的方法名稱裡面的driver
迷惑了,這裡獲取的實際上是日誌通道的名稱,也就是logging.php
配置檔案裡面channels
配置項的鍵名。而每個channel項裡面配置的driver
才是日誌驅動的名稱。在Laravel中,一個日誌channel
由一個日誌driver
和一個事件排程器組成。
獲取日誌通道例項
根據上面步驟2我們知道,獲取了日誌通道的名稱後,需要呼叫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,
]);
});
}
}
get
方法首先判斷channels
成員中是否有這個日誌通道的例項,有則直接返回,否則呼叫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.");
}
從resolve
方法我們知道,假設我們的日誌驅動名稱是stack
,那麼實際上建立這個日誌驅動的方法名稱是createStackDriver
。stack
驅動是用來對多個日誌驅動進行包裝,方便我們同時使用的。為了簡單起見,我們先來看看single
驅動。
日誌驅動例項的建立
以single
驅動為例,建立它的例項的方法名稱是createSingleDriver
,我們來看看這個方法:
protected function createSingleDriver(array $config)
{
return new Monolog($this->parseChannel($config), [
$this->prepareHandler(
new StreamHandler(
$config['path'], $this->level($config),
$config['bubble'] ?? true, $config['permission'] ?? null, $config['locking'] ?? false
), $config
),
]);
}
可以看到這裡返回的是類Monolog
的一個例項,而這個類實際上是Monolog\Logger
,一起來看看它的構造方法的簽名:
// 挖個坑,這裡的 handlers 是陣列
public function __construct(string $name, array $handlers = [], array $processors = [], ?DateTimeZone $timezone = null)
構造方法第一個引數是日誌通道的名稱(注意這裡指的是Monolog日誌的通道,不是Laravel日誌的通道),這個名稱將會出現在每條日誌記錄裡面,比如下面這條日誌中,production
就是Monolog的channel
。構造方法的第二個引數是Monolog
的Handlers。
[2020-09-11 13:36:16] production.ERROR: test
回到上面的createSingleDriver
方法,我們於是知道$this->parseChannel
獲取的是Monolog
日誌通道的名稱,這個名稱可以在logging.php
配置檔案中,透過給通道配置項加一個name
值進行修改。而$this->prepareHandler
方法的作用是準備好提供給Monolog
的日誌處理器(Handlers
),它主要是設定日誌的行格式。
在createSingleDriver
方法中,我們看到single
日誌驅動使用的是Monolog的StreamHandler
。Laravel中各驅動使用的MonologHandler
如下表所示:
Driver | Handler | Handler說明 |
---|---|---|
single | StreamHandler | 將日誌記錄到任意PHP流 |
daily | RotatingFileHandler | 將日誌記錄到一個檔案,並且每天產生一個新的日誌檔案 |
slack | SlackWebhookHandler | 使用Slack 網路鉤子記錄日誌到 Slack 賬戶中 |
monolog | – | 使用自定義的Monolog Handler |
syslog | SyslogHandler | 將日誌記錄到syslog 中 |
errorlog | ErrorLogHandler | 將日誌記錄到 php 的error_log 函式中 |
值得一提的是,logging.php
配置檔案中,還有一個沒有使用任何驅動的日誌通道emergency
,這個日誌通道類似於Laravel日誌系統的一個回退機制,當建立/獲取其它日誌通道失敗時,會退回到emergency
通道。emergency
通道使用Monolog的StreamHandler
,預設將日誌資訊記錄到storage/logs/laravel.log
檔案中。
日誌通道例項的建立
上面已經獲得了日誌驅動的例項,現在回到獲取日誌通道例項
小節裡的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,
]);
});
}
}
我們已經透過$this->resolve($name)
方法獲得了日誌驅動的例項,現在這個例項被傳遞給with
函式,這是一個助手函式
function with($value, callable $callback = null)
{
return is_null($callback) ? $value : $callback($value);
}
很明顯我們的驅動例項不是null
,於是它被傳遞到匿名函式中作為引數,並執行
return $this->channels[$name] = $this->tap($name, new Logger($logger, $this->app['events']));
這裡以日誌驅動例項和事件排程器為引數,建立了一個Illuminate\Log\Logger
的例項,也就是Laravel的日誌通道例項,這個例項由通道的名稱標記,最後被快取在channels
這個成員變數中。tap
方法主要是讀取日誌通道中的tap
配置項,將日誌通道的例項值傳遞給我們自定義的類中,方便我們對日誌進行自定義操作,詳細見為通道自定義 Monolog。tap
方法最終返回的是日誌通道的例項。
寫入日誌
日誌通道例項建立成功後,最後一步就是寫入日誌了。還是以Log::error(xxx)
方法為例,我們透過LogManager
的get
方法獲取到了Illuminate\Log\Logger
的一個例項,也就是Laravel的日誌通道例項,這個例項有兩個成員變數:$logger
,日誌驅動,這個變數一般是Monolog\Logger
的例項;$dispatcher
,Laravel自帶的事件排程器。所以我們可以簡單的認為Laravel的日誌通道是由Monolog的日誌物件+Laravel的事件排程器組成的。下面來看看這個通道的error
方法:
public function error($message, array $context = [])
{
$this->writeLog(__FUNCTION__, $message, $context);
}
error
方法呼叫了writeLog
方法,將方法名本身傳了進去,其它諸如debug
、info
、warn
等方法也是一樣的道理,透過對writeLog
方法的封裝增加呼叫的語義。
protected function writeLog($level, $message, $context)
{
$this->logger->{$level}($message = $this->formatMessage($message), $context);
$this->fireLogEvent($level, $message, $context);
}
writeLog
方法也很簡單,它直接呼叫了logger
成員變數相對應的方法,最後觸發一個日誌相關的事件。後面對日誌資訊的操作就交給Monolog處理了。
stack
日誌通道
剛才我們把stack
日誌通道放下了,先講了single
通道,現在讓我們回過頭來瞧瞧。stack
通道是Laravel預設的日誌通道,也就是我們剛把Laravel下載下來後,config/logging.php
配置檔案中default
項的配置值。我們直接來看它的原始碼:
protected function createStackDriver(array $config)
{
$handlers = collect($config['channels'])->flatMap(function ($channel) {
return $this->channel($channel)->getHandlers();
})->all();
if ($config['ignore_exceptions'] ?? false) {
$handlers = [new WhatFailureGroupHandler($handlers)];
}
return new Monolog($this->parseChannel($config), $handlers);
}
程式碼比較簡單,透過collect
函式將stack
通道配置中的channels
(通道名稱陣列)包裝成一個集合,然後依次呼叫$this->channel
方法,根據通道名稱獲取對應的日誌通道,將這些日誌通道使用的MonologHandler
取出來組成$handlers
陣列,最後與建立single
日誌通道時一樣,將通道名稱和Handlers
作為引數,建立出Monolog\Logger
的一個例項。$this->channel
方法實質上是我們剛才分析過的driver
方法,其它方法的作用也是顯而易見的,這裡就不再贅述了。
public function channel($channel = null)
{
return $this->driver($channel);
}
透過上面的分析,我們就明白了文件中stack 通道被用來將多個日誌通道聚合到一個單一的通道中
這句話的意思了,也就是我們可以做到一次呼叫,多方寫入
了。
總結
最後,讓我們拿Laravel的預設日誌配置檔案config/logging.php
來作個總結吧,詳細的說明直接放註釋裡面好了。
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that gets used when writing
| messages to the logs. The name specified in this option should match
| one of the channels defined in the "channels" configuration array.
|
*/
// 預設的日誌通道名稱,你可以在.env檔案中修改
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog",
| "custom", "stack"
|
*/
// 所有的日誌日誌通道配置,外層陣列的鍵名即為日誌通道的名稱
'channels' => [
// stack是預設使用的日誌通道
'stack' => [
// 這個驅動讓你可以將多個日誌通道組合成一個來使用
'driver' => 'stack',
// stack日誌通道是由這些日誌通道組合而成的
'channels' => ['single'],
// 設定為true後,如果某一通道記錄日誌出錯,日誌資訊仍然傳送到下一個日誌通道
'ignore_exceptions' => false,
],
'single' => [
// 這個驅動使用單個檔案記錄日誌
'driver' => 'single',
// 記錄日誌的檔案
'path' => storage_path('logs/laravel.log'),
// 日誌級別,所有可用的日誌級別:emergency、alert、 critical、 error、 warning、 notice、 info 和 debug。大於等於這個等級的日誌才會被記錄。
'level' => 'debug',
],
'daily' => [
// 這個驅動每天生成一個新的日誌檔案記錄日誌
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
// 保留多少天的日誌
'days' => 14,
],
'slack' => [
// 這個驅動將日誌檔案寫入你的slack賬號中
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => 'critical',
],
'papertrail' => [
// 這個驅動使用指定的Monolog Handler處理日誌
'driver' => 'monolog',
'level' => 'debug',
// 這個就是上面提到的指定的Monolog Handler
'handler' => SyslogUdpHandler::class,
// SyslogUdpHandler類構造方法的引數,這個Handler將日誌資訊記錄到遠端的syslogd伺服器
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
],
],
'stderr' => [
'driver' => 'monolog',
// 使用Monolog的StreamHandler處理日誌
'handler' => StreamHandler::class,
// 日誌格式
'formatter' => env('LOG_STDERR_FORMATTER'),
// StreamHandler構造方法的引數,指明輸出流
'with' => [
'stream' => 'php://stderr',
],
],
'syslog' => [
// 這個驅動使用php的syslog函式記錄日誌
'driver' => 'syslog',
'level' => 'debug',
],
'errorlog' => [
// 這個驅動使用php的error_log函式記錄日誌
'driver' => 'errorlog',
'level' => 'debug',
],
'null' => [
'driver' => 'monolog',
// 黑洞模式,相當於不記錄日誌
'handler' => NullHandler::class,
],
// 獲取或建立上面的日誌通道失敗時,回退使用的日誌通道,記錄失敗的原因及原來要記錄的日誌資訊,path指定日誌檔案路徑
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];
本作品採用《CC 協議》,轉載必須註明作者和本文連結