C++服务器框架:协程库——日志模块

介绍日志模块,主要包括日志级别、日志格式化、日志输出、日志分类等功能。

1 日志模块概述

日志对于一个库或者框架来说是非常重要的,它可以帮助开发者了解程序的运行状态,排查问题,优化性能等。具体来说,日志模块用于格式化输出程序日志,方便从日志中定位程序运行过程中出现的问题。这里的日志除了日志内容本身之外,还应该包括文件名/行号,时间戳,线程/协程号,模块名称,日志级别等额外信息,甚至在打印致命的日志时,还应该附加程序的栈回溯信息,以便于分析和排查问题。

一个完善的日志模块应该具备以下特点:

  • 多日志级别:如DEBUGINFOWARNERRORFATAL等,用于区分日志的重要性和严重性。
  • 日志格式化:支持格式化输出,如时间戳、文件名/行号、线程/协程号、模块名称等,并可灵活配置。
  • 日志输出:支持输出到控制台、文件、网络等不同的目的地,或者同时输出到多个目的地。
  • 日志分类:日志可以分类并命名,一个程序的各个模块可以使用不同的名称来输出日志,方便定位问题。
  • 日志可配置:可以通过配置文件或者命令行参数来配置。

sylar日志模块类似于log4cplus日志库,提供了多日志级别、日志格式化、日志输出、日志分类等功能。

主要有以下几个类:

  • Logger:日志器,用于定义日志级别、日志格式、日志输出地。
  • class LogLevel:定义日志级别,并提供将日志级别与文本之间的互相转化
  • LogEvent:记录日志事件,主要记录一下信息。
  • LogFormatter:日志格式化器,用于格式化日志输出。
  • LogEventWrap:日志事件包装器,用于将日志事件和日志器绑定在一起。
  • LogAppender:日志输出器,用于将日志事件输出到指定的地方。
  • LogManager:日志管理器,用于管理日志器和日志输出器,采用单例模式。

2 LogLevel

通过枚举定义了9个日志级别

1
2
3
4
5
6
7
8
9
10
11
enum Level {
FATAL = 0, // 致命错误
ALERT = 1, // 警报,高优先级情况
CRIT = 2, // 严重错误
ERROR = 3, // 一般错误
WARN = 4, // 警告
NOTICE = 5, // 通知
INFO = 6, // 一般信息
DEBUG = 7, // 调试信息
NOTSET = 8 // 未设置
};

2.1 成员函数

2.1.1 ToString

将日志级别转换为字符串,函数声明static const char* ToString(LogLevel::Level level);

具体实现通过宏定义函数XX,使用switch语句将日志级别转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const char* LogLevel::ToString(LogLevel::Level level) {
switch (level) {
#define XX(name) case LogLevel::name: return #name;
XX(FATAL);
XX(ALERT);
XX(CRIT);
XX(ERROR);
XX(WARN);
XX(NOTICE);
XX(INFO);
XX(DEBUG);
#undef XX
default:
return "NOTSET";
}
return "NOTSET";
}

2.1.2 FromString

将字符串转换为日志级别,转换时不针对大小写,DEBUG和debug都可以完成对应的转化。通过宏定义函数XX,将字符串与日志级别进行比较,返回对应的日志级别。如果没有匹配的日志级别,则返回NOTSET

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LogLevel::Level LogLevel::FromString(const std::string& str) {
#define XX(level, v) if (str == #v) { return LogLevel::level; }
XX(FATAL, fatal);
XX(ALERT, alert);
XX(CRIT, crit);
XX(ERROR, error);
XX(WARN, warn);
XX(NOTICE, notice);
XX(INFO, info);
XX(DEBUG, debug);

XX(FATAL, FATAL);
XX(ALERT, ALERT);
XX(CRIT, CRIT);
XX(ERROR, ERROR);
XX(WARN, WARN);
XX(NOTICE, NOTICE);
XX(INFO, INFO);
XX(DEBUG, DEBUG);
#undef XX
return LogLevel::NOTSET;
}

3 LogEvent

日志事件,用于记录日志现场,比如该日志的级别,文件名/行号,日志消息,线程/协程号,所属日志器名称等,有以下成员变量:

1
2
3
4
5
6
7
8
9
10
LogLevel::Level m_level;     // 日志级别
std::stringstream m_ss; // 日志内容,使用stringstream存储,便于流式写入日志
const char *m_file = nullptr; // 文件名
int32_t m_line = 0; // 行号
int64_t m_elapse = 0; // 程序启动开始到现在的毫秒数
uint32_t m_threadId = 0; // 线程id
uint64_t m_fiberId = 0; // 协程id
time_t m_time; // 时间戳
std::string m_threadName; // 线程名称
std::string m_loggerName; // 日志器名称

