日志模块
日志模块
1️⃣ 日志的作用
- 在服务器的运行过程中,日志可以记录服务器运行过程中的各种事件,所以日志是非常有作用的。
- 记录服务器的启动和关闭
- 记录客户端连接/断开
- 记录HTTP 请求的接收与响应
- 记录协程调度执行情况
- 记录异常或错误信息(如断网、文件打不开等)
- 当运行过程中当出现异常行为(比如响应延迟、崩溃、死锁等)时,开发者可以通过日志快速定位问题所在:
- 快速定位问题发生的代码逻辑
- 分析错误发生的上下文(线程 ID、时间戳、调用路径等)
- 重现问题流程
- 在
sylar服务器框架中,使用了类似Log4cpp的结构,支持多级日志输出,支持 流式日志风格 写日志和 格式化风格 写日志,支持日志格式自定义,日志级别,多日志分离等等功能
2️⃣ 日志级别
- 整体框架:
sylar框架中主要使用logLevel表示日志级别1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32class LogLevel {
public:
/**
* @brief 日志级别枚举
*/
enum Level {
/// 未知级别
UNKNOW = 0,
/// DEBUG 级别
DEBUG = 1,
/// INFO 级别
INFO = 2,
/// WARN 级别
WARN = 3,
/// ERROR 级别
ERROR = 4,
/// FATAL 级别
FATAL = 5
};
/**
* @brief 将日志级别转成文本输出
* @param[in] level 日志级别
*/
static const char* ToString(LogLevel::Level level);
/**
* @brief 将文本转换成日志级别
* @param[in] str 日志级别文本
*/
static LogLevel::Level FromString(const std::string& str);
};
- 转换函数
- 为了方便在代码里使用,
sylar在logLevel中提供了两个方法,用于将Level转换成字符串,或者将字符串转换成Level。
- 为了方便在代码里使用,
3️⃣ 日志事件
- 整体框架
sylar中主要使用LogEvent表示一个日志事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106class LogEvent {
public:
typedef std::shared_ptr<LogEvent> ptr;
/**
* @brief 构造函数
* @param[in] logger 日志器
* @param[in] level 日志级别
* @param[in] file 文件名
* @param[in] line 文件行号
* @param[in] elapse 程序启动依赖的耗时(毫秒)
* @param[in] thread_id 线程id
* @param[in] fiber_id 协程id
* @param[in] time 日志事件(秒)
* @param[in] thread_name 线程名称
*/
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);
/**
* @brief 返回文件名
*/
const char* getFile() const { return m_file;}
/**
* @brief 返回行号
*/
int32_t getLine() const { return m_line;}
/**
* @brief 返回耗时
*/
uint32_t getElapse() const { return m_elapse;}
/**
* @brief 返回线程ID
*/
uint32_t getThreadId() const { return m_threadId;}
/**
* @brief 返回协程ID
*/
uint32_t getFiberId() const { return m_fiberId;}
/**
* @brief 返回时间
*/
uint64_t getTime() const { return m_time;}
/**
* @brief 返回线程名称
*/
const std::string& getThreadName() const { return m_threadName;}
/**
* @brief 返回日志内容
*/
std::string getContent() const { return m_ss.str();}
/**
* @brief 返回日志器
*/
std::shared_ptr<Logger> getLogger() const { return m_logger;}
/**
* @brief 返回日志级别
*/
LogLevel::Level getLevel() const { return m_level;}
/**
* @brief 返回日志内容字符串流
*/
std::stringstream& getSS() { return m_ss;}
/**
* @brief 格式化写入日志内容
*/
void format(const char* fmt, ...);
/**
* @brief 格式化写入日志内容
*/
void format(const char* fmt, va_list al);
private:
/// 文件名
const char* m_file = nullptr;
/// 行号
int32_t m_line = 0;
/// 程序启动开始到现在的毫秒数
uint32_t m_elapse = 0;
/// 线程ID
uint32_t m_threadId = 0;
/// 协程ID
uint32_t m_fiberId = 0;
/// 时间戳
uint64_t m_time = 0;
/// 线程名称
std::string m_threadName;
/// 日志内容流
std::stringstream m_ss;
/// 日志器
std::shared_ptr<Logger> m_logger;
/// 日志等级
LogLevel::Level m_level;
};
- 重要函数:
LogEvent中使用get方法用来提供对外的接口,返回对应的私有成员,例如:1
LogLevel::Level getLevel() const { return m_level; }
LogEvent使用format方法来写入日志内容- 作用: 把
printf风格的可变参数格式化成字符串,再写入LogEvent内部的字符串流m_ss中。 - 结构:
- 第一个函数面向 外部调用,负责接收普通的 可变参数。
- 第二个函数面向 内部复用,负责接收已经整理好的
va_list,并真正完成格式化工作
- 解析:
- 第一层接口:
va_list al:专门指向函数参数的可变参数;va_start(al, fmt):初始化al,让它指向可变参数的起始位置;format(fmt, al):将准备好的参数列表al传给另一个重载函数处理;va_end(al);:结束对可变参数列表的使用,做清理工作。
- 第二层接口:
vasprintf(&buf, fmt, al):根据格式串fmt和参数列表al,动态分配一块足够大的内存,把格式化后的字符串写进去,并把地址赋给buf;m_ss << std::string(buf, len):将格式化的日志写入到 整体日志。
- 第一层接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15void LogEvent::format(const char* fmt, ...) {
va_list al;
va_start(al, fmt);
format(fmt, al);
va_end(al);
}
void LogEvent::format(const char* fmt, va_list al) {
char* buf = nullptr;
int len = vasprintf(&buf, fmt, al);
if(len != -1) {
m_ss << std::string(buf, len);
free(buf);
}
}- 作用: 把
4️⃣ 日志格式化器
LogFormatter作用- 在
sylar框架中,LogFormatter是日志系统的 格式化核心 模块。它负责将 日志事件(LogEvent)格式化为 字符串,使其能够以我们设定的格式输出到日志文件、控制台或其他日志输出目标。
- 在
为什么需要
LogFormatter- 直接输出
LogEvent结构体是不直观的,比如INFO 2025-06-11 13:45:12 thread1 [main] - something happened
LogFormatter就是用来定义和控制这个 “格式模板” 的,比如你希望日志长这样:2025-06-11 13:37:42 [0ms] 8127 Scheduler_0 99955 [INFO] [system] /home/gch/sylar/src/middleware/middleware.cpp:18- 这就是为什么需要使用
LogFormatter来格式化。
- 直接输出
默认格式
- 默认格式是
%%d{%%Y-%%m-%%d %%H:%%M:%%S}%%T%%t%%T%%N%%T%%F%%T[%%p]%%T[%%c]%%T%%f:%%l%%T%%m%%n - 默认格式描述:
年-月-日 时:分:秒 [累计运行毫秒数] \\t 线程id \\t 线程名称 \\t 协程id \\t [日志级别] \\t [日志器名称] \\t 文件名:行号 \\t 日志消息 换行符
- 默认格式是
整体框架
sylar框架中主要使用LogFormatter表示一个日志格式化器。

