【Laravel系列7.1】日志

燕永昌
2023-12-01

日志

对于一个框架系统来说,日志和异常处理可以说是非常重要的一个功能组件。我们的项目不管大还是小,对于错误异常都应该是零容忍的状态。异常处理机制可以帮助我们及时发现问题,并且将问题记录到日志中。而日志可以帮助我们掌握系统的运行情况,查找问题原因。总之,日志和异常处理是在系统的运维状态中非常重要的两个功能。

日志记录

Laravel 中的日志功能的使用非常简单,我们前面讲过的门面就可以直接使用。它是基于 Monolog 来实现的,底层就是一套 Monolog ,如果有使用过这个日志框架的同学学起来会非常轻松。

\Illuminate\Support\Facades\Log::info("记录一条日志");

一条简单地日志就这样记录下来了,如果没有进行别的配置,那么这条日志将记录在 storage/logs/laravel.log 里面,输出的是这样子的:

[2021-09-14 00:52:47] local.INFO: 记录一条日志

它还有第二个参数,可以记录一些上下文信息,我们也可以直接理解为记录一些参数。

\Illuminate\Support\Facades\Log::info("记录一条日志,加点参数", ['name'=>'ZyBlog']);
// [2021-09-14 00:52:47] local.INFO: 记录一条日志,加点参数 {"name":"ZyBlog"}

除了 info 之外,我们还可以定义 emergency、alert、critical、error、warning、notice、info 和 debug 等类型,这也是遵循 RFC 5424 specificationhttps://datatracker.ietf.org/doc/html/rfc5424 的日志标准格式。至于它们的使用的话,其实和 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: 记录一条日志

日志通道

日志通道可以看成是日志的类型分类,比如说我们最常用的就是要将日志按天记录,那么我们直接配置一个 daily 就可以了,这样所记录的日志就不会全部记录在一个 laravel.log 文件中。首先,我们来看一下默认情况下 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'),
        ],
    ],

];

从配置文件中,我们可以看出,默认的日志通道是在 .env 文件中 LOG_CHANNEL 配置属性定义的,下面提供了一些已经配置好的默认日志通道。默认情况下,我们使用的是 stack 这个通道,其实它是一个堆栈的聚合通道,在它的配置里面还有一个 channels 属性,这个里面可以配置其它通道。这样的话,我们就可以在这一个通道中让它配置在 channels 中的所能通道都有机会处理日志信息。比如说我们这样配置一下。

'stack' => [
    'driver' => 'stack',
    'channels' => ['single', 'daily', 'errorlog'],
    'ignore_exceptions' => false,
],

也就是给 channels 增加了一个 daily 配置。然后运行上面的日志路由,你会发现 storage/logs/ 目录下多了一个 laravel-2021-09-17.log 文件,这个就是按天记录的日志文件。然后在 laravel.log 和 laravel-2021-09-17.log 中都会记录我们的日志信息。另外还有一个 errorlog ,这个通道走的是 MonoLog 的 ErrorLogHandler ,也就是会把我们的错误信息写入到 PHP 的错误日志文件中,它就是你在 php.ini 中配置的那个错误日志路径。大家可以自己尝试一下,具体的 MonoLog 相关的知识不是我们今天学习的重点,所以就需要大家自己去查阅相关的资料咯。

名称描述
stack一个便于创建『多通道』通道的包装器
single单个文件或者基于日志通道的路径 (StreamHandler)
daily一个每天轮换的基于 Monolog 驱动的 RotatingFileHandler
slack一个基于 Monolog 驱动的 SlackWebhookHandler
papertrail一个基于 Monolog 驱动的 SyslogUdpHandler
syslog一个基于 Monolog 驱动的 SyslogHandler
errorlog一个基于 Monolog 驱动的 ErrorLogHandler
monolog一个可以使用任何支持 Monolog 处理程序的 Monolog 工厂驱动程序
custom一个调用指定工厂创建通道的驱动程序

这个表格的内容还是需要大家记住的。同时,我们也可以直接修改 .env 中的 LOG_CHANNEL 来单独指定某个日志通道,比如我们在线上经常就只会使用一个 daily 来进行日志记录。同时,我们也可以在记录日志时直接指定使用哪个日志通道。

\Illuminate\Support\Facades\Log::channel('errorlog')->info($message);

