一、日志模块整体设计
设想一下,凭借我们的经验,一般日志会有哪些内容呢?等级,具体定位信息(函数名、行号、文件名,线程名),时间。
OK,这确实就是日志输出的内容,但输出的内容仅呈现在前台,那么回到后台看,日志应该具备重定向和灵活配置功能,因此日志模块的设计应具备以下功能:
- 区分不同的日志级别,如 DEBUG/INFO/WARN/ERROR 等。 日志等级及控制输出
- 可以指定输出级别,灵活控制日志输出。
- 支持输出到不同的目标,如标准输出、文件、syslog、网络日志服务器等。
- 可以对日志进行分类和命名,方便判断来源。
- 日志格式可灵活配置,可以选择是否包含文件名/行号、时间戳、线程/协程号、日志级别等信息。
- 支持通过配置文件进行配置。
那么我们看下 sylar 关于日志处理是怎么设计的。
1.1、总体日志框图
-
整体的类依赖图
上图可以看到,以 Logger 类为中心,与之相关联的类分别有 LogAppender、LogEvent、LogFormatter...等,将类单独拎出来看注释如下:
-
详细的类定义 ^(使用了大量的智能指针)^
- LogFormatter:日志格式器,可用于格式化日志事件,通过 format 方法用于将日志事件格式化成字符串;
- LogAppender:日志输出目标,用于将日志事件输出到对应的输出地,该类包含 LogFormatter 格式化方法和 log 方法,可通过继承该类实现到不同的输出地输出;
- Logger:日志器,负责日志输出,提供 log 方法,若该日志需要输出则调用 LogAppender 将日志输出,否则抛弃;
- LogEvent:日志时间,用于记录日志信息,比如该日志的级别,文件名 / 行号,日志消息,线程 / 协程号,所属日志器名称等。;
- LogEventWrap:日志事件包装器,将日志事件与日志器包装在一起,通过宏定义简化日志模块的使用,当期析构时候,则调用日志器的 log 方法进行输出;
- LogManager:日志器管理类,单例模式,用于统一管理所有的日志器,提供日志器的创建与获取方法,内部维护一个名称到日志器的 map,当获取的日志器存在时,直接返回对应的日志器指针,否则创建对应的日志器并返回。
class Logger : public std::enable_shared_from_this<Logger> {};
// 日志级别
class LogLevel {};
// 日志格式化器
class LogFormatter {};
// 日志输出目标
class LogAppender {};
// 输出到控制台的日志输出目标
class StdoutLogAppender : public LogAppender {};
// 输出到文件的日志输出目标
class FileLogAppender : public LogAppender {};
// 日志事件
class LogEvent {};
// 日志事件包装器
class LogEventWrap {};
// 日志器管理类
class LogManager {};
二、代码流程
2.1、从怎么用去反推着看
作者在 ReadMe 中指引可以看到:
支持流式日志风格写日志和格式化风格写日志,支持日志格式自定义,日志级别,多日志分离等等功能
流式日志使用:SYLAR_LOG_INFO(g_logger) << "this is a log";
格式化日志使用:SYLAR_LOG_FMT_INFO(g_logger, "%s", "this is a log");
支持时间,线程 id,线程名称,日志级别,日志名称,文件名,行号等内容的自由配置
从两个宏可以看出通过日志事件包装器结合日志事件,将日志按约定格式打包进数据流,最终通过 getSS()
获取字符串流。那么对 LogEvent 和 LogEventWrap 进行拆解。
/**
* @brief 使用流式方式将日志级别level的日志写入到logger
*/
#define SYLAR_LOG_LEVEL(logger, level) \
if(logger->getLevel() <= level) \
sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
__FILE__, __LINE__, 0, sylar::GetThreadId(),\
sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()
/**
* @brief 使用格式化方式将日志级别level的日志写入到logger
*/
#define SYLAR_LOG_FMT_LEVEL(logger, level, fmt, ...) \
if(logger->getLevel() <= level) \
sylar::LogEventWrap(sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \
__FILE__, __LINE__, 0, sylar::GetThreadId(),\
sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getEvent()->format(fmt, __VA_ARGS__)
2.2、LogEvent 和 LogEventWrap
2.2.1、LogEvent
UML 显示如下:
该类主要是用于记录日志中的一些描述信息,如线程 ID、行列号、文件名、日志等级等,其中具体把 content 进行格式化的函数是 format
2.2.2、LogEventWrap 日志附加信息装饰器
Wrap 主要的作用是通过构造将 m_event 进行转换(获取)并通过 getSS 来获取日志内容流
2.2.3、LogLevel 等级有哪些?
其中 ToString 还使用了些技巧。把 Switch Case 干掉1
那么从以上三个类结合调用接口,仅仅可以看出是怎么获取日志内容流的,但似乎并没有看到是怎么组成内容流的?这里又有一个技巧,在 LogEventWrap 析构时,调用 Logger 中的 log 接口,把日志内容流塞回 Logger。
LogEventWrap::~LogEventWrap() {
m_event->getLogger()->log(m_event->getLevel(), m_event);
}
2.2.4、Logger
LogEvent、LogLevel、LogAppender、LogFormatter 和 Logger 之间的 UML 类图关系如下:
从应用输出日志看,输出出口就定在了 Logger::log
这个函数中:
void Logger::log(LogLevel::Level level, LogEvent::ptr event) {
if (level >= m_level) {
auto self = shared_from_this();
MutexType::Lock lock(m_mutex);
if (!m_appenders.empty()) {
for (auto& i : m_appenders) {
i->log(self, level, event);
}
} else if (m_root) {
m_root->log(level, event);
}
}
}
从上面代码中 appenders 可以看出,日志流的输出可以时多样的。
2.2.5、LogAppender
三、模块应用实例
支持流式日志风格写日志和格式化风格写日志,支持日志格式自定义,日志级别,多日志分离等等功能
流式日志使用:SYLAR_LOG_INFO(g_logger) << "this is a log";
格式化日志使用:SYLAR_LOG_FMT_INFO(g_logger, "%s", "this is a log");
支持时间,线程 id,线程名称,日志级别,日志名称,文件名,行号等内容的自由配置
四、移植使用
把 Switch Case 干掉
宏定义的方法
const char* LogLevel::ToString(LogLevel::Level level) { switch(level) { #define XX(name) \ case LogLevel::name: \ return #name; \ break; XX(DEBUG); XX(INFO); XX(WARN); XX(ERROR); XX(FATAL); #undef XX default: return "UNKNOW"; } return "UNKNOW"; }
↩
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于