Log (日志)
一个标准的日志格式的定义是非常有必要的。运维如果使用了ELK日志系统,那么标准化的日志格式便于运维切割搜索
后面我将给出相对标准的日志格式,首先还是分析一下【日志模型】
日志模型
日志的基本需求是【把日志记录到某个地方
】
日志级别
首先,需要知道日志有【级别】的属性,在【发送】之前,我们需要指定日志的级别。
通常日志级别有:
DEBUG
debug信息INFO
感兴趣的事件或信息,如用户登录信息,SQL日志信息NOTICE
重要的信息WARNING
异常信息,比如请求参数不合法ERROR
错误信息,比如RPC调用失败EMERGENCY
很严重紧急的错误,比如服务不可用
一般常用的是DEBUG
, INOF
, WARNING
, ERROR
日志Handler
定义好日志的级别后,就需要把日志发送到某个地方。
我们把提供【发送】功能抽象为"LogHandler"。
不同的【地方】对应不同的【handler】那么常见可以发送到哪些地方呢
stream
默认的handler,php的标准输出file
发送到某个文件syslog
发送给系统syslogmail
通过邮件发送给指定的人Slack
发送给Slackerror_log
发送给php的error loghttp_log_server
通过http 协议发送给日志服务器
日志Record
试想一下,当我们记录日志的时候,我们不仅仅需要记录message.
还需要记录一些其他的内容,比如日志记录的时间等。
在laravel中除了message
以外,laravel还会自动记录 datetime, channel, level等信息:
$record = array(
'message' => (string) $message, # 信息的主体
'context' => $context,
'level' => $level, # 信息的级别
'level_name' => $levelName,
'channel' => $this->name, # 表示是由哪个应用发送的
'datetime' => $ts,
'extra' => array(),
);
如果还需要记录请求的ip信息 所以就需要有个处理器把【请求ip】传递给record,达到的效果类似于:
$record['request_id'] = $ip;
扩充record信息的角色,我们抽象为Processor
。
简单理解,Processor
就是一个匿名函数,扩展record
的信息
processor = function (array $record) {
$record['now'] = substr($record['datetime']->format('Y-m-d H:i:s.u'), 0, -3);
$record['ip'] = getIp();
return $record;
}
日志Format
在上文中,我们提到日志最终会保存在一个record数组中。
发送日志的时候,我们还得考虑一个问题,那就是日志的格式化这个数组。
负责这部分的模块,我们可以抽象为Formatter
.
那常见的Formatter
有哪些呢
LineFormatter
格式化成一个字符串JsonFormatter
格式化为一个json字符串
我们常用的是把日志格式化为字符串
日志模型总结
在总结一下,一个日志模型涉及到了如下几个对象:
Handler
: 发送日志到某个地方Formator
: 格式化日志Processor
: 填充需要记录的信息
Laravel Log
laravel提供了一个Writer
的类作为日志的请求入口, Writer
提供一系列的api,如info, error等
Writer
是Monolog
更高级别的封装。
为什么要做更高级别的封装呢
可以扩展
Monolog
没有的功能,比如添加【事件】 机制。把选择handler, 选择Formator操作封装到一起,便于使用
Laravel 提供了LogServiceProvider
和 Illuminate\Support\Facades\Log
具体实现
假设我们的需求使用以“文件”的方式存放日志,我们改怎么做。
这里需要使用 FileHandler
'FileHandler‘有一个特殊的功能是【文件分割】,比如可以以天分割,一天一个日志文件。
可以看一下LogServiceProvider
的源代码
class LogServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton('log', function () {
return $this->createLogger();
});
}
public function createLogger()
{
$monolog = new Monolog($this->channel());
$log = new Writer($monolog, $this->app['events'])
# 初始化Handler
$this->configureHandler($log);
return $log;
}
protected function configureHandler(Writer $log)
{
# handler()返回从config对象的app.log读取到的配置
$this->{'configure'.ucfirst($this->handler()).'Handler'}($log);
}
# 以天分割日志
protected function configureDailyHandler(Writer $log)
{
$log->useDailyFiles(
$this->app->storagePath().'/logs/laravel.log', $this->maxFiles(),
$this->logLevel()
);
}
}
class Writer
{
# 可以看到monolog通过注入的方式注入到了Writer
public function __construct(MonologLogger $monolog, Dispatcher $dispatcher = null)
{
$this->monolog = $monolog;
if (isset($dispatcher)) {
$this->dispatcher = $dispatcher;
}
}
# 记录info 级别的日志
public function info($message, array $context = [])
{
$this->writeLog(__FUNCTION__, $message, $context);
}
# 加入事件机制
protected function writeLog($level, $message, $context)
{
$this->fireLogEvent($level, $message = $this->formatMessage($message), $context);
$this->monolog->{$level}($message, $context);
}
# path就是日志保存的文件,days是保存日志的数量,0表示日志数量没有限制,否则会自动删除相关日志
public function useDailyFiles($path, $days = 0, $level = 'debug')
{
# 注意hanlder是RotatingFileHandler,这里会自动实现日志分割
$this->monolog->pushHandler(
$handler = new RotatingFileHandler($path, $days, $this->parseLevel($level))
);
# handler里面可以设置Formatter
$handler->setFormatter($this->getDefaultFormatter());
}
# 配置Formatter 怎么自定义Formatter呢,
# 可以自定义一个`UserDefineWriter`类继承`Writer`, 改写getDefaultFormatter 方法
protected function getDefaultFormatter()
{
return new LineFormatter(null, null, true, true);
}
}
自定义processor
到目前为止,我们知道laravel怎么设置handler
, 怎么重写Formatter
, 还有一个最重要的问题没有说明,那就是怎么注入processor
这个方法Monolog
类里面有一个pushProcessor
方法可以实现注入自定义的Processor
, Writer
类里面有一个getMonolog
函数可以获取到Monolog
对象
# 注入自定义的`processor`
Log::getMonolog()->pushProcessor(function (array $record) {
$record['now'] = substr($record['datetime']->format('Y-m-d H:i:s.u'), 0, -3);
$record['pid'] = getmypid();
$record['session_id'] = session_id() ?: 'null';
$record['ip'] = Request::ip();
return $record;
});
同样的道理,如果你想使用Monolog
提供的SyslogHandler
,那么只需要这样
# ident 一般是程序的标示类似于channel
# facility 是系统syslog提供的,一般使用LOG_USER
Log::getMonolog()->pushHandler(SyslogHandler($ident, $facility, $level));
到目前为止,Writer
的使用套路就讲完了。
自定义日志格式
标准的格式"时间 级别 pid [通道 seq_id request_id] ip session_id message context"
use Illuminate\Log\Writer as BaseWriter;
use Monolog\Formatter\LineFormatter;
class Writer extends BaseWriter
{
protected function getDefaultFormatter()
{
return new LineFormatter("%now% %level_name% %[%pid%]: [%channel% %seq_id% %request_id%] %ip% %session_id% ## %message% %context%\n", "Y-m-d H:i:s.u", true);
}
}
error和access日志分离
laravel默认没有实现 access和error日志的分离。
所有的日志都定义在 $this->app->storagePath().'/logs/laravel.log'
为什么要实现分离呢,比如可以便于日志监控。
怎么实现分离呢, 定义两个handler
#level The minimum logging level at which this handler will be triggered
# 会记录info,warning, error信息
$logger->useDailyFiles($this->app['config']['log.file'], 0, 'info');
# 只会记录error信息
$logger->useDailyFiles($this->app['config']['log.error'], 0, 'error');