小白學Laravel日誌

諾大的院子發表於2020-09-16

看完了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,那麼實際上建立這個日誌驅動的方法名稱是createStackDriverstack驅動是用來對多個日誌驅動進行包裝,方便我們同時使用的。為了簡單起見,我們先來看看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。構造方法的第二個引數是MonologHandlers

[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配置項,將日誌通道的例項值傳遞給我們自定義的類中,方便我們對日誌進行自定義操作,詳細見為通道自定義 Monologtap方法最終返回的是日誌通道的例項。

寫入日誌

日誌通道例項建立成功後,最後一步就是寫入日誌了。還是以Log::error(xxx)方法為例,我們透過LogManagerget方法獲取到了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方法,將方法名本身傳了進去,其它諸如debuginfowarn等方法也是一樣的道理,透過對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 協議》,轉載必須註明作者和本文連結

相關文章