spdlog

1. 用于啥需求?

打日志。spdlog 是一个高性能、易用的 C++ 日志库。

2. 何时使用 spdlog?

其格式化风格同 std::format/std::print,如果这符合您的习惯可以考虑。稣一般会在复杂场景下使用 Boost.Log,测试程序或简单的程序里使用 spdlog。

举例啥叫复杂场景:产品里有多个可执行程序(Executable),譬如说 A 和 B,它们共同使用多个动态链接库(Dynamic-Link Library),譬如说 X 和 Y。注意,这里说的“使用”,可能是静态加载,也可能是动态加载(比如动态链接库是插件)。当 X/Y 在 A 进程里时,它们的打印风格、设置都应该受 A 控制,而在 B 进程时,则受 B 控制。不管可执行程序和动态链接库有多少个,每个进程都应该只有一个 Logger。这个需求 Boost.Log 能轻松实现,而 spdlog 可能不轻松,因为在雪蛤油时,有个熟悉 spdlog 的同学和稣打赌,结果他用没能轻易实现。也就是说即使 spdlog 能实现,那也不轻松。

3. 具体应用

首先注意到 Logger 的打印接口有两大类,一类是函数,比如 spdlog::info,只要你用了,它就被编译到程序里;另一类是宏,比如说 SPDLOG_LEVEL_INFO,它是否会被编译到程序里,受 SPDLOG_ACTIVE_LEVEL 控制。具体看以下注释:

1
2
3
4
5
6
7
8
9
10
11
12
//
// enable/disable log calls at compile time according to global level.
//
// define SPDLOG_ACTIVE_LEVEL to one of those (before including spdlog.h):
// SPDLOG_LEVEL_TRACE,
// SPDLOG_LEVEL_DEBUG,
// SPDLOG_LEVEL_INFO,
// SPDLOG_LEVEL_WARN,
// SPDLOG_LEVEL_ERROR,
// SPDLOG_LEVEL_CRITICAL,
// SPDLOG_LEVEL_OFF
//

spdlog 支持 6 种日志级别:

级别 说明 适用场景
trace 最详细的调试信息 开发调试
debug 调试信息 开发环境
info 一般信息 运行状态
warn 警告 潜在问题
error 错误(但程序可继续运行) 异常情况
critical 严重错误(可能崩溃) 致命问题

其中的 debug 级别,稣总觉得不应该存在,根据情况归到 trace 或 info 即可。

spdlog::set_level 设置的是运行时的显示级别,比如说:

1
2
3
spdlog::set_level(spdlog::level::debug); // 只显示 >= debug 的日志
spdlog::trace("This won't show (level too low)"); // 不会输出
spdlog::debug("Debug info"); // 会输出

但通常我们会使用宏来打印日志,并通过设定 SPDLOG_ACTIVE_LEVEL 来去掉低级别日志,以提高运行效率,或防止被“轻松逆向”。一般来说,Debug 版本可以设定 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_TRACE,而 Release 版本可以 #define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_WARN

既然我们使用宏来打印日志,那么我们就能注意到:宏也有两类,一类形如 SPDLOG_LOGGER_INFO,需要一个 logger 参数,另一类形如 SPDLOG_INFO,不需要传入 logger 参数。从以下代码可知,后者是默认 Logger 的打印宏。

1
2
3
4
5
6
7
8
#if SPDLOG_ACTIVE_LEVEL <= SPDLOG_LEVEL_INFO
#define SPDLOG_LOGGER_INFO(logger, ...) \
SPDLOG_LOGGER_CALL(logger, spdlog::level::info, __VA_ARGS__)
#define SPDLOG_INFO(...) SPDLOG_LOGGER_INFO(spdlog::default_logger_raw(), __VA_ARGS__)
#else
#define SPDLOG_LOGGER_INFO(logger, ...) (void)0
#define SPDLOG_INFO(...) (void)0
#endif

如果我们不想使用默认 Logger,就得自己创建 Logger,并使用第一类形如 SPDLOG_LOGGER_INFO 的宏,但这个宏有点长,还是把自己创建的 Logger 设置为默认,再使用第二类形如 SPDLOG_INFO 的宏方便点。

1
2
3
4
5
6
7
8
9
inline bool InitializeLogger() {
auto logger = std::make_shared<spdlog::logger>(
"", std::make_shared<spdlog::sinks::wincolor_stderr_sink_mt>());
if (!logger) {
return false;
}
spdlog::set_default_logger(std::move(logger));
return true;
}

至于输出格式,稣的测试程序一般如此设定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(int argc, char* argv[]) {
nw::args _(argc, argv);
nw::nowide_filesystem();
::SetConsoleOutputCP(CP_UTF8);

if (!InitializeLogger()) {
nw::cerr << "Failed to initialize logger!" << std::endl;
return EXIT_FAILURE;
}
#if _DEBUG
spdlog::set_level(spdlog::level::trace);
spdlog::set_pattern("%Y-%m-%dT%H:%M:%S.%e |%P~%5t <%^%L%$> %s#%#: %v");
#else
spdlog::set_level(spdlog::level::info);
spdlog::set_pattern("%Y-%m-%dT%H:%M:%S.%e |%P~%5t <%^%L%$> %v");
#endif
SPDLOG_TRACE("Starting up...");

注意:默认情况下,spdlog 是同步的!如果日志量很大,应该使用异步日志(减少主线程阻塞)。这个……问掐鸡(LLM)吧!

如果您使用微信,也可以关注公众号 UMU618,在公众号文章里评论。