日志模块
日志模块主要用于格式化输出程序日志,方便后续从日志中定位程序运行过程中出现的问题。当然日志除了日志内容本身之外,还应该包括文件名、行号、时间戳、线程、协程号、日志级别等信息。在输出错误日志时,还应附加程序的函数调用栈信息,便于后续分析和排查问题
从设计上看,一个完整的日志模块应该具备以下几点基本功能
- 日志级别。日志应该分为不同的级别,并且可以很方便的控制程序只输出大于某一级别的日志。比如:在调试和测试阶段可以设置低级别日志,尽可能多输出相关信息,方便开发人员定位问题;在程序发布之后设置一个较高级别的日志,以减少日志输出对性能产生的影响
- 日志格式。可灵活配置每条日志应该携带哪些附加信息和输出格式
- 日志类别。日志可以分类,同一程序可以同时有多个日志类别。这样可以很清晰的知道到当前日志属于哪一模块
- 日志输出。可灵活配置不同模块的日志可输出到不同目的地,同时同一条日志也应该可被输出到多个目的地
GitHub
- https://github.com/huxiaohei/tiger.git
实现
日志模块主要包含LogLevel
、LogEvent
、LogEventWarp
、LogFormatter
、LogAppender
、Logger
、LoggerMgr
类和用于定义配置格式的LoggerDefine
与AppenderDefine
结构体,以及方便使用的宏
LogLevel
用于定义日志级别,以及日志级别与字符串的相互转换
LogEvent
日志事件用于记录日志现场,包括日志发生的日志级别、行号、文件名、线程号、线程名称、协程号、事件、日志器名称等
LogEventWarp
日志事件包装类,在日志现场构造并包装日志事件对象,在日志记录结束后,LogEventWrap
析构时会使用日志事件中当时保存的日志器进行日志输出。包装类有利于后续使用宏来简化日志模块的使用
LogFormatter
日志格式器,用于格式化一个日志事件。由于日志事件包含了很多项(文件名、行号、时间戳、线程、协程号、日志级别等),但使用者并不希望每次都输出全部项,并且希望在日志中添加一个指定字符串。 因此日志格式器提供了模版字符串用于满足使用者这一需求。
日志格式器在构造的时候会先解析模版,获取模版所需对象的日志项并保存到日志器的一个属性中。在真的输出日志时,取出对应日志项实例组,将日志事件作为参数传入到每个日志项输出具体日志,日志项在根据自身类型决定输出具体类容
-
模版字符串示例
"%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n";
-
解析模版获取对应日志项
void LogFormatter::init() { std::vector<std::tuple<std::string, std::string, int>> vec; std::string nstr; // ... 模版解析代码省略 static std::map<std::string, std::function<FormatItem::ptr(const std::string &str)>> s_format_items = { #define XX(tag, Format) \ { \ #tag, [](const std::string &fmt) { return std::make_shared<Format>(fmt); } \ } XX(m, MessageFormatItem), // m:消息 XX(p, LevelFormatItem), // p:日志级别 XX(c, LoggerNameFormatItem), // c:日志名称 XX(t, ThreadIdFormatItem), // t:线程id XX(n, NewLineFormatItem), // n:换行 XX(d, DateTimeFormatItem), // d:时间 XX(f, FileNameFormatItem), // f:文件名 XX(l, LineFormatItem), // l:行号 XX(T, TabFormatItem), // T:Tab XX(F, CoIdFormatItem), // F:协程id XX(N, ThreadNameFormatItem), // N:线程名称 #undef XX }; for (auto &i : vec) { if (std::get<2>(i) == 0) { FormatItem::ptr items = std::make_shared<StringFormatItem>(std::get<0>(i)); m_items.push_back(items); } else { auto it = s_format_items.find(std::get<0>(i)); if (it == s_format_items.end()) { continue; } else { m_items.push_back(it->second(std::get<1>(i))); } } } }
-
选取日志项输出
std::string LogFormatter::format(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) { std::stringstream ss; for (auto &i : m_items) i->format(logger, ss, level, event); return ss.str(); } std::ostream &LogFormatter::format(std::shared_ptr<Logger> logger, std::ostream &ofs, LogLevel::Level level, LogEvent::ptr event) { for (auto &i : m_items) i->format(logger, ofs, level, event); return ofs; }
LogAppender
日志输出器类,用于将日志输出到对应的位置。它实际上是一个虚基类,具有一个纯虚方法virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
,不同类型的日志输出器通过重载此方法,将日志输送到不同目的地
在tiger
里面实现了StdOutLogAppender
和FileLogAppender
日志输出器,分别将日志输送到控制台和文件
Logger
日志器,用于输出日志。这个类是直接与用户进行交互的类,提供了void log(LogLevel::Level level, const LogEvent::ptr event)
接口,用于传入日志级别和日志事件,将日志输出。日志器的实现包含了日子器名称、日志器级别、对应的日志输出器组等。在使用者调用log
接口时,日志器先判断是否大于等于自身级别。如果大于等于日志器将日志事件分别送往日志输出器组,这也是就实现了同一个日志事情可以在多个地方输出
void Logger::log(LogLevel::Level level, const LogEvent::ptr event) {
if (level < m_level) return;
SpinLock::Lock lock(m_lock);
auto self = shared_from_this();
for (auto &it : m_appenders)
it->log(self, level, event);
}
LogManager
日志器管理类,用于统一管理日志器,项目中所有日志器都应该由同一个日志管理器管理。因此使用者应该使用SingletonLoggerMgr
类,此类可以理解为LogManager
的单例实现。日志管理器提供添加、删除、获取日志器接口
宏和结构体
为了实现模块的灵活性,日志模块定义了对应的日志配置格式,可通过配置初始化日志器
log:
- name: SYSTEM
level: DEBUG
formater: "%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n"
appenders:
- type: FileLogAppender
level: INFO
file: ./system
interval: 7200
- type: StdOutLogAppender
level: DEBUG
- name: TEST
level: DEBUG
formater: "%d{%Y-%m-%d %H:%M:%S} %t %N %F [%p] [%c] %f:%l %m%n"
appenders:
- type: FileLogAppender
level: DEBUG
file: ./test
interval: 7200
- type: StdOutLogAppender
level: DEBUG
为了实现模块的便捷性,tiger
中为日志器定了宏,使日志输出变的非常轻松
TIGER_LOG_D(tiger::TEST_LOG) << "[test log]";
TIGER_LOG_D(tiger::SYSTEM_LOG) << "[test log]";