理解 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
负责落地的日志处理器,例如 MailHander
、RedisHandler
、StreamHandler
等。
存放 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 框架来说,非常有用。
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.php
和 Illuminate\Log\LogManager
,集中配置、管理日志。