C++ 日志框架 spdlog 的初步上手
本文最后更新于:2023年2月8日 晚上
简述
最近一直在忙着做毕业设计,就是一个客户端程序,算下来我做过的两个比较大的项目,一个是去年实习用 Python 和 Qt 做的客户端程序,一个就是毕业设计用 C++ 和 Qt 做的客户端,和 Qt 还挺有缘分的。
在这两个项目中我都写了日志打印的代码,自己写的,因为完全没接触到过日志框架这种东西,当然一方面我的需求也不是特别的高,都是很基础的日志打印,在这两个项目中我都是采用写一个标准日志打印函数,这个函数接受一个字符串,即日志内容,然后函数内部获取当前时间并添加到字符串首部,打印到控制台,或者同时写入日志文件保存。挺简单的,毕竟日志数量并不算多,也不会有太大的性能问题。
前些天突然了解到日志框架这种概念,就特意去了解了 C++ 的日志框架,挺多的,比如 glog 、spdlog 、 log4cplus 等等。
没有做详细的对比,我直接选了 GitHub 星星最多的 spdlog 作为尝试。总的来说,是非常方便,挺值得使用的。
安装
spdlog 是仅含有头文件的开源库,直接下载源码把头文件包含到项目里就可以,我比较喜欢 vcpkg :
vcpkg install spdlog
仓库首页也提供了很多种安装方式。
使用
快速上手
关于使用我也不做过多的介绍了,推荐查看官方文档:https://github.com/gabime/spdlog/wiki
能想到的以及不能想到的特性它都支持,比如:
- 输出日志到终端;
- 输出日志到文件;
- 以不同颜色输出到终端;
- 日志等级;
- 多文件文件管理;
- ……
如前所述,我的需求是很简单的,只要可以通过一个标准接口将日志输出到终端和文件即可。spdlog 可以在包含头文件后一行代码直接输出日志,像 std::cout 一样,但实际上创建一个 logger 更方便管理一些。如下代码分别创建了一个输出日志到终端的 logger 和一个输出日志到文件的 logger ,后续就可以调用 logger 的日志输出函数用于打印日志。
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
std::shared_ptr<spdlog::logger> console_logger = spdlog::stdout_color_mt("console_logger");
std::shared_ptr<spdlog::logger> file_logger = spdlog::basic_logger_mt("file_logger", "filename.log");
console_logger->info("Welcome to spdlog!");
file_logger->warn("Welcome to spdlog!");
但上述代码并没有满足我的需求,因为我希望的是通过一个函数同时输出到终端和日志文件,而上述代码需要分别调用两个 logger 的日志输出函数才可以做到。查阅 spdlog 的文档,其实也是可以做到的,例如如下的代码即官方给出的示例,可以将多个 logger 绑定到一起,一次调用即输出到多个终端或文件,并且可以通过日志等级按需输出,真的是特别细致啊!
// create logger with 2 targets with different log levels and formats.
// the console will show only warnings or errors, while the file will log all.
void multi_sink_example()
{
auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console_sink->set_level(spdlog::level::warn);
console_sink->set_pattern("[multi_sink_example] [%^%l%$] %v");
auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/multisink.txt", true);
file_sink->set_level(spdlog::level::trace);
spdlog::logger logger("multi_sink", {console_sink, file_sink});
logger.set_level(spdlog::level::debug);
logger.warn("this should appear in both console and file");
logger.info("this message should not appear in the console, only in the file");
}
不过在我的项目中,我还是没有采用这种方式,原因是我其实并没有输出不同等级日志的需求,它提供的服务反而对我来说有点复杂了😂
我想要的
所以我自己封装了两个函数,如下。initSpdLogger()
用于在程序启动时初始化日志框架,包括初始化两个 logger 和设置了我想要的日志格式,像这样:[2021-05-25 19:05:07.441] [784] Welcome to spdlog!
,分别是时间、线程和日志内容;printLog()
用于打印日志, save
参数默认为 true
,即只需将要打印的内容作为参数传入即可,默认同时输出到终端和文件,也可以指定 save
为 false
,即仅打印到终端而不写入文件。
#include <QString>
#include <QDate>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
std::shared_ptr<spdlog::logger> file_logger;
std::shared_ptr<spdlog::logger> console_logger;
bool initSpdLogger()
{
try
{
spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] %v");
QString date = QDate::currentDate().toString("yyyy-MM-dd");
std::string log_file = "log/" + date.toLocal8Bit() + ".log";
file_logger = spdlog::basic_logger_mt("file_logger", log_file);
console_logger = spdlog::stdout_color_mt("console_logger");
}
catch (const spdlog::spdlog_ex &ex)
{
std::cout << "Log init failed: " << ex.what() << std::endl;
return false;
}
return true;
}
void printLog(const QString &log, bool save=true)
{
console_logger->info(log.toLocal8Bit().toStdString());
if (save)
{
file_logger->info(log.toUtf8().toStdString());
}
}
void printLog(const std::string &log, bool save=true)
{
console_logger->info(log);
if (save)
{
file_logger->info(log);
}
}
官方文档提供了关于格式化输出的详细介绍:https://github.com/gabime/spdlog/wiki/3.-Custom-formatting 。
由于我项目主要是 Qt 写的,上述代码中也用到了 Qt 的相关函数,中文字符的打印一定是很多人的疑惑,字符编码真的是很麻烦事情。关于 spdlog 支持的字符串类型我没有找到相关说明,大概是 std::string
和 char *
是支持的,所有 QString 需要做转换,如果是中文,通常统一使用 log.toLocal8Bit().toStdString()
是没有问题的,终端一般默认都是 GBK 编码,一定要这么转才能正常显示;打印到文件则可以使用兼容性更好一些的 UTF-8 编码,即 log.toUtf8().toStdString()
,都可以正常显示。
刷新机制
需要注意以下 spdlog 的刷新机制,输出到终端的日志应该是立即就刷新了,但输出到文件的日志会在程序正常关闭的时候才刷写到日志文件中,也就是说如果程序没有正常关闭,比如卡死了,那么日志就会丢失。
官方文档专门对刷新策略进行了介绍:https://github.com/gabime/spdlog/wiki/7.-Flush-policy
一是手动调用 flush()
函数刷新,如果要确保每条日志都立即刷新,那么就在输出一条日之后立即调用该函数。
console_logger->info(log);
console_logger->flush();
二是设置某种等级的日志就刷新,相当于对日志等级加了一个判断,如果是某个等级的日志就刷新。
my_logger->flush_on(spdlog::level::err);
还有一种方法是设置刷新间隔时间,每隔一定时间就刷新一次,例如每 5s 刷新一次。
spdlog::flush_every(std::chrono::seconds(5));
结语
本文并没有深入介绍 spdlog ,因为我觉得它的使用真的很简单,基本没有上手难度啊!
spdlog 是可以支撑服务端快速打印大量日志的,简言之就是它的能力很强,而我的小程序对日志打印的速度并没有太高的要求😉
最后,开源的世界真好 (≧∇≦)ノ 感谢开发者们!