Laravel 的日志系统 - monolog

日志的重要程度不言而喻, 不管是在开发过程中, 还是部署到生产环境后, 都是经常使用的.
随着 psr-3 的出现, 终于统一了 php 中日志的风格.
但是, 好用的记录日志系统, 也很重要.
monolog 是我遇到的最好的日志系统.
而且, laravel 中也是用的 monolog.

参考

psr-3

github.com/Seldaek/monolog

phpfudao 这可能是php世界中最好的日志库——monolog

Using Monolog

安装 (Install)

通过 composer 或者 github.

composer require monolog/monolog

文档

核心概念 (Core Concepts)

Every Logger instance has a channel (name) and a stack of handlers. Whenever you add a record to the logger, it traverses the handler stack. Each handler decides whether it fully handled the record, and if so, the propagation of the record ends there.

This allows for flexible logging setups, for example having a StreamHandler at the bottom of the stack that will log anything to disk, and on top of that add a MailHandler that will send emails only when an error message is logged. Handlers also have a $bubble property which defines whether they block the record or not if they handled it. In this example, setting the MailHandler's $bubble argument to false means that records handled by the MailHandler will not propagate to the StreamHandler anymore.

You can create many Loggers, each defining a channel (e.g.: db, request, router, ..) and each of them combining various handlers, which can be shared or not. The channel is reflected in the logs and allows you to easily see or filter records.

Each Handler also has a Formatter, a default one with settings that make sense will be created if you don't set one. The formatters normalize and format incoming records so that they can be used by the handlers to output useful information.

Custom severity levels are not available. Only the eight RFC 5424 levels (debug, info, notice, warning, error, critical, alert, emergency) are present for basic filtering purposes, but for sorting and other use cases that would require flexibility, you should add Processors to the Logger that can add extra information (tags, user ip, ..) to the records before they are handled.

每一个日志服务实例 (Logger) 都有一个通道(名称),并有一个处理器 (Handler)栈. 无论何时你添加一条 记录 到对应的日志服务实例,这个处理器栈将被遍历一遍:每个处理器都将依次决定是否要处理这条记录,而如果要处理,则遍历结束(译注:类似DOM事件冒泡)。

这样子可以创建非常灵活的日志配置。比如一个 StreamHandler 可以把所有日志都写入磁盘,而上面加个MailHandler 可以把错误日志作为邮件发送出去。处理器还有一个 $bubble 属性定义了是否屏蔽某条记录或者处理了某条记录。在这个示例中,配置 MailHandler 的 $bubble 参数为 false 则意味着 MailHandler 将不会把自己已处理过的记录继续冒泡给 StreamHandler.

你可以创建许多日志服务实例(Logger),每一个则定义一个通道(比如数据库、请求、路由...)。而每一个日志服务实例都可以组合各种各样的处理器,可以共享处理器也可以不共享。这个通道将会在日志中反映出来,从而允许你可以很容易地查看或者筛选记录。

每一个处理还会有一个格式化器(Formatter)。如果你没有配置一个,则一个有意义的默认的格式化器将被创建。格式化器用来规范化并格式化输入的记录,以便处理器能输出一些有用的信息。

不支持自定义的严重性级别。只支持使用RFC 5424中定义的八个级别(调试/Debug、信息/Info、提示/Notice、警告/Warning、错误/Error、严重/Critical、警报/Alert、紧急/Emergency)来作为基本的筛选目的。不过,如果为了排序或者其他需要灵活性的使用场景,你可以添加加工程序(Processor)从而可以在(处理器)处理前添加额外的信息(标签、用户IP...)。

给日志添加额外的信息

using the logging context

The first way is the context, allowing to pass an array of data along the record:

<?php
$logger->info('Adding a new user', array('username' => 'Seldaek'));

Simple handlers (like the StreamHandler for instance) will simply format the array to a string but richer handlers can take advantage of the context (FirePHP is able to display arrays in pretty way for instance).

using processors

The second way is to add extra data for all records by using a processor. Processors can be any callable. They will get the record as parameter and must return it after having eventually changed the extra part of it. Let's write a processor adding some dummy data in the record:

<?php

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

   return $record;
});

Monolog provides some built-in processors that can be used in your project. Look at the dedicated chapter for the list.

Tip: processors can also be registered on a specific handler instead of the logger to apply only for this handler.

  • 使用上下文(context)

第一种方式是使用上下文(context),这允许你在传递记录的时候传递一个数组格式的数据:

简单的处理器(比如StreamHandler)将只是把数组转换成字符串。而复杂的处理器则可以利用上下文的优点(如 FirePHP 则将以一种优美的方式显示数组)。

  • 使用加工程序(Processor)

