一个标准的日志格式的定义是非常有必要的。运维如果使用了ELK日志系统,那么标准化的日志格式便于运维切割搜索。

后面我将给出相对标准的日志格式,首先还是分析一下【日志模型】

日志模型

日志的基本需求是【把日志记录到某个地方

日志级别

首先,需要知道日志有【级别】的属性,在【发送】之前,我们需要指定日志的级别。

通常日志级别有:

  • DEBUG debug信息
  • INFO 感兴趣的事件或信息,如用户登录信息,SQL日志信息
  • NOTICE 重要的信息
  • WARNING 异常信息,比如请求参数不合法
  • ERROR 错误信息,比如RPC调用失败
  • EMERGENCY 很严重紧急的错误,比如服务不可用

一般常用的是DEBUGINOFWARNING , ERROR

日志Handler

定义好日志的级别后,就需要把日志发送到某个地方。

我们把提供【发送】功能抽象为”LogHandler”。

不同的【地方】对应不同的【handler】那么常见可以发送到哪些地方呢

  • stream 默认的handler,php的标准输出
  • file 发送到某个文件
  • syslog 发送给系统syslog
  • mail 通过邮件发送给指定的人
  • Slack 发送给Slack
  • error_log 发送给php的error log
  • http_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等

WriterMonolog更高级别的封装。

为什么要做更高级别的封装呢

  • 可以扩展Monolog没有的功能,比如添加【事件】 机制。
  • 把选择handler, 选择Formator操作封装到一起,便于使用

Laravel 提供了LogServiceProvider和 IlluminateSupportFacadesLog

具体实现

假设我们的需求使用以“文件”的方式存放日志,我们改怎么做。

这里需要使用 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方法可以实现注入自定义的ProcessorWriter类里面有一个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 IlluminateLogWriter as BaseWriter;
use MonologFormatterLineFormatter;

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