小白学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
,那么实际上创建这个日志驱动的方法名称是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 协议》,转载必须注明作者和本文链接
推荐文章: