用于格式化输出程序日志,方便从日志中定位程序运行过程中出现的问题。这里的日志除了日志内容本身之外,还应该包括文件名/行号,时间戳,线程/协程号,模块名称,日志级别等额外信息,甚至在打印致命的日志时,还应该附加程序的栈回溯信息,以便于分析和排查问题。
从设计上看,一个完整的日志模块应该具备以下功能:
- 区分不同的级别,比如常的DEBUG/INFO/WARN/ERROR等级别。日志模块可以通过指定级别实现只输出某个级别以上的日志,这样可以灵活开关一些不重要的日志输出,比如程序在调试阶段可以设置一个较低的级别,以便看到更多的调度日志信息,程序发布之后可以设置一个较高的级别,以减少日志输出,提高性能。
- 区分不同的输出地。不同的日志可以输出到不同的位置,比如可以输出到标准输出,输出到文件,输出到syslog,输出到网络上的日志服务器等,甚至同一条日志可以同时输出到多个输出地。
- 区分不同的类别。日志可以分类并命名,一个程序的各个模块可以指定各自的日志类来输出日志,这样可以很方便地从日志中判断出当前日志是哪个程序模块输出的。
- 日志格式可灵活配置。可以按需指定日志消息是否包含文件名/行号、时间戳、线程/协程号、日志级别、启动时间等内容。
- 可通过配置文件的方式配置以上功能。
这里先通过一个典型的C++日志框架log4cpp来分析以上几点的运行。关于log4cpp的介绍和使用可参考官网:http://log4cpp.sourceforge.net/,下面是一个它的示例:
#include <log4cpp/Category.hh>
#include <log4cpp/Appender.hh>
#include <log4cpp/OstreamAppender.hh>
#include <log4cpp/FileAppender.hh>
#include <log4cpp/Layout.hh>
#include <log4cpp/PatternLayout.hh>
#include <log4cpp/PropertyConfigurator.hh>
int main() {
// 日志类别
log4cpp::Category &root_logger = log4cpp::Category::getRoot(); // 获取root日志器,root日志器名称默认为空
log4cpp::Category &file_logger = log4cpp::Category::getInstance("file_logger"); // 获取指定名称为file_logger的日志器
// 日志输出地
log4cpp::Appender *coutAppender = new log4cpp::OstreamAppender("cout", &std::cout); // 输出到终端
log4cpp::FileAppender *fileAppender = new log4cpp::FileAppender("file", "./log.txt"); // 输出到文件
// 日志格式
log4cpp::PatternLayout *coutPattern = new log4cpp::PatternLayout();
coutPattern->setConversionPattern("[%d{%Y-%m-%d %H:%M:%S:%l}] %p %m%n"); // 格式:[年:月:日 时:分:秒:毫秒] 日志级别 日志内容
log4cpp::PatternLayout *filePattern = new log4cpp::PatternLayout();
filePattern->setConversionPattern("[%d{%Y-%m-%d %H:%M:%S:%l}] %R %c %p %m%n"); // 格式:[年:月:日 时:分:秒:毫秒] UTC秒数 日志器名称 日志级别 日志内容
// 绑定日志器和appender,设置日志器的输出级别
coutAppender->setLayout(coutPattern);
root_logger.addAppender(coutAppender);
root_logger.setPriority(log4cpp::Priority::INFO);
fileAppender->setLayout(filePattern);
file_logger.addAppender(fileAppender);
file_logger.setPriority(log4cpp::Priority::ERROR);
// 支持c风格的日志打印和流式的日志打印
root_logger.debug("debug msg");
root_logger.debugStream() << "debug msg by stream";
root_logger.info("info msg");
root_logger.infoStream() << "info msg by stream";
root_logger.error("error msg");
root_logger.errorStream() << "err msg by stream";
file_logger.debug("debug msg");
file_logger.debugStream() << "debug msg by stream";
file_logger.info("info msg");
file_logger.infoStream() << "info msg by stream";
file_logger.error("error msg");
file_logger.errorStream() << "err msg by stream";
return 0;
}
上面的代码在终端输出如下:
[2021-06-25 09:33:59:734] INFO info msg [2021-06-25 09:33:59:734] INFO info msg by stream [2021-06-25 09:33:59:734] ERROR error msg [2021-06-25 09:33:59:734] ERROR err msg by stream [2021-06-25 09:33:59:734] ERROR error msg [2021-06-25 09:33:59:734] ERROR err msg by stream
日志文件内容如下:
[2021-06-25 09:33:59:734] 1624584839 file_logger ERROR error msg [2021-06-25 09:33:59:734] 1624584839 file_logger ERROR err msg by stream
对于log4cpp总结如下:
1. Category对应日志器类,使用 log4cpp::Category::getInstance() 获取指定名称的日志器实例,如果两个日志器名称相同,那么对应同一个日志器实例。
2. 使用Category的的setPriority()方法设置日志器的日志级别,日志级别使用log4cpp::Priority枚举值来表示。
3. 使用Appender来表示日志输出地,Appender可以细分为OstreamAppender和FileAppender等不同类型,一个Category可以有多个Appender,通过addAppender()方法为Category新增Appender。
4. 使用PatternLayout来表示日志的格式,通过字符串来表示,比如%d表示时间,后面可用{}指定具体的时间格式,%R表示UTC秒数,%c表示日志器名称,%p表示日志级别,%m表示日志消息等
5. PatternLayout和Appender绑定,Priority和Category绑定,也就是Appender不具备日志级别判断功能。
下面开始sylar的日志模块设计,首先是日志级别,这个参考log4cpp即可,一共有以下几个级别:
接下来是几个关键的类:
class LogFormatter{}
class LogAppender{}
class Logger{}
class LogManager{}
关于这几个类的设计概念如下:
LogFormatter: 日志格式化类,构造时指定pattern,提供format方法,将原始的日志消息按指定的pattern进行内容丰富
LogAppender: 日志输出器,内部包含LogFormatter成员,提供log方法,将日志消息进行格式化后输出到指定的输出地,从这个类可以派生出不同的Appender类型
Logger: 日志器,包含多个LogAppender和一个日志级别,提供log方法,传入日志和日志的级别,判断级别高于日志器的级别之后调用LogAppender将日志进行输出
LogManager: 日志器管理类,单例模式,用于统一管理所有的日志器,提供日志器的创建与获取方法。LogManager自带一个Root Logger,为日志模块提供一个初始时可用的日志器
至此,日志模块就基本设计完了,一条日志先经Logger类的log方法,判断日志级别,再传入各个LogAppender,用LogFormatter格式化之后再输出到各个目的地。
由于一条日志总是对应一个日志级别,并且一条日志只会在一个Logger上进行输出,所以这里为了简化设计,定义一个LogEvent类,将日志相关的属性进行封装,具体来说,LogEvent包含以下成员:
日志器名称
日志内容
日志级别
对应的日志器
文件名/行号
启动时间
实时时间
线程/协程号
线程名称
...
上面的设计与sylar的唯一不同点是,sylar的LogAppender是可以单独设置日志级别的,一条日志要先经过Logger进行一次级别判断,然后在LogAppender输出时还要再判断一次级别,个人感觉增加了复杂度,可以简化掉。
日志模块剩下的工作就是定义一些功能宏用于增加易用性了,这部分参考源码即可理解。
与sylar的区别:
1. Logger不带root Looger,并且默认没有Appender,只有LoggerManager有root logger
2. 重新实现了一套Formatter pattern解析,支持%%转义
3. LogEvent支持%r,也就是程序启动的累计毫秒数
sylar的日志设计:
每个日志器,默认有一个root模块,名称为root,默认只有stdout appender,也就是向终端打印
对于框架内的源码,全部使用system模块,对于测试代码,全部使用root模块。
获取线程名称,sylar的早期版本是用pthread_setname_np/pthread_getname_np来实现的,后期改用自定义的线程局部变更,这里还是用老的方法,方便使用系统工具比如ps和top查看线程运行
todo:
循环写文件appender, 日志文件分片