小白学Laravel日志

看完了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 协议》,转载必须注明作者和本文链接
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 5

日志在哪里写入的呢? 没有详细说明

2年前 评论
andyzu 1年前
andyzu 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!