我是FFZero,一个10年开发经验的cpper。
ZLToolkit是著名流媒体开源库ZLMediakit的底层工具库,封装了日志,时间通知,IO复用等能力,对我们学习实践C++有很大参考价值。
本文中使用的C++语法(模板,基本语法,类)都可以参考我的专栏 C++补完计划,哪里不会点哪里。
本文介绍ZLToolkit的Logger模块,一起看看如何实现一个实用的Logger类吧。
Logger介绍
Logger类位于ZLToolkit/src/Util 路径下,是典型的工具类,负责所有的日志打印。Logger的基类std::enable_shared_frome_this和noncopyable都可以在 C++补完计划中找到对应的文章进行学习。
Logger具有下面的能力:
- 支持控制台,文件,广播三种输出通道,支持异步输出(单独线程写日志)。
- 支持按时间、文件数量,文件大小进行清理和切片。
- 支持三种输出风格(cout, printf, 可变长模板)。
- 支持重载<<的类。
Logger使用示例
#include "Util/logger.h"
int main() {
// Initialize the logging system
Logger::Instance().add(std::make_shared<ConsoleChannel> ());//输出日志到终端
Logger::Instance().add(std::make_shared<FileChannel>()); ////输出日志到文件,默认路径:exeDir() + "log/"
Logger::Instance().setWriter(std::make_shared<AsyncLogWriter>()); //设置日志写文件为异步写入(单独线程写文件)
TraceL << "int"<< (int)1 << endl; //cout输出风格
PrintT("this is a %s test:%d", "printf trace", 124); //printf输出风格
LogT(1, "+", "2", '=', 3); //可变长模板输出风格
return 0;
}
上面的代码初始化了日志系统,日志同时输出到终端和文件,设置了异步写文件。后面展示了输出文件的三种方式。
源码分析
类图
Logger类是单例类,负责管理所有的输出通道(LogChannel),设置日志等级和是否开启异步写文件。
LogContext是日志上下文类,继承自std::ostringstream,operator<<存储内容,str函数输出日志内容字符串。
类分析
- LogContext
存储日志的打印信息,继承了ostringstream用<<存储内容和str()输出日志内容的字符串。
_repeat 成员变量用来记录重复日志的次数,避免短时间重复打印同样的日志。
使用如下:
void Logger::writeChannels(const LogContextPtr &ctx) {
if (ctx->_line == _last_log->_line && ctx->_file == _last_log->_file && ctx->str() == _last_log->str()) {
//重复的日志每隔500ms打印一次,过滤频繁的重复日志
++_last_log->_repeat;
if (timevalDiff(_last_log->_tv, ctx->_tv) > 500) { //超过500ms,打印一条
ctx->_repeat = _last_log->_repeat; //ctx->_repeat要继承过来
writeChannels_l(ctx);
}
return;
}
if (_last_log->_repeat) { //上一条日志短时间内一直重复,没有打印出来,最后打印一下
writeChannels_l(_last_log);
}
writeChannels_l(ctx);
}
-
Logger
单例,核心类,所有的重要操作几乎都是由这个类完成。- add() 添加输出通道
- setWriter() 设置异步写日志线程
- setLevel() 设置所有日志通道的log等级
- write() 写日志,如果调用过setWriter,则异步写,否则直接同步写
-
LogChannel
日志通道,负责格式化日志并打印到输出流
void format(const Logger &logger, std::ostream &ost, const LogContextPtr &ctx, bool enable_color, bool enable_detail)函数负责将LogContex的内容进行格式化并输出ost中。- ConsoleChannel: format函数的输出参数设置位std::cout。
- FileChannelBase: 文件输出通道的基类。
- FileChannel: 文件输出实际使用的类,自动清理的日志文件通道。主要是在write前检测文件夹和文件大小,并创建新文件、更新index信息、删除过期的日志文件。最后调用FileChannelBase的write函数写文件。
- EventChannel: 事件通道。向注册在“kBroadcastLogEvent”的listener emit日志上下文信。
-
LogWriter/AsyncLogWriter
LogWriter只是作为一个基类,几乎没有实现。AsyncLogWriter是一个使用率很高的类,这个类开启了一个线程用于写日志。
write函数用于将日志暂存进_pending对象,并唤醒阻塞的信号量刷新日志。
** flushAll**函数用于刷新日志。void AsyncLogWriter::flushAll() { decltype(_pending) tmp; { lock_guard<mutex> lock(_mutex); tmp.swap(_pending); } tmp.for_each([&](std::pair<LogContextPtr, Logger *> &pr) { pr.second->writeChannels(pr.first); }); }
这个函数将_pending的日志先交换到tmp对象再进行打印。可以优先减少锁的时间,提高效率。
-
LogContextCapture
每次打印的时候会生成该类的临时对象,在构造时生成LogContext对象并与logger对象进行关联,结束后主要是生成LogContext对象并将logger对象于其进行关联。比较有意思的是LogContextCapture &operator<<(std::ostream &(*f)(std::ostream &))这个函数,会在参数为std::endl(回车符)的时候是调用,这个函数会调用_logger.write(_ctx)将日志立即输出。 -
LoggerWrapper包装类
这个类用于支持printf和可变参数格式。实现比较简单,但是对模板的使用很巧妙。template<typename First, typename ...ARGS> static inline void printLogArray(Logger &logger, LogLevel level, const char *file, const char *function, int line, First &&first, ARGS &&...args) { LogContextCapture log(logger, level, file, function, line); log << std::forward<First>(first); appendLog(log, std::forward<ARGS>(args)...); } template<typename Log, typename First, typename ...ARGS> static inline void appendLog(Log &out, First &&first, ARGS &&...args) { out << std::forward<First>(first); appendLog(out, std::forward<ARGS>(args)...); } template<typename Log> static inline void appendLog(Log &out) {}
将第一个参数用First &&first和其余参数区分开。First &&first和std::forward(first)是完美转发,不改变first的左值或者右值属性。printLogArray创建了一个LogContextCapture对象,交给appendLog进行<<处理,直到最后一次空调用。
C++11
List及const成员函数
template<typename T>
class List : public std::list<T> {
public:
template<typename ... ARGS>
List(ARGS &&...args) : std::list<T>(std::forward<ARGS>(args)...) {};
~List() = default;
void append(List<T> &other) {
if (other.empty()) {
return;
}
this->insert(this->end(), other.begin(), other.end());
other.clear();
}
template<typename FUNC>
void for_each(FUNC &&func) {
for (auto &t : *this) {
func(t);
}
}
template<typename FUNC>
void for_each(FUNC &&func) const { //const List<int>& listRef = list; const List* listPtr = &list调用的版本
for (auto &t : *this) {
func(t);
}
}
};