第二种方式是使用加工程序来为所有的记录添加额外数据。加工程序可以是任何可以调用的函数。

加工程序接收日志记录作为参数,并且需要在修改了extra字段后再返回日志记录。

Monolog提供了一些内置的加工程序,你可以在你的项目中使用它们.

小技巧:加工程序可以被注册到一个特定的处理器上而不是直接在日志服务实例上,从而可以只在对应的处理器上生效.

mongolog中几个重要的概念

handler 日志处理器

存放 handler 的数据结构是一个“栈”,一个日志实例可以有多个 handler,通过 Logger 实例的 pushHandler 方法压入一个 handler,该方法接受一个 HandlerInterface 类型的参数.

如果你设置了多个 handler,当你新增一条日志的时候,他会从栈顶开始往下传播,关心这个级别日志的 handler 将会处理这条日志.

所有的 handler 都会继承 AbstractProcessingHandler 这个抽象类,并且只需要实现里面的抽象方法 write 就可以了.

同时这个抽象类会继承 AbstractHandler 这个抽象类,这个抽象类的构造函数有两个参数:levelbubble.

level:表示该 handler 关心的最低日志级别,是个整型.

bubble:表示日志被当前 handler 处理后是否接着向下传递.

formatter 设置日志格式

每个 handler 可以单独设置记录的日志格式,AbstractHandler 抽象类中有一个 setFormatter 方法,该参数接受一个 FormatterInterface 类型的参数.

可以看到 monolog 自带的 formatter 都继承自 ormalizerFormatter,该类实现了 format和formatBatch 方法。

processor 日志加工程序,用来给日志添加额外信息

存放 processor 的结构也是一个“栈”,意味着你也可以通过 pushProcessor 方法给一个 Logger 实例配置多个 processor

我们注意到,这里 pushProcesso 接受一个 callable ,也就是需要一个函数或者类方法,但是官方自带的这些 processor 都是类,

随便点进去一个源码就会发现,其实这些类都用到了 __invoke 魔术方法,所以在被当做 callable 调用的时候会自动调用 __invoke.

代码实践

代码在我的test仓库中可以下载.

github 测试代码

<?php
/**
 * monolog package usage
 * User: qiuyu
 * Date: 2018/2/14
 * Time: 下午1:44
 */

require dirname(__FILE__)."/../vendor/autoload.php";

use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Processor\UidProcessor;
use Monolog\Processor\ProcessIdProcessor;
use Monolog\Formatter\JsonFormatter;

// 实例化一个日志实例, 参数是 channel name
$logger = new Logger('qiuyuhome');

// StreamHandler_1
$streamHander1 = new StreamHandler(__DIR__.'/testLog1.log', Logger::INFO);
// 设置日志格式为json
$streamHander1->setFormatter(new JsonFormatter());
// 入栈, 往 handler stack 里压入 StreamHandler 的实例
$logger->pushHandler($streamHander1);

// StreamHandler_2
// 如果第三个参数为false, 则只会执行这个一个Handler. 默认是true
$streamHander2 = new StreamHandler(__DIR__.'/testLog2.log', Logger::INFO);
// 入栈, 往 handler stack 里压入 StreamHandler 的实例
$logger->pushHandler($streamHander2);

/**
 * processor 日志加工程序,用来给日志添加额外信息.
 *
 * 这里调用了内置的 UidProcessor 类和 ProcessIdProcessor 类.
 * 在生成的日志文件中, 会在最后面显示这些额外信息.
 */
$logger->pushProcessor(new UidProcessor());
$logger->pushProcessor(new ProcessIdProcessor());
$logger->pushProcessor(function ($record) {
    $record['message'] = 'Hello ' . $record['message'];
    return $record;
});

/**
 * 设置记录到日志的信息.
 *
 * 开始遍历 handler stack.
 * 先入后出, 后压入的最先执行. 所以先执行 FirePHPHandler, 再执行 StreamHandler
 * 如果设置了 ErrorLogHandler 的 $bubble = false, 会停止冒泡, StreamHandler 不会执行.
 * 第二个参数为数组格式, 通过使用使用上下文(context)添加了额外的数据.
 * 简单的处理器(比如StreamHandler)将只是把数组转换成字符串。而复杂的处理器则可以利用上下文的优点(如 FirePHP 则将以一种优美的方式显示数组).
 */
$logger->info('Welcome to QiuYu Blog.', ['username' => 'QiuYu']);

运行结果

运行文件的同目录下, 会生成 testLog1.logtestLog2.log 日志文件. 内容为:

testLog1.log

