日志模块
日志模块
1️⃣ 日志的作用
- 在服务器的运行过程中,日志可以记录服务器运行过程中的各种事件,所以日志是非常有作用的。
- 记录服务器的启动和关闭
- 记录客户端连接/断开
- 记录HTTP 请求的接收与响应
- 记录协程调度执行情况
- 记录异常或错误信息(如断网、文件打不开等)
- 当运行过程中当出现异常行为(比如响应延迟、崩溃、死锁等)时,开发者可以通过日志快速定位问题所在:
- 快速定位问题发生的代码逻辑
- 分析错误发生的上下文(线程 ID、时间戳、调用路径等)
- 重现问题流程
- 在
sylar
服务器框架中,使用了类似Log4cpp
的结构,支持多级日志输出,支持流式日志风格写日志和格式化风格写日志,支持日志格式自定义,日志级别,多日志分离等等功能
2️⃣ 日志级别
整体框架
sylar
框架中主要使用logLevel
表示日志级别
日志等级
- 日志等级主要根据严重程度,从大到小依次排序
1
2
3
4
5
6
7
8
9
10
11
12enum Level
{
FATAL = 0, // 致命情况,系统不可用
ALERT = 100, // 高优先级情况,例如数据库系统崩溃
CRIT = 200, // 严重错误,例如硬盘错误
ERROR = 300, // 错误
WARN = 400, // 警告
NOTICE = 500, // 正常但值得注意
INFO = 600, // 一般信息
DEBUG = 700, // 调试信息
NOTSET = 800 // 未设置
};
- 日志等级主要根据严重程度,从大到小依次排序
转换函数
- 为了方便在代码里使用,
sylar
在logLevel
中提供了两个方法,用于将Level
转换成字符串,或者将字符串转换成Level
。1
2
3
4
5
6
7
8
9
10
11
12
13
14/**
* @brief 日志级别转字符串
* @param Level 日志级别
* @return string 字符串形式的日志级别
*/
static const char* LevelToString(LogLevel::Level level);
/**
* @brief 字符串形式的日志级别转换成LogLevel::Level
* @param const char*
* @return LogLevel::Level
*/
static const LogLevel::Level StringToLevel(const std::string& str);
- 为了方便在代码里使用,
3️⃣ 日志事件
整体框架
sylar
中主要使用LogEvent
表示一个日志事件
主要成员
1
2
3
4
5
6
7
8
9
10LogLevel::Level m_level; // 日志级别
std::stringstream m_ss; // 日志内容
const char* m_filename = 0; // 文件名
int32_t m_line = 0; // 行号
uint64_t m_elapse = 0; // 从日志器创建到当前日志的时间
uint32_t m_threadid = 0; // 线程号
uint64_t m_fiberid = 0; // 协程号
time_t m_time; // 时间戳
std::string m_threadName; // 线程名称
std::string m_loggerName; // 日志器的名称LogEvent
中只有get
方法用来提供对外的接口,返回对应的私有成员,例如:1
LogLevel::Level getLevel() const { return m_level; }
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
7
8
9
10std::string LogFormatter::format(LogEvent::ptr event)
{
std::stringstream ss;
// 遍历具体的日志处理类
for (auto& i: m_items)
{
i->format(ss, event);
}
return ss.str();
}
5️⃣ 日志输出地
LogAppender
的作用- 在
sylar
框架中,LogAppender
是日志系统中的核心抽象类。它负责将格式化后的日志输出到具体的目标位置(如控制台、文件、远程服务器等)。
- 在
核心概念
- 在日志系统中,日志的生成与日志的输出是分离的。
LogAppender
就是日志输出端的抽象类,允许我们将同一条日志灵活地输出到多个位置。
- 在日志系统中,日志的生成与日志的输出是分离的。
整体架构
主要成员
1
2
3MutexType m_mutex; // 互斥量
LogFormatter::ptr m_formatter; // 日志格式化器
LogFormatter::ptr defalut_formatter; // 默认日志格式化器解析
LogAppender
的目的是将向日志根据指定的格式输出到我们想要指定的地方,所以LogAppender
一定会拥有成员LogFormatter
,用于解析日志;同时为了将日志输出到指定地点,我们需要继承LogAppender
,因为我们需要根据想要输出的地点封装不同的类。- 不管是
StdCoutLogAppender
还是FileLogAppender
,都只需要覆写log
函数,将日志输出到不同的地点。就算我们还想将日志输出到不同的地点,我们只需要继承LogAppender
, 覆写log
函数就可以了。
6️⃣ 日志器
Logger
的职责- 通过
LogLevel
管理日志的等级控制- 每个
Logger
对象都有一个等级(如DEBUG
、INFO
、WARN
、ERROR
、FATAL
),只会记录大于或等于当前等级的日志。
- 每个
- 组织多个输出目标(
LogAppender
)Logger
并不直接将日志写入文件或控制台,而是将其分发给多个日志输出器(LogAppender
),这些输出器可以是文件、控制台、远程服务器等。
- 保证线程安全的日志处理流程
- 通过互斥锁(如
Spinlock
)保证多线程环境下的安全访问,防止日志混乱或丢失。
- 通过互斥锁(如
- 生成日志元数据
- 比如记录日志创建时间(
m_createTime
)、名称(m_name
)等,方便后期分析与归档。
- 比如记录日志创建时间(
- 通过
整体框架
解析
- 日志器存放了一组日志输出地
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
关键函数
1
~LogEventWrap() { m_logger->log(m_event); }
解析
- 日志事件包装器主要是将日志事件
LogEvent
和日志器Logger
结合起来 ! LogEventWrap
在构造时持有一个日志器Logger
和一个日志事件LogEvent
,但并不会立即将日志事件写入日志器。而是在析构时自动调用日志器的log()
方法,将日志事件正式输出。- 这使得日志语句可以自然地延迟到作用域结束时统一输出。
- 日志事件包装器主要是将日志事件
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️⃣ 日志模块整体框架图
参考资料:
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 GYu的妙妙屋!