构造函数如下:

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
/**
* @brief 构造函数
* @param[in] logger_name 日志器名称
* @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::LogEvent(const std::string& logger_name, LogLevel::Level level, const char* file, int32_t line,
int64_t elapse, uint32_t thread_id, uint64_t fiber_id, time_t time,
const std::string& thread_name)
: m_level(level)
, 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_loggerName(logger_name) { }

4 LogEventWrap

LogEventWrap类用于将LogEventLogger绑定在一起,因为一条日志只会在一个日志器上进行输出。将日志事件和日志器包装到一起后,方便通过宏定义来简化日志模块的使用。另外,LogEventWrap还负责在构建时指定日志事件和日志器,在析构时调用日志器的log方法将日志事件进行输出。

成员变量如下:

1
2
Logger::ptr m_logger;       // 日志器
LogEvent::ptr m_event; // 日志事件

4.1 成员函数

4.1.1 LogEventWrap()

构造函数:

1
2
3
4
LogEventWrap::LogEventWrap(Logger::ptr Logger, LogEvent::ptr event) 
: m_logger(Logger)
, m_event(event) { }

4.1.2 ~LogEventWrap()

析构函数:

1
2
3
4
// 将日志事件输出到日志器,在销毁时将m_event记录到日志中
LogEventWrap::~LogEventWrap() {
m_logger -> log(m_event);
}

在这里说一下,Sylar使用的日志的宏,log.h头文件定义了一系列的宏,其中SYLAR_LOG_LEVEL是一个通用的宏,用来输出Level级别的LogEvent,并将LogEvent写入到Logger中。

当使用该宏打印一次日志后,由于LogEvent使用的是智能指针,在定义该宏的作用域下这个LogEvent并不会立即释放,所以使用LogEventWarp包装LogEvent::ptr当定义该宏的语句执行完后就会自动进入析构函数,并将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

‵‵‵cpp
#define SYLAR_LOG_LEVEL(logger, level) \
if(logger -> getLevel() >= level) \
sylar::LogEventWrap(logger, sylar::LogEvent::ptr(new sylar::LogEvent(logger -> getName(), \
level, __FILE__, __LINE__, sylar::GetElapsedMS() - logger -> getCreateTime(), \
sylar::GetThreadId(), sylar::GetFiberId(), time(0), sylar::GetThreadName()))).getLogEvent() -> getSS()

#define SYLAR_LOG_FATAL(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::FATAL)

#define SYLAR_LOG_ALERT(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ALERT)

#define SYLAR_LOG_CRIT(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::CRIT)

#define SYLAR_LOG_ERROR(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::ERROR)

#define SYLAR_LOG_WARN(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::WARN)

#define SYLAR_LOG_NOTICE(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::NOTICE)

#define SYLAR_LOG_INFO(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::INFO)

#define SYLAR_LOG_DEBUG(logger) SYLAR_LOG_LEVEL(logger, sylar::LogLevel::DEBUG)

5 FormatItem

FormatItem类为LogFormatterpublic内部类成员,通过该类得到解析后的格式。 是一个抽象类,不同事件的子类继承该类,并且重写纯虚函数format将日志格式转化到std::ostream中。

格式化项的子类有:

  • MessageFormatItem:日志内容格式化项
  • LevelFormatItem:日志级别格式化项
  • ElapseFormatItem:日志耗时格式化项
  • LoggerNameFormatItem:日志器名称格式化项
  • ThreadIdFormatItem:线程id格式化项
  • FiberIdFormatItem:协程id格式化项
  • ThreadNameFormatItem:线程名称格式化项
  • DateTimeFormatItem:日期时间格式化项
  • FilenameFormatItem:文件名格式化项
  • LineFormatItem:行号格式化项
  • NewLineFormatItem:换行格式化项
  • StringFormatItem:字符串格式化项
  • TabFormatItem:Tab格式化项
  • PercentFormatItem:%格式化项
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

/// 日志格式器
class MessageFormatItem : public LogFormatter::FormatItem {
public:
MessageFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << event -> getContent(); }
};

/// 日志级别格式器
class LevelFormatItem : public LogFormatter::FormatItem {
public:
LevelFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << LogLevel::ToString(event -> getLevel()); }
};

/// 日志时间格式器
class ElapseFormatItem : public LogFormatter::FormatItem {
public:
ElapseFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << event -> getElapse(); }
};

/// 日志名称格式器
class LoggerNameFormatItem : public LogFormatter::FormatItem {
public:
LoggerNameFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << event -> getLoggerName(); }
};

/// 线程id格式器
class ThreadIdFormatItem : public LogFormatter::FormatItem {
public:
ThreadIdFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << event -> getThreadId(); }
};

/// 协程id格式器
class FiberIdFormatItem : public LogFormatter::FormatItem {
public:
FiberIdFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << event -> getFiberId(); }
};

/// 线程名称格式器
class ThreadNameFormatItem : public LogFormatter::FormatItem {
public:
ThreadNameFormatItem(const std::string& str) {}
void format(std::ostream& os, LogEvent::ptr event) override { os << event -> getThreadName(); }
};

/// 日期时间格式器
class DateTimeFormatItem : public LogFormatter::FormatItem {
public:
DateTimeFormatItem(const std::string& format = "%Y-%m-%d %H:%M:%S")
: m_format(format) {
if(m_format.empty()) m_format = "%Y-%m-%d %H:%M:%S";
}

void format(std::ostream& os, LogEvent::ptr event) override {
struct tm tm;
time_t time = event -> getTime();
localtime_r(&time, &tm); // 将时间转换为tm结构体
char buf[64];
strftime(buf, sizeof(buf), m_format.c_str(), &tm); // 格式化时间
os << buf;
}
private:
std::string m_format;
};

6 LogFormatter

日志格式器,与log4cppPatternLayout对应,用于格式化一个日志事件。该类构建时可以指定pattern,表示如何进行格式化。提供format方法,用于将日志事件格式化成字符串。

主要有以下成员变量:

1
2
3
std::string m_pattern;  // 日志格式模板
std::vector<FormatItem::ptr> m_items; // 日志格式解析后的格式项
bool m_error = false; // 是否有错误

6.1 成员函数

6.1.1 LogFormatter()

构造函数,初始化日志格式模板和解析日志格式项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    LogFormatter::LogFormatter(const std::string& pattern) 
: m_pattern(pattern) {
init();
}
```
### 6.1.2 `format()`

格式化日志,将日志事件格式化输出到`std::ostream`中。

```cpp
/// 格式化日志事件,返回格式化后的字符串
std::string LogFormatter::format(LogEvent::ptr event) {
std::stringstream ss;
for(auto& i : m_items) i -> format(ss, event);
return ss.str();
}

6.1.3 init()

初始化,解析格式模板,提取模板项,得到相应的FormatItem,放入到m_items

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
void LogFormatter::init() {
/// 按顺序存放解析出来的pattern项
/// 每个pattern项是一个pair,第一个元素是pattern的类型,第二个元素是pattern的内容
/// 类型为0表示普通字符串,类型为1表示需要格式化的项
std::vector<std::pair<int, std::string>> patterns;
std::string tmp; /// 临时存储常规字符串
std::string dateformat; /// 日期格式字符串,默认把位于%d后面的大括号对里的全部字符都当作格式字符,不校验格式是否合法
bool error = false; /// 标记解析是否出现错误
bool parsing_string = true; /// 标记当前是否在解析普通字符串, 初始为true

size_t i = 0; /// 当前解析到的位置
while(i < m_pattern.size()) {
std::string c = std::string(1, m_pattern[i]);
if(c == "%") {
if(parsing_string) {
if(!tmp.empty()) patterns.push_back(std::make_pair(0, tmp));
tmp.clear();
parsing_string = false; /// 在解析常规字符时遇到%,表示开始解析模板字符
i++;
continue;
} else {
patterns.push_back(std::make_pair(1, c));
parsing_string = true;
i++;
continue;
}
} else { /// 当前字符不是%
if(parsing_string) { /// 在解析常规字符, 直接加入tmp
tmp += c;
i++;
continue;
} else { // 模板字符,直接添加到patterns中,添加完成后,状态变为解析常规字符,%d特殊处理
patterns.push_back(std::make_pair(1, c));
parsing_string = true;

// 如果%d后面直接跟了一对大括号,那么把大括号里面的内容提取出来作为dateformat
if(c != "d") {
i++;
continue;
}
i++;
if(i < m_pattern.size() && m_pattern[i] != '{') continue;
i++;
while(i < m_pattern.size() && m_pattern[i] != '}') {
dateformat.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 = true;
return;
}

// 模板解析结束之后剩余的常规字符也要算进去
if(!tmp.empty()) {
patterns.push_back(std::make_pair(0, tmp));
tmp.clear();
}

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(c, LoggerNameFormatItem), // c:日志器名称
XX(r, ElapseFormatItem), // r:累计毫秒数
XX(f, FileNameFormatItem), // f:文件名
XX(l, LineFormatItem), // l:行号
XX(t, ThreadIdFormatItem), // t:编程号
XX(F, FiberIdFormatItem), // F:协程号
XX(N, ThreadNameFormatItem), // N:线程名称
XX(%, PercentFormatItem), // %:百分号
XX(T, TabFormatItem), // T:制表符
XX(n, NewLineFormatItem), // n:换行符
#undef XX
};

for(auto& v : patterns) {
if( v.first == 0) m_items.push_back(FormatItem::ptr(new StringFormatItem(v.second)));
else if(v.second == "d") m_items.push_back(FormatItem::ptr(new DateTimeFormatItem(dateformat)));
else {
auto it = s_format_items.find(v.second);
if(it == s_format_items.end()) {
std::cout << "[ERROR] LogFormatter::init() " << "pattern: [" << m_pattern << "] " <<
"unknown format item: " << v.second << std::endl;
error = true;
break;
} else m_items.push_back(it -> second(v.second));
}
}

if(error) {
m_error = true;
return;
}
}

假设给定的日志格式模板为

1
%d{%Y-%m-%d %H:%M:%S} [%rms]%T%t%T%N%T%F%T[%p]%T[%c]%T%f:%l%T%m%n
则经过init()函数解析后,得到的模板项patterns如下:

1
2
3
4
5
6
7
8
9
patterns = {
{1, "d"}, {0, "["}, {1, "r"}, {0"ms]"},
{1, "T"}, {1, "t"}, {1, "T"}, {1, "N"},
{1, "T"}, {1, "F"}, {1, "T"}, {0, "["},
{1, "p"}, {0, "]"}, {1, "T"}, {0, "["},
{1, "c"}, {0, "]"}, {1, "T"}, {1, "f"},
{0, ":"}, {1, "l"}, {1, "T"}, {1, "m"},
{1, "n"}
}

代码主要逻辑

  1. 首先定义了一个存储解析出来的模板项的容器patterns,每个模板项是一个pair,第一个元素表示模板类型(0表示普通字符串,1表示需要格式化的项),第二个元素是模板内容。

  2. 使用tmp变量临时存储常规字符串,dateformat变量存储日期格式字符串,默认将%d后面的大括号对里的全部字符都当作日期格式字符。

  3. 使用error标志来标记解析是否出现错误。

  4. 使用parsing_string标志来标记当前是否在解析普通字符串,初始为true

  5. 遍历日志格式模板,根据字符%的出现情况分别处理普通字符串和模板字符。

  6. 如果遇到%,则表示开始解析模板字符,如果在解析普通字符串时遇到%,则说明一个模板项解析完毕,将其加入patterns中。

  7. 如果遇到模板字符,则将其加入patterns中,并根据模板字符类型特殊处理%d,提取出日期格式字符串。

  8. 解析完成后,将剩余的常规字符也加入patterns中。

  9. 定义一个存储模板项创建函数的静态映射表s_format_items,将每个模板字符与相应的模板项创建函数关联起来。

  10. 根据解析出来的模板项,创建相应的FormatItem并存储到m_items中。

7 LogAppender

日志输出器,用于将一个日志事件输出到对应的输出地。

LogAppender类是一个抽象类,用于派生了StdoutLogAppenderFileLogAppender两个子类,分别用于将日志输出到控制台和文件中。两个子类分别实现了LogAppender类的log方法实现写入日志,实现toYamlString方法将日志输出器的配置转换为YAML格式的字符串。

成员变量:

1
2
3
MutexType m_mutex;
LogFormatter::ptr m_formatter; // 日志格式化器
LogFormatter::ptr m_defaultFormatter; // 默认格式化器

7.1 成员函数

7.1.1 setFormatter()

设置日志格式化器。

1
2
3
4
void LogAppender::setFormatter(LogFormatter::ptr val) {
MutexType::Lock lock(m_mutex);
m_formatter = val;
}

7.1.2 getFormatter()

获取日志格式化器,如果有设置则返回设置的格式化器,否则返回默认格式化器。

1
2
3
4
LogFormatter::ptr LogAppender::getFormatter() {
MutexType::Lock lock(m_mutex);
return m_formatter ? m_formatter : m_defaultFormatter;
}

7.2 派生类——StdoutLogAppender

类定义如下:

1
2
3
4
5
6
7
class StdoutLogAppender : public LogAppender {
public:
typedef std::shared_ptr<StdoutLogAppender> ptr;
StdoutLogAppender();
void log(LogEvent::ptr event) override;
std::string toYamlString() override;
};

7.2.1 log()

将日志事件输出到控制台,如果有设置格式化器,则使用设置的格式化器,否则使用默认格式化器。

1
2
3
4
void StdoutLogAppender::log(LogEvent::ptr event) {
if(m_formatter) m_formatter -> format(std::cout, event); // 如果有设置格式化器,则使用设置的格式化器
else m_defaultFormatter -> format(std::cout, event); // 否则使用默认格式化器
}

7.2.2 toYamlString()

将日志输出器的配置转换为YAML格式的字符串。

1
2
3
4
5
6
7
8
9
std::string StdoutLogAppender::toYamlString() {
MutexType::Lock lock(m_mutex);
YAML::Node node;
node["type"] = "StdoutLogAppender";
node["pattern"] = m_formatter -> getPattern();
std::stringstream ss;
ss << node;
return ss.str();
}

7.3 派生类——FileLogAppender

输出到文件,类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class FileLogAppender : public LogAppender {
public:
typedef std::shared_ptr<FileLogAppender> ptr;
FileLogAppender(const std::string &file);
void log(LogEvent::ptr event) override;
bool reopen();
std::string toYamlString() override;

private:
std::string m_filename; // 文件路径
std::ofstream m_filestream; // 文件流
uint64_t m_lastTime = 0; // 上次重新打开时间
bool m_reopenError = false; // 文件打开错误标志
};

7.3.1 FileLogAppender()

构造函数,初始化文件路径,并打开

1
2
3
4
5
FileLogAppender::FileLogAppender(const std::string& file) : LogAppender(LogFormatter::ptr(new LogFormatter)) {
m_filename = file;
reopen();
if(m_reopenError) std::cout << "reopen file " << m_filename << " error" << std::endl;
}

7.3.2 reopen()

重新打开日志文件, 如果文件已经打开,先关闭文件,再打开文件。

1
2
3
4
5
6
7
8
bool FileLogAppender::reopen() {
MutexType::Lock lock(m_mutex);
if(m_filestream)
m_filestream.close();
m_filestream.open(m_filename, std::ios::app); // 以追加的方式写入文件
m_reopenError = !m_filestream; // 文件打开失败,设置错误标志为true,否则为false
return !m_reopenError; // 返回文件打开是否成功,成功true
}

7.3.3 log()

重写了log方法,将日志事件输出到文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void FileLogAppender::log(LogEvent::ptr event) {
uint64_t now = event -> getTime();
if(now >= m_lastTime + 3) { // 如果当前时间距离上次写日志超过3秒,那就重新打开一次日志文件
reopen();
if(m_reopenError)
std::cout << "reopen file " << m_filename << " error" << std::endl;
m_lastTime = now;
}

if(m_reopenError) return;

MutexType::Lock lock(m_mutex);
if(m_formatter) {
// 格式化器存在,用格式化器格式化日志事件,如果格式化失败,输出错误信息
if(!m_formatter -> format(m_filestream, event))
std::cout << "[ERROR] FileLogAppender::log() " << "format error" << std::endl;
} else {
// 格式化器不存在,用默认格式化器格式化日志事件,如果格式化失败,输出错误信息
if(!m_defaultFormatter -> format(m_filestream, event))
std::cout << "[ERROR] FileLogAppender::log() " << "format error" << std::endl;
}
}

7.3.4 toYamlString()

重写toYamlString方法,将日志输出器的配置转换为YAML格式的字符串。

1
2
3
4
5
6
7
8
9
10
std::string FileLogAppender::toYamlString() {
MutexType::Lock lock(m_mutex);
YAML::Node node;
node["type"] = "FileLogAppender";
node["file"] = m_filename;
node["pattern"] = m_formatter ? m_formatter -> getPattern() : m_defaultFormatter -> getPattern();
std::stringstream ss;
ss << node;
return ss.str();
}

8 Logger

日志器,负责进行日志输出。一个Logger包含多个LogAppender和一个日志级别,提供log方法,传入日志事件,判断该日志事件的级别高于日志器本身的级别之后调用LogAppender将日志进行输出,否则该日志被抛弃。

主要有以下成员变量:

1
2
3
4
5
MutexType m_mutex;                          // 互斥锁
std::string m_name; // 日志器名称
LogLevel::Level m_level; // 日志级别
std::list<LogAppender::ptr> m_appenders; // 日志输出器集合
uint64_t m_createTime; // 创建时间

8.1 Logger类的成员函数

8.1.1 Logger()

构造函数,初始化日志器名称和日志级别。

1
2
3
4
Logger::Logger(const std::string& name) 
: m_name(name)
, m_level(LogLevel::INFO)
, m_createTime(GetElapsedMS()) { }

8.1.2 log()

调用Logger的所有appenders将日志写一遍,Logger至少要有一个appender,否则没有输出

1
2
3
4
5
6
void Logger::log(LogEvent::ptr event) {
if(event -> getLevel() >= m_level) {
MutexType::Lock lock(m_mutex);
for(auto& i : m_appenders) i -> log(event);
}
}

8.1.3 addAppender()

添加日志输出器。

1
2
3
4
void Logger::addAppender(LogAppender::ptr appender) {
MutexType::Lock lock(m_mutex);
m_appenders.push_back(appender);
}

8.1.4 delAppender()

删除日志输出器,在m_appenders中查找appender,找到后删除。

1
2
3
4
5
6
7
8
9
void Logger::delAppender(LogAppender::ptr appender) {
MutexType::Lock lock(m_mutex);
for(auto it = m_appenders.begin(); it != m_appenders.end(); it++) {
if(*it == appender) {
m_appenders.erase(it);
break;
}
}
}

8.1.5 toYamlString()

将当前Loggerlevelnameappenders转换为YAML格式的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
std::string Logger::toYamlString() {
MutexType::Lock lock(m_mutex);
YAML::Node node;
node["name"] = m_name;
node["level"] = LogLevel::ToString(m_level);
for(auto& i : m_appenders) {
node["appenders"].push_back(YAML::Load(i -> toYamlString()));
}
std::stringstream ss;
ss << node;
return ss.str();
}

9 LogManager

日志器管理类,单例模式,用于统一管理所有的日志器,提供日志器的创建与获取方法。LogManager自带一个root Logger,用于为日志模块提供一个初始可用的日志器。

1
typedef sylar::Singleton<LoggerManager> LoggerMgr;

成员变量:

1
2
3
MutexType m_mutex;                              // 互斥锁
std::map<std::string, Logger::ptr> m_loggers; // 日志器集合
Logger::ptr m_root; // 根日志器

9.1 成员函数

9.1.1 LoggerManager()

构造函数,初始化根日志器。

1
2
3
4
5
6
LoggerManager::LoggerManager() {
m_root.reset(new Logger);
m_root -> addAppender(LogAppender::ptr(new StdoutLogAppender));
m_loggers[m_root -> getName()] = m_root;
init();
}

9.1.2 getLogger()

获取日志器,如果指定名称的日志器未找到,那会就新创建一个,但是新创建的Logger是不带Appender的,需要手动添加Appender

1
2
3
4
5
6
7
8
9
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));
m_loggers[name] = logger;
return logger;
}

9.1.3 toYamlString()

将日志格式转化为YAML格式的字符串。

1
2
3
4
5
6
7
8
9
10
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();
}

总结

虽然这个模块的知识点不多,但它确实考验了对C++基础知识的理解和应用。各个类之间的联系非常紧密,有些代码虽然没被详细解释,但它们都是相对简单的,关键部分的代码已经进行了详尽的阐述。

可以注意到,在编写代码时,频繁地使用了宏定义,在后续的项目开发中,宏定义同样被广泛采用,它让代码变得更加简洁。

在日志管理方面,采用了单例模式来确保从容器m_loggers中获取的日志器是唯一的。

日志管理器首先会初始化一个主日志器并放入容器中。如果尝试创建一个新的日志器,并且没有为它指定一个附加器(appender),那么就会使用这个主日志器来输出日志。

值得注意的是,尽管使用的是主日志器进行输出,但输出时显示的日志名称并不是主日志器的名称,而是根据事件(event)中指定的日志器名称来输出的。

总结一下日志模块的工作流程:

  1. 初始化LogFormatterLogAppender, Logger

  2. 通过宏定义提供流式风格和格式化风格的日志接口。每次写日志时,通过宏自动生成对应的日志事件LogEvent,并且将日志事件和日志器Logger包装到一起,生成一个LogEventWrap对象。

  3. 日志接口执行结束后,LogEventWrap对象析构,在析构函数里调用Logger的log方法将日志事件进行输出。