解析
- 使用 日志格式化器 时,其会调用函数
init(),解析给定日志格式模板。 - 之后将调用函数
format用来格式化日志,这就会遍历 日志格式化器 的成员m_items。 - 由于
m_items存放的是FormateItem::ptr,而FormateItem::ptr是基类指针,所以可以用基类指针来动态调用虚函数,执行的都是派生类对象的方法,所以派生类对象都会执行自己的format方法。这就可以将每条LogEvent的 不同部分 交给对应的FormateItem::ptr来进行格式化输出。
- 使用 日志格式化器 时,其会调用函数
主要成员
1
2
3std::string m_pattern; // 日志模板格式
std::vector<FormateItem::ptr> m_items; // 用于存放不同的日志处理类
bool m_error = false; // 用于判断日志模板格式化的时候是否出错重要函数
init()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160void LogFormatter::init()
{
/**
* @brief
* 简单的状态机判断,提取pattern中的常规字符和模式字符
* 解析的过程就是从头到尾遍历,根据状态标志决定当前字符是常规字符还是模式字符
* 一共有两种状态,即正在解析常规字符和正在解析模板转义字符
*/
// 按顺序存储从m_pattern解析到的patterns
std::vector<std::pair<int,std::string>> patterns;
// 判断解析过程是否出错
bool error = false;
// 临时存储常规字符串
std::string temp;
// 存储日期格式的字符串%d{}
std::string date;
// 判断是否在解析常规字符串
bool parse_normal = true;
// 从m_pattern的起始字符串开始
size_t i = 0;
while (i < m_pattern.size())
{
std::string c = std::string(1, m_pattern[i]);
// 首先判断是否是%
if (c == "%")
{
if (parse_normal) // 这表示前面一个字符不是%,仍然是普通字符
{
patterns.push_back(std::make_pair(0, temp));
temp.clear();
parse_normal = false;
++i;
continue;
}
else
{
// 这说明%是转义字符,直接将其添加到patterns
patterns.push_back(std::make_pair(1,"%"));
parse_normal = true;
++i;
continue;
}
}
else // 这表示当前字符不是%
{
if (parse_normal) // 这表示前面一个字符不是%,仍然是普通字符
{
temp += c;
++i;
continue;
}
else
{
// 这表示是模板字符
patterns.push_back(std::make_pair(1,c));
parse_normal = true;
if (c != "d") // 这说明不是日期格式
{
++i;
continue;
}
else
{
// 这表明是日期格式
++i;
if (i < m_pattern.size() && m_pattern[i] != '{')
{
// 说明格式错误
continue;
}
else
{
while (i < m_pattern.size() && m_pattern[i] != '}')
{
date.push_back(m_pattern[i]);
++i;
}
}
if (m_pattern[i] != '}')
{
// %d后面的大括号没有闭合,直接报错
std::cout << "[ERROR] LogFormatter::init() " << "pattern: [" << m_pattern << "] '{' not closed" << std::endl;
error = true;
break;
}
++i;
continue;
}
}
}
}
// 判断是否出错
if (error)
{
m_error = error;
return;
}
// 循环结束完,需要将剩余的字符串也添加到patterns中
if (!temp.empty())
{
patterns.push_back(std::make_pair(0,temp));
temp.clear();
}
// 定义具体的日志处理类,根据日志模板来调用对应的日志处理函数
static std::map<std::string, std::function<FormateItem::ptr(const std::string&)>> s_format_item =
{
{"m", CreateFormateItem<MessageFormatItem>},
{"p", CreateFormateItem<LevelFormatItem>},
{"c", CreateFormateItem<LoggerNameFormatItem>},
{"r", CreateFormateItem<ElapseFormatItem>},
{"f", CreateFormateItem<FileNameFormatItem>},
{"l", CreateFormateItem<LineFormatItem>},
{"t", CreateFormateItem<ThreadIdFormatItem>},
{"F", CreateFormateItem<FiberIdFormatItem>},
{"N", CreateFormateItem<ThreadNameFormatItem>},
{"%", CreateFormateItem<PercentSignFormatItem>},
{"T", CreateFormateItem<TabFormatItem>},
{"n", CreateFormateItem<NewLineFormatItem>}
};
for (auto iterator : patterns)
{
if (iterator.first == 0)
{
// 这是在处理普通的常规字符
m_items.push_back(FormateItem::ptr(new StringFormatItem(iterator.second)));
}
else if (iterator.second == "d")
{
// 这是在处理日志中的日期
m_items.push_back(FormateItem::ptr(new DateTimeFormatItem(date)));
}
else
{
// 这是在处理模板字符
if (auto it = s_format_item.find(iterator.second); it != s_format_item.end())
{
// 这说明能够找到对应模板字符的类
m_items.push_back(it->second(iterator.second));
}
else
{
// 这表示该字符不是模板字符
std::cout << "[ERROR] LogFormatter::init() " << "pattern: [" << m_pattern << "] " << "unknown format item: " << iterator.second << std::endl;
error = true;
break;
}
}
}
if(error) {
m_error = true;
return;
}
}format(LogEvent::ptr event)- 作用: 按照 预先解析好的格式项 顺序,将 日志器、日志级别 和 日志事件 中的信息逐项写入字符串流,最终拼接成一条 完整的格式化日志字符串 并返回
1
2
3
4
5
6
7std::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();
}
5️⃣ 日志输出地
LogAppender的作用- 在
sylar框架中,LogAppender是日志系统中的 核心抽象类。它负责 格式化后的日志 输出到具体的目标位置(如 控制台、文件、远程服务器 等)。
- 在
核心概念
- 在日志系统中,日志的生成与日志的输出是分离的。
LogAppender就是 日志输出端 的抽象类,允许我们将同一条日志灵活地输出到多个位置。
- 在日志系统中,日志的生成与日志的输出是分离的。
整体架构
- 概述
LogAppender的目的是将 日志 按照 指定的格式 输出到 我们想要指定的地方;LogAppender一定会拥有成员LogFormatter,用于解析日志;- 同时为了将日志输出到指定地点,我们需要继承
LogAppender,因为我们需要根据想要输出的地点封装不同的类。
- 不管是
StdCoutLogAppender还是FileLogAppender,都只需要覆写log函数,将日志输出到不同的地点。就算我们还想将日志输出到不同的地点,我们只需要继承LogAppender,覆写log函数就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56class LogAppender {
friend class Logger;
public:
typedef std::shared_ptr<LogAppender> ptr;
typedef Spinlock MutexType;
/**
* @brief 析构函数
*/
virtual ~LogAppender() {}
/**
* @brief 写入日志
* @param[in] logger 日志器
* @param[in] level 日志级别
* @param[in] event 日志事件
*/
virtual void log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) = 0;
/**
* @brief 将日志输出目标的配置转成YAML String
*/
virtual std::string toYamlString() = 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;}
virtual bool reopen() { return true;}
protected:
/// 日志级别
LogLevel::Level m_level = LogLevel::DEBUG;
/// 是否有自己的日志格式器
bool m_hasFormatter = false;
/// Mutex
MutexType m_mutex;
/// 日志格式器
LogFormatter::ptr m_formatter;
};- 概述
LoggerAppender种类控制台(
StdCoutLogAppender)1
2
3
4
5
6void StdoutLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
if(level >= m_level) {
MutexType::Lock lock(m_mutex);
m_formatter->format(std::cout, logger, level, event);
}
}文件(
FileAppender)1
2
3
4
5
6
7
8
9
10
11
12
13
14void FileLogAppender::log(std::shared_ptr<Logger> logger, LogLevel::Level level, LogEvent::ptr event) {
if(level >= m_level) {
uint64_t now = event->getTime();
if(now >= (m_lastTime + 3)) {
reopen();
m_lastTime = now;
}
MutexType::Lock lock(m_mutex);
if(!m_formatter->format(m_filestream, logger, level, event)) {
std::cout << "error" << std::endl;
}
}
}日志服务器(
LogserverAppender)- 作用: 把一条达到输出级别的日志先格式化成字符串,再封装成 通知消息,通过 负载均衡 从
logserver集群里挑一个连接发出去;如果发送失败,就最多重试3次。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56void LogserverAppender::log(Logger::ptr logger, LogLevel::Level level, LogEvent::ptr event) {
// 日志级别过滤
if(level >= m_level) {
// 日志事件格式化成最终字符串
std::stringstream ss;
MutexType::Lock lock(m_mutex);
m_formatter->format(ss, logger, level, event);
lock.unlock();
// 把格式化后的日志内容封装成业务消息
logserver::LogNotify nty;
nty.set_body(ss.str());
nty.set_topic(m_topic);
nty.set_key(m_key);
// 业务消息包成 Rock 协议通知
sylar::RockNotify::ptr rock_nty = std::make_shared<sylar::RockNotify>();
rock_nty->setNotify(100);
rock_nty->setAsPB(nty);
// 尝试最多发送 3 次
for(size_t i = 0; i < 3; ++i) {
// 懒加载获取日志服务器负载均衡器
if(!m_lb) {
auto app = sylar::Application::GetInstance()->getRockSDLoadBalance();
if(app) {
m_lb = app->get("logserver", "logserver", true);
}
}
if(!m_lb) {
continue;
}
// 从负载均衡器里选一个可用节点
auto item = m_lb->get();
if(!item) {
continue;
}
// 拿到和该节点的 Rock 连接
auto conn = item->getStreamAs<sylar::RockStream>();
if(!conn) {
continue;
}
// 通过连接发送消息
if(conn->sendMessage(rock_nty) <= 0) {
continue;
}
return;
}
//SYLAR_LOG_ERROR(g_logger) << "send to logserver fail, " << ss.str();
}
}
- 作用: 把一条达到输出级别的日志先格式化成字符串,再封装成 通知消息,通过 负载均衡 从
6️⃣ 日志器
Logger的职责- 通过
LogLevel管理日志的等级控制- 每个
Logger对象都有一个等级(如DEBUG、INFO、WARN、ERROR、FATAL),只会记录大于或等于当前等级的日志。
- 每个
- 组织多个输出目标(
LogAppender)Logger并不直接将日志写入文件或控制台,而是将其分发给多个日志输出器(LogAppender),这些输出器可以是 文件、控制台、远程服务器 等。
- 保证线程安全的日志处理流程
- 通过互斥锁(如
Spinlock)保证多线程环境下的安全访问,防止日志混乱或丢失。
- 通过互斥锁(如
- 生成日志元数据
- 比如记录日志创建时间(
m_createTime)、名称(m_name)等,方便后期分析与归档。
- 比如记录日志创建时间(
- 通过
整体框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116class Logger : public std::enable_shared_from_this<Logger> {
friend class LoggerManager;
public:
typedef std::shared_ptr<Logger> ptr;
typedef RWSpinlock RWMutexType;
/**
* @brief 构造函数
* @param[in] name 日志器名称
*/
Logger(const std::string& name = "root");
/**
* @brief 写日志
* @param[in] level 日志级别
* @param[in] event 日志事件
*/
void log(LogLevel::Level level, LogEvent::ptr event);
/**
* @brief 写debug级别日志
* @param[in] event 日志事件
*/
void debug(LogEvent::ptr event);
/**
* @brief 写info级别日志
* @param[in] event 日志事件
*/
void info(LogEvent::ptr event);
/**
* @brief 写warn级别日志
* @param[in] event 日志事件
*/
void warn(LogEvent::ptr event);
/**
* @brief 写error级别日志
* @param[in] event 日志事件
*/
void error(LogEvent::ptr event);
/**
* @brief 写fatal级别日志
* @param[in] event 日志事件
*/
void fatal(LogEvent::ptr event);
/**
* @brief 添加日志目标
* @param[in] appender 日志目标
*/
void addAppender(LogAppender::ptr appender);
/**
* @brief 删除日志目标
* @param[in] appender 日志目标
*/
void delAppender(LogAppender::ptr appender);
/**
* @brief 清空日志目标
*/
void clearAppenders();
/**
* @brief 返回日志级别
*/
LogLevel::Level getLevel() const { return m_level;}
/**
* @brief 设置日志级别
*/
void setLevel(LogLevel::Level val) { m_level = val;}
/**
* @brief 返回日志名称
*/
const std::string& getName() const { return m_name;}
/**
* @brief 设置日志格式器
*/
void setFormatter(LogFormatter::ptr val);
/**
* @brief 设置日志格式模板
*/
void setFormatter(const std::string& val);
/**
* @brief 获取日志格式器
*/
LogFormatter::ptr getFormatter();
/**
* @brief 将日志器的配置转成YAML String
*/
std::string toYamlString();
bool reopen();
private:
/// 日志名称
std::string m_name;
/// 日志级别
LogLevel::Level m_level;
/// Mutex
RWMutexType m_mutex;
/// 日志目标集合
std::list<LogAppender::ptr> m_appenders;
/// 日志格式器
LogFormatter::ptr m_formatter;
/// 主日志器
Logger::ptr m_root;
};解析
- 日志器 存放了一组日志输出地
LogAppender,当调用函数log记录一个日志的时候,如果该日志的等级大于 日志器 的等级,就会遍历所有的 日志输出地,使用 日志输出地 将其解析并输出到指定的地方。
- 日志器 存放了一组日志输出地
7️⃣ 日志事件包装器
LogEventWrap的作用- 在实现一个高效、灵活的日志系统时,我们往往会设计一个
LogEvent类来描述一次具体的日志行为,同时用Logger类负责将这些事件格式化、输出到终端、文件或远程服务器等目标媒介。 - 但在实际开发中,仅靠
LogEvent和Logger可能并不足以满足需求,特别是在 使用宏定义简化日志语句 的场景下,如何优雅地延迟日志输出、自动触发日志写入成为一个实际问题。
- 在实现一个高效、灵活的日志系统时,我们往往会设计一个
整体框架
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class LogEventWrap {
public:
/**
* @brief 构造函数
* @param[in] e 日志事件
*/
LogEventWrap(LogEvent::ptr e);
/**
* @brief 析构函数
*/
~LogEventWrap();
/**
* @brief 获取日志事件
*/
LogEvent::ptr getEvent() const { return m_event;}
/**
* @brief 获取日志内容流
*/
std::stringstream& getSS();
private:
/**
* @brief 日志事件
*/
LogEvent::ptr m_event;
};关键函数
1
2
3LogEventWrap::~LogEventWrap() {
m_event->getLogger()->log(m_event->getLevel(), m_event);
}- 解析
LogEventWrap在构造时持有 **日志事件LogEvent**。- 在 析构时 自动调用 日志事件 对应的日志器的
log()方法,将当前日志事件正式输出。 - 这使得 日志语句 可以自然地延迟到作用域结束时统一输出。
- 解析
关键宏定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* @brief 使用流式方式将日志级别debug的日志写入到logger
*/
/**
* @brief 使用流式方式将日志级别info的日志写入到logger
*/
/**
* @brief 使用流式方式将日志级别warn的日志写入到logger
*/
/**
* @brief 使用流式方式将日志级别error的日志写入到logger
*/
/**
* @brief 使用流式方式将日志级别fatal的日志写入到logger
*/
/**
* @brief 使用格式化方式将日志级别level的日志写入到logger
*/
/**
* @brief 使用格式化方式将日志级别debug的日志写入到logger
*/
/**
* @brief 使用格式化方式将日志级别info的日志写入到logger
*/
/**
* @brief 使用格式化方式将日志级别warn的日志写入到logger
*/
/**
* @brief 使用格式化方式将日志级别error的日志写入到logger
*/
/**
* @brief 使用格式化方式将日志级别fatal的日志写入到logger
*/
8️⃣ 日志器管理类
LoggerManager的作用- 在构建一个功能完善的日志系统中,我们不仅需要能够生成和输出日志的
Logger类,还需要一个统一的日志管理中心来组织和协调多个日志器的使用。这就是LoggerManager类存在的意义。
- 在构建一个功能完善的日志系统中,我们不仅需要能够生成和输出日志的
为什么需要
LoggerManager?- 如果希望为每个模块配置不同的日志器,以便控制它们的日志级别、输出格式、日志文件路径等。这时,如果我们每次都手动创建和维护多个
Logger实例,不但容易出错,而且很难集中管理。
- 如果希望为每个模块配置不同的日志器,以便控制它们的日志级别、输出格式、日志文件路径等。这时,如果我们每次都手动创建和维护多个
整体框架

重要函数
getLogger()1
2
3
4
5
6
7
8
9
10
11
12
13
14Logger::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->addAppender(LogAppender::ptr(new StdoutLogAppender));
m_loggers[name] = logger;
return logger;
}如果在日志器管理类
LoggerManager中找不到对应名称的日志器Logger,就自动创建一个对应名称的日志器Logger,并返回给该日志器。
解析
在
LoggerManager的构造函数里,默认都有一个日志器root,该日志器的日志输出地是控制台StdCoutAppender,并被注册到了LoggerManager的成员日志器集合中m_loggers。主要用途在:
1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @brief 获得root日志器
*/
/**
* @brief 获得指定名称的日志器
*/
/**
* 单例模式的日志管理器
*/
using LoggerMgr = sylar::SingleTon<LoggerManager>;使用单例模式获得
LoggerManager的实例。
9️⃣ 日志配置
LogAppenderDefine
- 作用: 描述一个
LogAppender配置描述结构体通常用于从 YAML 配置文件 中读取 日志输出器 信息,再根据这些配置去创建真正的
LogAppender1
2
3
4
5
6
7
8
9
10
11
12
13struct LogAppenderDefine {
int type = 0; //1 File, 2 Stdout, 3 Logserver
LogLevel::Level level = LogLevel::UNKNOW;
std::string formatter;
std::string file;
bool operator==(const LogAppenderDefine& oth) const {
return type == oth.type
&& level == oth.level
&& formatter == oth.formatter
&& file == oth.file;
}
};
LogDefine
作用: 描述一个完整
Logger的配置项,用于保存某个日志器的名称、日志级别、默认格式器以及它关联的所有输出器配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21struct LogDefine {
std::string name;
LogLevel::Level level = LogLevel::UNKNOW;
std::string formatter;
std::vector<LogAppenderDefine> appenders;
bool operator==(const LogDefine& oth) const {
return name == oth.name
&& level == oth.level
&& formatter == oth.formatter
&& appenders == appenders;
}
bool operator<(const LogDefine& oth) const {
return name < oth.name;
}
bool isValid() const {
return !name.empty();
}
};
日志配置序列化与反序列化
日志配置反序列化器(
LexicalCast<std::string, LogDefine>)- 作用:
- 负责将
YAML形式 的 日志配置 解析为LogDefine对象。 - 它会从配置中提取
logger的名称、日志级别、格式器以及各个appender的配置信息,并组装成程序内部可用的日志配置结构。 - 本质上,它承担的是 “把配置文件中的文本描述转成内存中的日志配置对象” 的职责,是日志配置加载过程中的入口。
- 负责将
- 作用:
日志配置反序列化器(
LexicalCast<LogDefine, std::string>)- 作用:
- 将程序内部的
LogDefine对象重新转换成YAML形式的配置内容。 - 把
logger的 名称、级别、格式器 以及所有appender配置重新组织成 可输出的配置文本。 - 本质上,它承担的是 “把内存中的日志配置对象转回文本配置” 的职责,便于配置保存、打印、比较或调试。
- 将程序内部的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68template<>
class LexicalCast<std::string, LogDefine> {
public:
LogDefine operator()(const std::string& v) {
YAML::Node n = YAML::Load(v);
LogDefine ld;
if(!n["name"].IsDefined()) {
std::cout << "log config error: name is null, " << n
<< std::endl;
throw std::logic_error("log config name is null");
}
ld.name = n["name"].as<std::string>();
ld.level = LogLevel::FromString(n["level"].IsDefined() ? n["level"].as<std::string>() : "");
if(n["formatter"].IsDefined()) {
ld.formatter = n["formatter"].as<std::string>();
}
if(n["appenders"].IsDefined()) {
//std::cout << "==" << ld.name << " = " << n["appenders"].size() << std::endl;
for(size_t x = 0; x < n["appenders"].size(); ++x) {
auto a = n["appenders"][x];
if(!a["type"].IsDefined()) {
std::cout << "log config error: appender type is null, " << a
<< std::endl;
continue;
}
std::string type = a["type"].as<std::string>();
LogAppenderDefine lad;
if(type == "FileLogAppender") {
lad.type = 1;
if(!a["file"].IsDefined()) {
std::cout << "log config error: fileappender file is null, " << a
<< std::endl;
continue;
}
lad.file = a["file"].as<std::string>();
if(a["formatter"].IsDefined()) {
lad.formatter = a["formatter"].as<std::string>();
}
} else if(type == "StdoutLogAppender") {
lad.type = 2;
if(a["formatter"].IsDefined()) {
lad.formatter = a["formatter"].as<std::string>();
}
} else if(type == "LogserverAppender") {
lad.type = 3;
if(!a["topic"].IsDefined()) {
std::cout << "log config error: logserverappender topic is null, " << a
<< std::endl;
continue;
}
lad.file = a["topic"].as<std::string>();
if(a["formatter"].IsDefined()) {
lad.formatter = a["formatter"].as<std::string>();
}
} else {
std::cout << "log config error: appender type is invalid, " << a
<< std::endl;
continue;
}
ld.appenders.push_back(lad);
}
}
return ld;
}
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38template<>
class LexicalCast<LogDefine, std::string> {
public:
std::string operator()(const LogDefine& i) {
YAML::Node n;
n["name"] = i.name;
if(i.level != LogLevel::UNKNOW) {
n["level"] = LogLevel::ToString(i.level);
}
if(!i.formatter.empty()) {
n["formatter"] = i.formatter;
}
for(auto& a : i.appenders) {
YAML::Node na;
if(a.type == 1) {
na["type"] = "FileLogAppender";
na["file"] = a.file;
} else if(a.type == 2) {
na["type"] = "StdoutLogAppender";
} else if(a.type == 3) {
na["type"] = "LogserverAppender";
}
if(a.level != LogLevel::UNKNOW) {
na["level"] = LogLevel::ToString(a.level);
}
if(!a.formatter.empty()) {
na["formatter"] = a.formatter;
}
n["appenders"].push_back(na);
}
std::stringstream ss;
ss << n;
return ss.str();
}
};- 作用:
全局配置项(g_log_defines)
- 作用:
- 用于在配置系统中注册一个名为
logs的配置项,其类型为std::set<LogDefine>。 logs保存的是整个系统所有logger的配置集合,相当于日志模块在内存中的总配置入口。- 后续无论是 配置加载、配置比较,还是 热更新监听,都是围绕这个 全局配置变量 展开的。
- 用于在配置系统中注册一个名为
1 | sylar::ConfigVar<std::set<LogDefine> >::ptr g_log_defines = |
日志配置热更新初始化器(LogIniter)
- 作用:
- 在构造时为
g_log_defines注册一个 配置变更监听器,用于感知日志配置的 新增、修改和删除。 - 当
logs配置发生变化时,它会根 新旧配置差异,动态地创建、更新或移除对应的Logger。 - 对于变更的
logger,它会重新设置日志级别、格式器,并按配置重新挂载FileLogAppender、StdoutLogAppender或LogserverAppender。 - 它本质上承担的是 “将配置变化实时同步到日志系统运行状态中” 的职责,是日志热更新机制的核心入口。
- 在构造时为
1 | struct LogIniter { |
日志模块整体框架图

参考资料:
