参照 log4j 先写一个日志系统
以下代码均在同一文件sylar/log.h
开头两行:
cpp
#ifndef __SYLAR_LOG_H__
#define __SYLAR_LOG_H__
#endif
#ifndef 是 "if not defined" 的缩写,它是一个预处理指令,去检查在当前的编译阶段,SYLAR_LOG_H 这个标识符是否还没有被定义过。如果没有被定义,那么后续在 #ifndef 和对应的 #endif 之间的代码将会被正常编译处理;反之,如果该标识符已经被定义了,那么这中间的代码将会被预处理器跳过,不会参与编译。
109 - 142 LogLevel
日志级别枚举
cpp
enum Level {
UNKNOW = 0, // 未知级别
DEBUG = 1, // 调试级别
INFO = 2, // 信息级别
WARN = 3, // 警告级别
ERROR = 4, // 错误级别
FATAL = 5 // 致命错误级别
};
ToString 方法
cpp
static const char* ToString(LogLevel::Level level);
具体实现:
cpp
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";
}
宏定义:
宏 XX(name)
接受一个参数 name,然后生成一个 switch 的 case 语句。
#name
是一个 预处理器字符串化操作,它会将宏参数 name 转换为字符串(例如:name = DEBUG 时,#name 会变为 "DEBUG")。
如果 level 不匹配任何一个已知的日志级别),switch 语句会进入 default 分支,返回字符串 "UNKNOW"
具体就是通过宏减少了代码量
FromString 方法
cpp
static LogLevel::Level FromString(const std::string& str);
具体实现:
cpp
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 类是日志系统中非常关键的一个部分,它提供了日志记录所需的所有元数据。
类的构造
cpp
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
:生成该日志事件的线程名称。
成员函数列表
cpp
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()
cpp
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
类的目的是对日志事件进行封装,方便在其他地方处理日志事件的相关内容。通过它,可以获取到日志事件对象和日志内容流。
成员变量
cpp
LogEvent::ptr m_event;
m_event 是 LogEvent::ptr 类型的成员变量,存储了实际的日志事件。通过这个成员,LogEventWrap 类可以持有并操作一个日志事件对象
类的构造和析构
cpp
LogEventWrap(LogEvent::ptr e);
~LogEventWrap();
cpp
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类会说
成员函数
cpp
LogEvent::ptr getEvent() const { return m_event; }
cpp
std::stringstream& LogEventWrap::getSS() {
return m_event->getSS();
}
288 - 366 LogFormatter 日志格式化
成员变量
cpp
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;
构造函数
cpp
传入字符串参数
LogFormatter::LogFormatter(const std::string& pattern)
:m_pattern(pattern) {
init();
}
init()函数
cpp
// 初始化函数,用于解析日志格式模板并生成相应的格式化项
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: 线程 ID
XX(n, NewLineFormatItem), // n: 换行符
XX(d, DateTimeFormatItem), // d: 时间
XX(f, FilenameFormatItem), // f: 文件名
XX(l, LineFormatItem), // l: 行号
XX(T, TabFormatItem), // T: 制表符
XX(F, FiberIdFormatItem), // F: 协程 ID
XX(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
cpp
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函数
cpp
virtual void format(std::ostream& os,
std::shared_ptr<Logger> logger, LogLevel::Level level,
LogEvent::ptr event) = 0;
内部类FormatItem
FormatItem内部类用于封装日志内容项格式化的相关逻辑,使得对每一项具体的格式化操作能够独立进行处理。不同的日志内容项(比如消息、日志级别、时间等)都有各自的格式化要求和实现方式,通过将它们抽象成一个个FormatItem对象,
cpp
virtual void format(std::ostream& os, std::shared_ptr
<Logger> logger, LogLevel::Level level,
LogEvent::ptr event) = 0;
提供用于继承的虚函数
428 - 541 Logger 日志器
成员变量
cpp
m_name用于存储日志器的名称,方便对不同日志器进行区分和标识。
m_level记录当前日志器的日志级别,决定了哪些级别的日志可以被该日志器记录。
m_mutex是前面定义的Spinlock类型的锁,用于在多线程环境下保护类中共享资源(如m_appenders、m_formatter等)的并发访问安全。
m_appenders是一个存储LogAppender智能指针的链表,用于管理该日志器关联的所有日志目标对象。
m_formatter是指向日志格式器的智能指针,负责控制日志的输出格式。
m_root是指向主日志器的智能指针,可能在日志系统的层次结构或者一些特殊的管理逻辑中起到关联、继承等相关作用,具体依赖于整个日志系统的设计。
构造函数
cpp
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方法实现
cpp
/**
* 记录日志事件
* @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继承它
提供给子类继承的方法
cpp
virtual std::string toYamlString() = 0;
virtual void log(std::shared_ptr<Logger> logger,
LogLevel::Level level, LogEvent::ptr event) = 0;
这两个是不同子类之间有区分,所以要虚函数
对于子类没区别的
cpp
/**
* @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
cpp
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进行了重写
默认构造
cpp
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 字符串
}
得到日志输出的字符串
cpp
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
cpp
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设置根日志器:
cpp
LoggerManager::LoggerManager() {
m_root.reset(new Logger);
m_root->addAppender(LogAppender::ptr(new StdoutLogAppender));
m_loggers[m_root->m_name] = m_root;
init();
}
在日志器管理器中寻找文件名是name的日志器
cpp
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;
}
返回根目录器
cpp
Logger::ptr getRoot() const { return m_root;}
cpp
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 宏定义
cpp
#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就把宏替换成
调用
cpp
LogEventWrap::LogEventWrap(LogEvent::ptr e)
:m_event(e) {
}
的构造函数,
该构造要传入,LogEvent的指针,该指针由
cpp
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需要调用构造函数
cpp
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) {
}
其中
cpp
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;}