参照 log4j 先写一个日志系统
以下代码均在同一文件sylar/log.h
开头两行:
#ifndef __SYLAR_LOG_H__
#define __SYLAR_LOG_H__#endif
#ifndef 是 “if not defined” 的缩写,它是一个预处理指令,去检查在当前的编译阶段,SYLAR_LOG_H 这个标识符是否还没有被定义过。如果没有被定义,那么后续在 #ifndef 和对应的 #endif 之间的代码将会被正常编译处理;反之,如果该标识符已经被定义了,那么这中间的代码将会被预处理器跳过,不会参与编译。
109 - 142 LogLevel
日志级别枚举
enum Level {UNKNOW = 0, // 未知级别DEBUG = 1, // 调试级别INFO = 2, // 信息级别WARN = 3, // 警告级别ERROR = 4, // 错误级别FATAL = 5 // 致命错误级别
};
ToString 方法
static const char* ToString(LogLevel::Level level);
具体实现:
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 XXdefault:return "UNKNOW";}return "UNKNOW";
}
宏定义:
宏 XX(name)
接受一个参数 name,然后生成一个 switch 的 case 语句。
#name
是一个 预处理器字符串化操作,它会将宏参数 name 转换为字符串(例如:name = DEBUG 时,#name 会变为 “DEBUG”)。
如果 level 不匹配任何一个已知的日志级别),switch 语句会进入 default 分支,返回字符串 “UNKNOW”
具体就是通过宏减少了代码量
FromString 方法
static LogLevel::Level FromString(const std::string& str);
具体实现:
LogLevel::Level LogLevel::FromString(const std::string& str) {
#define XX(level, v) \if(str == #v) { \return LogLevel::level; \}XX(DEBUG, debug);XX(INFO, info);XX(WARN, warn);XX(ERROR, error);XX(FATAL, fatal);XX(DEBUG, DEBUG);XX(INFO, INFO);XX(WARN, WARN);XX(ERROR, ERROR);XX(FATAL, FATAL);return LogLevel::UNKNOW;
#undef XX
}
通过使用宏,代码实现变得非常简洁。每一个日志级别的匹配判断都由宏来生成,这样就避免了手动编写多个重复的 if 判断语句。代码的可维护性和可扩展性也得到了提升。
如果需要添加更多的日志级别,只需要在 XX 宏调用处添加新的日志级别,而不需要修改整个函数的结构。
支持了字符串(小写和大写)到日志级别枚举值的转换。
宏和函数定义总结
宏的基本特点
宏是由 预处理器 在编译之前展开的,它通过文本替换来处理代码。宏通常使用 #define 来定义。
优点:
- 性能:宏在编译前直接替换代码,因此 没有函数调用的开销。这在某些性能关键的代码中非常有用,比如需要大量重复计算的常量表达式。
- 灵活性:宏可以接受任意复杂的参数,并通过字符串化 (#) 或拼接 (##) 来动态生成代码。
缺点:
- 调试困难:宏没有类型检查,它们在预处理阶段展开,调试时你无法看到宏展开后的结果。如果宏中存在错误,编译器可能会提示不明确的错误信息。
可读性差:宏的展开过程是自动进行的,可能导致代码的可读性和可维护性差,尤其是复杂的宏定义。 - 无法进行类型检查:宏没有类型信息,它们只是简单的文本替换,因此很容易出现错误(例如,传递了错误类型的参数)。
- 作用域问题:宏是全局的,没有作用域限制,可能会无意中覆盖现有变量或导致名字冲突。
147 - 252 LogEvent
主要用于表示一次日志事件(日志条目)。每个日志事件包含了丰富的上下文信息,如日志的来源文件、行号、线程信息、日志级别、日志内容等。LogEvent 类是日志系统中非常关键的一个部分,它提供了日志记录所需的所有元数据。
类的构造
LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level,const char* file, int32_t line, uint32_t elapse,uint32_t thread_id, uint32_t fiber_id, uint64_t time,const std::string& thread_name);
-
std::shared_ptr<Logger> logger
:指向日志器对象的智能指针,表示记录该日志事件的日志器。一个日志事件必须通过某个日志器来输出。 -
LogLevel::Level level
:日志级别,表示此次日志事件的严重程度。 -
const char* file
:触发日志事件的源文件的文件名。 -
int32_t line
:触发日志事件的源文件中的行号。 -
uint32_t elapse
:程序启动到当前日志事件发生时的时间差(以毫秒为单位)。这可以帮助追踪程序运行的时间。 -
uint32_t thread_id
:生成该日志事件的线程 ID。 -
uint32_t fiber_id
:生成该日志事件的协程 ID。这个值适用于多协程的程序,有助于区分是哪个协程产生的日志。 -
uint64_t time
:日志事件生成的时间戳(通常以秒为单位)。 -
const std::string& thread_name
:生成该日志事件的线程名称。
成员函数列表
getFile():返回触发日志事件的源文件的文件名。
getLine():返回触发日志事件的源文件中的行号。
getElapse():返回程序启动后到日志事件发生的毫秒数。
getThreadId():返回生成该日志事件的线程 ID。
getFiberId():返回生成该日志事件的协程 ID。
getTime():返回该日志事件的时间戳(秒)。
getThreadName():返回生成该日志事件的线程名称。
getContent():返回日志内容,即 std::stringstream 中的字符串内容。这是日志的主要信息部分。
getLogger():返回指向日志器对象的智能指针,表示生成该日志事件的日志器。
getLevel():返回日志事件的日志级别。
getSS():返回 std::stringstream 对象的引用,允许将日志内容写入该流中。
11个成员函数,对应10个成员变量,其中m_ss内容流对应两个成员函数:
分别返回m_ss和m_ss.str()
std::string getContent() const { return m_ss.str();}std::stringstream& getSS() { return m_ss;}
格式化函数
format(const char* fmt, ...)
这是一个变参函数,用于将格式化后的字符串内容写入到 m_ss 中。它使用 printf 风格的格式化字符串。
format(const char* fmt, va_list al)
:这是一个接收 va_list 的版本,用于处理 format 函数中的可变参数列表,支持更灵活的格式化输出。
使用场景
LogEvent 类通常由 Logger 类在日志记录时生成,并提供给 Appender 类用于输出。它通过包含丰富的上下文信息(如文件、行号、线程、协程等),使得开发者可以在分析日志时,快速定位问题。
257 - 285LogEventWrap 日志事件包装器
LogEventWrap
类的目的是对日志事件进行封装,方便在其他地方处理日志事件的相关内容。通过它,可以获取到日志事件对象和日志内容流。
成员变量
LogEvent::ptr m_event;
m_event 是 LogEvent::ptr 类型的成员变量,存储了实际的日志事件。通过这个成员,LogEventWrap 类可以持有并操作一个日志事件对象
类的构造和析构
LogEventWrap(LogEvent::ptr e);
~LogEventWrap();
LogEventWrap::LogEventWrap(LogEvent::ptr e):m_event(e) {
}LogEventWrap::~LogEventWrap() {m_event->getLogger()->log(m_event->getLevel(), m_event);
}
析构里:调用getLogger()返回了一个share_ptr < Logger > 日志器
再使用他的log方法传入logLevel,和logevent
具体的Logger类会说
成员函数
LogEvent::ptr getEvent() const { return m_event; }
std::stringstream& LogEventWrap::getSS() {return m_event->getSS();
}
288 - 366 LogFormatter 日志格式化
成员变量
std::string m_pattern
保存日志格式模板(例如:
%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n)。std::vector<FormatItem::ptr> m_items:
保存解析后的格式化项对象,每个对象对应模板中的一个占位符
(如 %d、%p 等)。bool m_error:
标识日志格式是否出现错误。
如果 m_error 为 true,表示格式解析过程中发生了错误。typedef std::shared_ptr<LogFormatter> ptr;
构造函数
传入字符串参数LogFormatter::LogFormatter(const std::string& pattern):m_pattern(pattern) {init();
}
init()函数
// 初始化函数,用于解析日志格式模板并生成相应的格式化项
void LogFormatter::init() {// 用于存储格式化项的临时数据(包括字符串部分、格式化部分和标记)std::vector<std::tuple<std::string, std::string, int>> vec;std::string nstr; // 临时存储普通字符串部分// 遍历日志格式模板for (size_t i = 0; i < m_pattern.size(); ++i) {// 如果当前字符不是 '%',说明它是普通字符,直接加入到 nstr 中if (m_pattern[i] != '%') {nstr.append(1, m_pattern[i]);continue;}// 如果遇到连续的 '%%',将 '%' 添加到 nstr 中,跳过下一个字符if ((i + 1) < m_pattern.size()) {if (m_pattern[i + 1] == '%') {nstr.append(1, '%');continue;}}// 解析格式化项size_t n = i + 1; // 从 '%' 后的下一个字符开始解析int fmt_status = 0; // 0: 解析格式标识符, 1: 解析格式内容size_t fmt_begin = 0; // 格式内容的起始位置std::string str; // 存储格式标识符std::string fmt; // 存储格式内容// 遍历模板中的每个字符,直到格式项解析完毕while (n < m_pattern.size()) {// 如果当前字符既不是字母也不是 '{' 或 '}',说明已解析完格式标识符if (!fmt_status && (!isalpha(m_pattern[n]) && m_pattern[n] != '{' && m_pattern[n] != '}')) {str = m_pattern.substr(i + 1, n - i - 1); // 提取格式标识符break;}if (fmt_status == 0) {// 如果是 '{',说明是带格式内容的格式化项,进入格式化内容解析状态if (m_pattern[n] == '{') {str = m_pattern.substr(i + 1, n - i - 1); // 提取格式标识符fmt_status = 1; // 进入格式化内容解析fmt_begin = n; // 标记格式开始位置++n; // 跳过 '{'continue;}} else if (fmt_status == 1) {// 如果是 '}',则结束格式内容的解析if (m_pattern[n] == '}') {fmt = m_pattern.substr(fmt_begin + 1, n - fmt_begin - 1); // 提取格式内容fmt_status = 0; // 格式内容解析结束++n; // 跳过 '}'break;}}++n; // 移动到下一个字符if (n == m_pattern.size()) {// 如果解析到字符串结尾,且没有找到 '}', 说明格式项不完整if (str.empty()) {str = m_pattern.substr(i + 1); // 提取剩余部分}}}// 如果格式项解析完成,将其存储到 vec 中if (fmt_status == 0) {if (!nstr.empty()) {vec.push_back(std::make_tuple(nstr, std::string(), 0)); // 存储普通字符串部分nstr.clear();}vec.push_back(std::make_tuple(str, fmt, 1)); // 存储格式化项i = n - 1; // 更新 i 为格式项解析结束的位置} else if (fmt_status == 1) {// 如果格式项解析失败(例如没有找到对应的 '}'),记录错误std::cout << "pattern parse error: " << m_pattern << " - " << m_pattern.substr(i) << std::endl;m_error = true;vec.push_back(std::make_tuple("<<pattern_error>>", fmt, 0)); // 存储错误项}}// 如果 nstr 中还有剩余的普通字符串,添加到 vec 中if (!nstr.empty()) {vec.push_back(std::make_tuple(nstr, "", 0));}// 定义一个静态映射表,将格式标识符(如 'm'、'p' 等)映射到相应的 FormatItem 类型static std::map<std::string, std::function<FormatItem::ptr(const std::string& str)>> s_format_items = {
#define XX(str, C) \{#str, [](const std::string& fmt) { return FormatItem::ptr(new C(fmt));}}XX(m, MessageFormatItem), // m: 消息XX(p, LevelFormatItem), // p: 日志级别XX(r, ElapseFormatItem), // r: 累计毫秒数XX(c, NameFormatItem), // c: 日志名称XX(t, ThreadIdFormatItem), // t: 线程 IDXX(n, NewLineFormatItem), // n: 换行符XX(d, DateTimeFormatItem), // d: 时间XX(f, FilenameFormatItem), // f: 文件名XX(l, LineFormatItem), // l: 行号XX(T, TabFormatItem), // T: 制表符XX(F, FiberIdFormatItem), // F: 协程 IDXX(N, ThreadNameFormatItem), // N: 线程名称
#undef XX};// 遍历 vec,根据格式标识符创建相应的 FormatItem 对象,并加入 m_items 容器for (auto& i : vec) {if (std::get<2>(i) == 0) {// 如果是普通字符串部分,创建 StringFormatItem 对象m_items.push_back(FormatItem::ptr(new StringFormatItem(std::get<0>(i))));} else {// 如果是格式化项,查找对应的 FormatItem 类型auto it = s_format_items.find(std::get<0>(i));if (it == s_format_items.end()) {// 如果没有找到对应的格式化项类型,记录错误m_items.push_back(FormatItem::ptr(new StringFormatItem("<<error_format %" + std::get<0>(i) + ">>")));m_error = true;} else {// 创建对应的 FormatItem 对象并传入格式内容m_items.push_back(it->second(std::get<1>(i)));}}}
}
成员函数format
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(ss, logger, level, event);}return ss.str();
}std::ostream& LogFormatter::format(std::ostream& ofs, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {for(auto& i : m_items) {i->format(ofs, logger, level, event);}return ofs;
}
这两个 format 函数分别用于将日志事件格式化为字符串或输出流(std::ostream)格式。它们的核心逻辑非常相似,主要依赖于 m_items 容器中的格式化项(FormatItem)来逐步构建最终的日志信息。
返回string类型的是给后面StdoutLogAppender用
返回ostream类型的是给FileLogAppender用
i是m_items里的,是嵌套类FormatItem对象
他有format函数
virtual void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
内部类FormatItem
FormatItem内部类用于封装日志内容项格式化的相关逻辑,使得对每一项具体的格式化操作能够独立进行处理。不同的日志内容项(比如消息、日志级别、时间等)都有各自的格式化要求和实现方式,通过将它们抽象成一个个FormatItem对象,
virtual void format(std::ostream& os, std::shared_ptr<Logger> logger, LogLevel::Level level,LogEvent::ptr event) = 0;
提供用于继承的虚函数
428 - 541 Logger 日志器
成员变量
m_name用于存储日志器的名称,方便对不同日志器进行区分和标识。
m_level记录当前日志器的日志级别,决定了哪些级别的日志可以被该日志器记录。
m_mutex是前面定义的Spinlock类型的锁,用于在多线程环境下保护类中共享资源(如m_appenders、m_formatter等)的并发访问安全。
m_appenders是一个存储LogAppender智能指针的链表,用于管理该日志器关联的所有日志目标对象。
m_formatter是指向日志格式器的智能指针,负责控制日志的输出格式。
m_root是指向主日志器的智能指针,可能在日志系统的层次结构或者一些特殊的管理逻辑中起到关联、继承等相关作用,具体依赖于整个日志系统的设计。
构造函数
Logger::Logger(const std::string& name):m_name(name),m_level(LogLevel::DEBUG) {m_formatter.reset(new LogFormatter("%d{%Y-%m-%d %H:%M:%S}%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n"));
}
log方法实现
/*** 记录日志事件* @param level 日志级别* @param event 日志事件*/
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) {// 调用每个目标的 log 方法,记录日志事件i->log(self, level, event);}// 如果没有日志输出目标,但存在根日志器} else if(m_root) {// 调用根日志器的 log 方法,记录日志事件m_root->log(level, event);}}
}
371 - 423 LogAppender 日志输出目标
定义了一个 LogAppender 类,是一个基类
子类StdoutLogAppender和FileLogAppender继承它
提供给子类继承的方法
virtual std::string toYamlString() = 0;virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
这两个是不同子类之间有区分,所以要虚函数
对于子类没区别的
/*** @brief 更改日志格式器*/void setFormatter(LogFormatter::ptr val);/*** @brief 获取日志格式器*/LogFormatter::ptr getFormatter();/*** @brief 获取日志级别*/LogLevel::Level getLevel() const { return m_level;}/*** @brief 设置日志级别*/void setLevel(LogLevel::Level val) { m_level = val;}
其中,后两个setLevel和getLevel 仅仅是对成员变量 m_level 进行读取或修改,因此它们可以直接在类的定义中实现。
而前面两个需要对m_formatter 和 m_hasFormatter 成员的访问是线程安全的。
546 - 551 StdoutLogAppender 输出到控制台的Appender
class StdoutLogAppender : public LogAppender {
public:typedef std::shared_ptr<StdoutLogAppender> ptr;void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;std::string toYamlString() override;
};
对log和toYamlString进行了重写
默认构造
std::string StdoutLogAppender::toYamlString() {MutexType::Lock lock(m_mutex); // 1. 锁定互斥量,保证线程安全YAML::Node node; // 2. 创建一个 YAML 节点对象node["type"] = "StdoutLogAppender"; // 3. 设置输出目标的类型// 4. 如果日志级别不是 UNKNOW,加入 level 字段if (m_level != LogLevel::UNKNOW) {node["level"] = LogLevel::ToString(m_level); // 将日志级别转换为字符串并设置}// 5. 如果存在格式器并且格式器有效,加入 formatter 字段if (m_hasFormatter && m_formatter) {node["formatter"] = m_formatter->getPattern(); // 获取并设置格式器的模式(模板)}// 6. 将 YAML 节点对象输出到字符串流中std::stringstream ss;ss << node; // 将 YAML 节点序列化为字符串流内容return ss.str(); // 7. 返回 YAML 字符串
}
得到日志输出的字符串
void StdoutLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {if(level >= m_level) { // 1. 检查日志级别MutexType::Lock lock(m_mutex); // 2. 锁定互斥量,确保线程安全m_formatter->format(std::cout, logger, level, event); // 3. 格式化日志并输出到控制台}
}其中format是这样的:
std::ostream& LogFormatter::format(std::ostream& ofs,
std::shared_ptr<Logger> logger, LogLevel::Level level,
LogEvent::ptr event) {for(auto& i : m_items) {i->format(ofs, logger, level, event);}return ofs;
}
556 - 575 FileLogAppender输出到文件的Appender
lass FileLogAppender : public LogAppender {
public:typedef std::shared_ptr<FileLogAppender> ptr;FileLogAppender(const std::string& filename);void log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) override;std::string toYamlString() override;/*** @brief 重新打开日志文件* @return 成功返回true*/bool reopen();
private:/// 文件路径std::string m_filename;/// 文件流std::ofstream m_filestream;/// 上次重新打开时间uint64_t m_lastTime = 0;
};
由于是文件,添加了文件打开时间操作
也有构造函数,因为要传入文件名
580 - 615 LoggerManager 日志器管理类
给logger设置根日志器:
LoggerManager::LoggerManager() {m_root.reset(new Logger);m_root->addAppender(LogAppender::ptr(new StdoutLogAppender));m_loggers[m_root->m_name] = m_root;init();
}
在日志器管理器中寻找文件名是name的日志器
Logger::ptr LoggerManager::getLogger(const std::string& name) {MutexType::Lock lock(m_mutex);auto it = m_loggers.find(name);if(it != m_loggers.end()) {return it->second;}Logger::ptr logger(new Logger(name));logger->m_root = m_root;m_loggers[name] = logger;return logger;
}
返回根目录器
Logger::ptr getRoot() const { return m_root;}
std::string LoggerManager::toYamlString() {MutexType::Lock lock(m_mutex);YAML::Node node;for(auto& i : m_loggers) {node.push_back(YAML::Load(i.second->toYamlString()));}std::stringstream ss;ss << node;return ss.str();
}
这个函数的目的是将 LoggerManager 管理的所有日志器的配置信息以 YAML 格式输出,以便于查看和管理
25-100 宏定义
#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()
定义宏SYLAR_LOG_LEVEL
如果logger->getLevel() <= level就把宏替换成
调用
LogEventWrap::LogEventWrap(LogEvent::ptr e):m_event(e) {
}
的构造函数,
该构造要传入,LogEvent的指针,该指针由
sylar::LogEvent::ptr(new sylar::LogEvent(logger, level, \__FILE__, __LINE__, 0, sylar::GetThreadId(),\sylar::GetFiberId(), time(0), sylar::Thread::GetName()))).getSS()
new 出来,赋值给一个std::shared_ptr< LogEvent >
而new需要调用构造函数
LogEvent::LogEvent(std::shared_ptr<Logger> logger, LogLevel::Level level,const char* file, int32_t line, uint32_t elapse,uint32_t thread_id, uint32_t fiber_id, uint64_t time,const std::string& thread_name):m_file(file),m_line(line),m_elapse(elapse),m_threadId(thread_id),m_fiberId(fiber_id),m_time(time),m_threadName(thread_name),m_logger(logger),m_level(level) {
}
其中
pid_t GetThreadId() {return syscall(SYS_gettid);
}uint32_t GetFiberId() {return sylar::Fiber::GetFiberId();
}const std::string& Thread::GetName() {return t_thread_name;
}std::stringstream& LogEventWrap::getSS() {return m_event->getSS();
}// m_event->getSS()std::stringstream& getSS() { return m_ss;}