iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 21
1
Software Development

Laravel 原始碼分析系列 第 21

分析 Log

Laravel 的 Log 套件在 5.5 版之前,是使用 Writer 包裝 Monolog,成為一個 proxy pattern,被代理的類別則是寫死 Monolog。在 5.6 版開始,設計改採用 Logger 包裝 PSR-3 的 LoggerInterface,一樣是 proxy pattern,但被代理的類別只要是符合 PSR-3 的介面,就能使用。

後面一樣只會討論 5.7.6 版的原始碼。

Service provider 定義非常簡單,就直接初始化 LogManager

$this->app->singleton('log', function () {
    return new LogManager($this->app);
});

LogManager 實作了 LoggerInterface。它跟之前提到的 SessionManager 有異區同工之妙,方法定義也非常接近,只差 LogManager 並沒有繼承 Manager 而已。

只要 logging.php 有設定好,基本上就可以直接 make 出來用:

app()->make('log')->info('something');

來看看 info() 裡面的實作:

public function info($message, array $context = [])
{
    return $this->driver()->info($message, $context);
}

非常簡單,取得 LoggerInterface driver 後,馬上再把 log 資訊轉交給 driver。這也是為何會說它是 proxy pattern 的主因--因為 LogManager 本身並沒有做跟 log 有關的事。

接著,看看 driver() 做了哪些事才能取得實例:

public function driver($driver = null)
{
    return $this->get($driver ?? $this->getDefaultDriver());
}

getDefaultDriver() 是從 logging.php 裡取得設定,檔案註解裡有提到有下列幾種 driver:

  • single
  • daily
  • slack
  • syslog
  • errorlog
  • monolog
  • custom
  • stack

我們接著來看 single 與 stack 兩種 driver 怎麼透過 get() 產生吧:

protected function get($name)
{
    try {
        // 當還沒有產 driver 實例的話,就解析同時設定 driver
        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) {
        // 當產 log 實例有錯的話,就改使用預設的 logger,層級會是最嚴重的 emergency
        return tap($this->createEmergencyLogger(), function ($logger) use ($e) {
            $logger->emergency('Unable to create configured logger. Using emergency logger.', [
                'exception' => $e,
            ]);
        });
    }
}

這裡的 channels 指的是屬於 LogManager 的 channel,與 Monolog 無關。

with() 函式的等價程式碼如下:

$callable = function ($logger) {
    // ...
}

return $callbale($this->resolve($name));

Laravel 會有這些簡單的函式,最主要的用途是為了 function chain。比方說 with()->blah(); 就有辦法實現。

不過事實上,PHP 7.0 開始也支援下面的寫法,也是可以參考的:

return (function($logger) {
    // ...
})($this->resolve($name));

產生實例後,會使用 tap() 方法(跟之前提到的 tap() 函式是不同的)設定實例。

protected function tap($name, Logger $logger)
{
    // 看看 driver 設定裡的 tap 有什麼
    foreach ($this->configurationFor($name)['tap'] ?? [] as $tap) {
        // 有的話就解析出來
        list($class, $arguments) = $this->parseTap($tap);

        // 接著會建 tap class 實例,並傳入 logger,並把參數用 , 拆分傳入 
        $this->app->make($class)->__invoke($logger, ...explode(',', $arguments));
    }

    return $logger;
}

protected function parseTap($tap)
{
    // 如果有 : 的話,就以它為分界拆成兩個字串
    return Str::contains($tap, ':') ? explode(':', $tap, 2) : [$tap, ''];
}

從以上原始碼可以得知,tap 設定與 middleware 設定有點雷同,如:

class SomeTap
{
    public function __invoke($logger, $param1, $param2)
    {
    }
}

// config

[
    'tap' => SomeTap::class . ':arg1,arg2',
];

簡單來說,這是一種用類別 __invoke() 來取代 tap() 功能的另解法。

tap() 裡面基本上是對 $logger 做一些初始化設定,比方說 Monolog 設定 processor 等。

再回頭過來看 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.");
}

從上面的程式碼來看,single 與 stack 會對應到的建置方法為:

  • createSingleDriver
  • createStackDriver
protected function createSingleDriver(array $config)
{
    // 從 $config 設定中,找出要設定給 monolog 的 channel 名稱
    return new Monolog($this->parseChannel($config), [
        // 建置 Handler
        $this->prepareHandler(
            new StreamHandler(
                $config['path'], $this->level($config),
                $config['bubble'] ?? true, $config['permission'] ?? null, $config['locking'] ?? false
            )
        ),
    ]);
}

protected function createStackDriver(array $config)
{
    // 取得所有 channel 的 handler 
    $handlers = collect($config['channels'])->flatMap(function ($channel) {
        return $this->channel($channel)->getHandlers();
    })->all();

    // 再重新建置一個包含所有 handler 的 Monolog 實例
    return new Monolog($this->parseChannel($config), $handlers);
}

Stack 本身代表的意義是把所有 Laravel Logger 所定義的 channels(也就是 driver)整合成一個懶人包,只要對 stack 推送 log ,所有在設定裡的 channel 都會跟著推送 log。

如果想針對單一 driver 推 log 也很簡單,使用 driver() 取得對應的 driver 即可。

雖然 Monolog 已經有內建 Registry,是類似概念的做法,但比起 Laravel 的 LogManager 還是缺了少了幾個元素,一個是設定轉成建實例的方法;另一個則是單元測試的易用性。

談到測試,Laravel 還是大勝許多套件,剩下沒幾天時間,希望有機會能談到測試。


上一篇
解析 Middleware 的實作細節
下一篇
分析 Facade
系列文
Laravel 原始碼分析46
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言