另外你也可以手动创建一个日志栈 stack 来进行日志处理。

\Illuminate\Support\Facades\Log::stack(['daily', 'errorlog'])->info($message);

自定义日志处理

我们可以直接使用上面介绍的那些日志处理通道进行组合搭配来实现自己的日志操作功能,同时也可以自己来定义一个自己的日志通道。

'custom'=>[
    'driver'=>'custom',
    'via'=>App\Logging\CreateCustomLogger::class,
    'tap' => [App\Logging\CustomizeFormatter::class],
    'path' => storage_path('logs/zyblog.log'),
]

指定 driver 类型为 custom ,就可以实现一个你自己完全控制和配置的 Monolog 日志操作通道,在这个配置中,必须要有的是一个 via 属性,它指向将被调用以创建 Monolog 实例的工厂类。

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'])]);
    }
}

在这个 CreateCustomLogger 类中,我们只需要实现 __invoke() 这个魔术方法,让它返回一个 Monolog 实例对象,就可以实现通道的指定类处理。在配置文件中的参数会通过 $config 变量注入进来。比如在这段代码中,我们就是简单地定义了一个 Logger 对象,使用的处理器是 StreamHandler ,并且让它的路径指定为我们在配置文件中配置好的路径。

另外一个 tap 属性是干什么的呢?它是在通道创建完成之后,对 Logger 对象进行自定义处理的。因此,它的 __invoke() 方法注入进来的就是一个 Logger 对象。

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%'
            ));
        }

    }
}

我们可以为这个 Logger 对象进行其它的属性设置。在这里本身我们就是使用的自定义的通道,所以效果可能不明显,这个 tap 属性是可以放在其它系统默认提供的通道中的,比如说 single 或者 daily 中的。在这里我们只是修改了记录的格式,使用的依然还是 LineFormatter ,但格式中有一些简单的变化。

最后,我们看一下生成的日志,它被记录在了 storage/logs/zyblog.log 文件中。

ZYBLOG [2021-09-23T00:58:17.965496+00:00] ZyBlog.INFO: 记录一条日志custom [] []

日志记录分析

日志功能其实也是走的门面模式,这个已经不用多说了,大家可以一路找到门面最后实现的对象,也就是 vendor/laravel/framework/src/Illuminate/Log/LogManager.php 。我们以 daily 为例,看一下这个按天分割的日志处理器是怎么定义的。

在 LogManager 中,直接找到 info() 方法,也就是我们记录的普通日志的方法,其它方法也是类似的,所以我们就从这个方法入手。

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

可以看到它调用了当前类中的 driver() 方法的 info() 方法。

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

driver() 从名字就能看出是驱动的意思,接下来,它又调用了 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,
            ]);
        });
    }
}

看着很复杂呀,其实主要就是 try 里面的内容,如果当前类中的 channels 变量中已经保存了当前指定的通道的话,那么就使用这个通道,否则的话使用 resolve() 方法去创建通道,接下来我们就进入到 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() 方法用于从配置文件中获取指定的通道配置信息。接下来判断是否是自定义的通道,如果不是的话,调用一个组合起来的方法名,也就是 createXXXDriver 这样的方法。我们要看的是 daily 这个通道,所以组合起来的应该是 createDailyDriver 这个方法,继续在文件中查找,果然,这个方法是存在的。

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),
    ]);
}

重点需要关心的是通道的创建,在这里它使用的是 RotatingFileHandler() 这个通道,剩下的相信不用多说了吧,这是 Monolog 自带的一个通道,每天创建一个文件,自动删除超时的文件。整体看下来,是不是和我们自定义日志通道配置的处理流程是一样一样的。

总结

通过今天的学习,我们了解到了 Laravel 中日志相关处理的流程以及使用方式。这个东西吧,大家只要做了 Laravel 项目多少都会接触到,只是平常可能就是简单地配置一下 .env 文件就完事了,并没有深入的了解。Monolog 很强大,而且也很实用,但如果你想用别的日志工具,其实也可以通过之前的文章去配置 服务提供者 和 门面 来进行方便地使用。

关于 Monolog 的内容,将来我们再单独开小系列的文章进行学习,今天日志相关的内容就简单地介绍到这里,下节课我们再了解一下 Laravel 的异常和错误处理机制。

参考文档:

https://learnku.com/docs/laravel/8.x/logging/9376

 类似资料: