理解 MonoLog 日志类库的工作流程

GitHub: https://github.com/Seldaek/monolog/

最佳实践

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\FirePHPHandler;

// 创建日志实例
$logger = new Logger('日志实例标识');

// 添加日志处理器
$logger->pushHandler(new StreamHandler(__DIR__ . '/my_app.log', Logger::DEBUG));
$logger->pushHandler(new FirePHPHandler());

// 开始记录
$logger->info('My logger is now ready');
$logger->error('My logger is now ready');

核心名词概念

1. Logger

日志实例对象(或者称日志场景)例如订单支付成功日志、队列失败日志等

$logger = new Logger('日志实例标识');

2. Handler

负责落地的日志处理器,例如 MailHanderRedisHandlerStreamHandler 等。

存放 Handler 的数据结构是一个“栈”,最后压入的的会被最先执行。 所有的 Handler 都会继承 AbstractProcessingHandler 并实现 write() 方法。构造函数有两个参数:

  • level 表示该 Handler 关心的最低日志级别(查看级别定义
  • bubble 表示日志被当前 Handler 处理后是否还需要继续传递

3. Formatter

定义了日志记录的格式。 每个 Handler 可以单独设置记录的日志格式,例如:

// 文件日志处理器
$handler = new StreamHandler(__DIR__.'/my_app.log', Logger::INFO);

// 转换为 JSON 记录
$handler->setFormatter(new JsonFormatter());

可以看到my_app.log 中记录的日志就变为 JSON 格式了。

4. Processor

额外信息添加器,可以给一条日志添加额外的信息。

Monolog 有两种方法可以记录除了 message (第一个参数)之外的信息。

(1) 记录时使用第二个参数 context 记录上下文

$logger->info('Adding a new user', ['username' => 'Seldaek']);

(2) 使用 Processors

Processors 可以是任何 callable 的对象(例如闭包函数和类方法)。

$logger->pushProcessor(function (array $record) {
    $record['extra']['dummy'] = 'Hello world!';
    return $record;
});

提示:Processors 不仅可以应用在 Logger 上,也可以应用在指定 Handler 上。

Monolog 提供的开箱即用的 Processors:

https://github.com/Seldaek/monolog/blob/master/doc/02-handlers-formatters-processors.md#processors

use Monolog\Processor\UidProcessor;
use Monolog\Processor\ProcessIdProcessor;

$logger = new Logger('my_logger');
$logger->pushHandler(new StreamHandler('...', Logger::INFO));

// 额外记录 $_REQUEST 等请求报文信息
$logger->pushProcessor(new WebProcessor);

// 额外记录进程ID
$logger->pushProcessor(new ProcessIdProcessor);

$logger->info('Adding a new user');

5. Message

记要记录的日志文本信息 参见数据结构

使用心得

查看 Monolog 自带的 Handler / Formatters / Processors 一览

Monolog/ErrorHandler

https://github.com/Seldaek/monolog/blob/master/src/Monolog/ErrorHandler.php

不要被它的类名骗了,它实际上一个助手类,主要负责注册并接管捕捉所有异常和错误(即通过 set_exception_handler + set_error_handler + register_shundown_function 接管所有错误异常),对于没有集成日志管理的 PHP 框架来说,非常有用。

原理详见 PHP7 Error & Exception 知识点整理

Wrapper Handler 装饰器

  • Monolog/Handler/FingersCrossedHandler

先缓冲住所有等级的日志,直到某条新日志达到了我们指定的等级(可理解为触发了我们设置的错误红线),所有日志才会批量落地,否则之前缓冲在 PHP 数组中的日志将被丢弃。

  • Monolog\Handler\BufferHandler

先将 $record 临时积攒在 PHP 数组中(即内存中),当达到一定条数后,再批量落地。

  • Monolog\Handler\DeduplicationHandler

继承于 BufferHandler。对一段时间内的 $record 进行缓冲并去重,可避免生产服短时间内大量重复的邮件错误报警。

  • Monolog\Handler\GroupHandler

对多个 Handler 进行分组,相当于使用时调用多次 pushHandler,这个包装器只是为了便于分组复用。

foreach ($this->handlers as $handler) {
    $handler->handle($record);
}
  • Monolog\Handler\WhatFailureGroupHandler

继承于 GroupHandler,在遍历循环处理时 try-catch-continue 遇到错误则忽略,不中断,让循环可以继续执行。

foreach ($this->handlers as $handler) {
    try {
        $handler->handle($record);
    } catch (\Throwable $e) {
        // What failure?
        // do nothing
    }
}

扩展自己的 Handler

例如 Monolog 没有现成的 Db 日志落地处理器(虽然不常用),我在 Laravel 里补充了一下,上代码:

use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;

// @see https://github.com/Seldaek/monolog/blob/master/doc/04-extending.md
class DatabaseHandler extends AbstractProcessingHandler
{
    protected $table;

    public function __construct(string $table, $level = Logger::INFO, $bubble = true)
    {
        $this->table = $table;

        parent::__construct($level, $bubble);
    }

    protected function write(array $record)
    {
        DB::table($this->table)->insert([
            'level'      => $record['level'],
            'level_name' => $record['level_name'],
            'channel'    => $record['channel'],
            'message'    => $record['message'],
            'context'    => toJson($record['context']),
            'extra'      => toJson($record['extra']),
            'created_at' => $record['datetime']->format('Y-m-d H:i:s'),
        ]);
    }
}

另一个完整示例,零信 LeanChatHandler

namespace App\Extensions\Logger;

use Monolog\Logger;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Handler\Curl\Util;

/**
 * 零信 Incoming 通知(扩展 Monolog)
 *
 * @author JiangJian <silverd@sohu.com>
 *
 * @see https://pubu.im/integrations
 * @see https://github.com/Seldaek/monolog/blob/master/doc/04-extending.md
 */

class LeanChatHandler extends AbstractProcessingHandler
{
    private $channel;

    public function __construct($channel, $level = Logger::ERROR, $bubble = true)
    {
        parent::__construct($level, $bubble);

        $this->channel = $channel;
    }

    protected function write(array $record)
    {
        $url = 'https://hooks.pubu.im/services/' . $this->channel;

        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, [
            'text' => $record['message'],
        ]);

        Util::execute($ch);
    }
}

附录

如果是独立的项目想使用 Monolog 库,可以尝试 https://github.com/theorchard/monolog-cascade

这个库作用类似于 Laravel 5.6+ 封装的 config/logging.phpIlluminate\Log\LogManager,集中配置、管理日志。