{"message":"Hello Welcome to QiuYu Blog.","context":{"username":"QiuYu"},"level":200,"level_name":"INFO","channel":"qiuyuhome","datetime":{"date":"2018-02-14 23:33:28.928112","timezone_type":3,"timezone":"Asia/Shanghai"},"extra":{"process_id":21520,"uid":"54a8a7d"}}

testLog2.log

[2018-02-14 23:33:28] qiuyuhome.INFO: Hello Welcome to QiuYu Blog. {"username":"QiuYu"} {"process_id":21520,"uid":"54a8a7d"}
本作品采用《CC 协议》,转载必须注明作者和本文链接
当才华还支持不起理想时,就应该静下心来好好学习了。
本帖由系统于 4年前 自动加精
qiuyuhome
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
讨论数量: 6

@qiuyuhome 最主要的区别是
普通的日志组件

假设一个接口里,里写了5次记录日志,

对于monolog而言是5次写磁盘操作,也就是5次IO,

在高并发场下,日志写入到磁盘中,

机器的磁盘IO ,网络IO,内存操作 都会对CPU的负载造成压力.

频繁的刷磁盘,CPU会因为日志写入导致占用率过高,
因为接口本身的逻辑 1. 读写Mysql 2.读写Redis 3.队列任务
本身就已经有了不少的网络IO和磁盘IO操作了.

那,seasLog是怎么做的呢?
具体如下
你可以在配置文件中自定义 每N次 日志数据写入到磁盘一次
在写入磁盘之前先存储在buffer中, 也就是内存中
对于上面的场景 相当于5次磁盘IO变成了一次磁盘IO

1.高并发下日志不会成为性能瓶颈 秒QPS 8000

2.自带日志分析预警框架(默认关闭)(主要用来分析Error日志)
A:统计某种日志级别的数量
B:查看某种日志级别的所有日志 C:通过邮件方式发出日志报警

3.支持每log N次后,日志数据写入磁盘(日志缓冲区)

4.支持RequestId 请求全局id区分请求

5.支持按照时间切割日志

具体请看github里的介绍 github.com/SeasX/SeasLog/blob/mast...

觉得不错,可以贡献个star

我是seaslog的受益者,

使用这个组件后,解决了我司的日志对CPU占用问题.

还有链路追踪问题,得益于(全局的RequestId )

这个好东西,分享给你. :grin:

3年前 评论

@qiuyuhome 最主要的区别是
普通的日志组件

假设一个接口里,里写了5次记录日志,

对于monolog而言是5次写磁盘操作,也就是5次IO,

在高并发场下,日志写入到磁盘中,

机器的磁盘IO ,网络IO,内存操作 都会对CPU的负载造成压力.

频繁的刷磁盘,CPU会因为日志写入导致占用率过高,
因为接口本身的逻辑 1. 读写Mysql 2.读写Redis 3.队列任务
本身就已经有了不少的网络IO和磁盘IO操作了.

那,seasLog是怎么做的呢?
具体如下
你可以在配置文件中自定义 每N次 日志数据写入到磁盘一次
在写入磁盘之前先存储在buffer中, 也就是内存中
对于上面的场景 相当于5次磁盘IO变成了一次磁盘IO

1.高并发下日志不会成为性能瓶颈 秒QPS 8000

2.自带日志分析预警框架(默认关闭)(主要用来分析Error日志)
A:统计某种日志级别的数量
B:查看某种日志级别的所有日志 C:通过邮件方式发出日志报警

3.支持每log N次后,日志数据写入磁盘(日志缓冲区)

4.支持RequestId 请求全局id区分请求

5.支持按照时间切割日志

具体请看github里的介绍 github.com/SeasX/SeasLog/blob/mast...

觉得不错,可以贡献个star

我是seaslog的受益者,

使用这个组件后,解决了我司的日志对CPU占用问题.

还有链路追踪问题,得益于(全局的RequestId )

这个好东西,分享给你. :grin:

3年前 评论

phpfudao 这可能是php世界中最好的日志库——monolog 这个链接有毒啊

5年前 评论
Meriodas

为最后一句话 点赞 > 当才华还支撑不起理想的时候,就应该静下心来好好学习。
再加一句 > 我语言的界限就是我知识的界限。

4年前 评论

SeasLog是用C语言编写的PHP扩展库,功能强大而且性能极高 monolog比这个差远了。

3年前 评论
qiuyuhome (楼主) 3年前
kenvent 3年前
qiuyuhome

@maoyizhi 好的, 减少磁盘 I / O 压力, 确实不错, 感谢分享, 扩充了知识面.

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
PHP主管 @ 北京众晶锐驰科技有限公司
文章
1
粉丝
10
喜欢
27
收藏
30
排名:347
访问:2.7 万
私信
所有博文
社区赞助商