C++ 日志框架 spdlog 的初步上手

本文最后更新于:2021年5月27日 晚上

简述

最近一直在忙着做毕业设计,就是一个客户端程序,算下来我做过的两个比较大的项目,一个是去年实习用 Python 和 Qt 做的客户端程序,一个就是毕业设计用 C++ 和 Qt 做的客户端,和 Qt 还挺有缘分的。

在这两个项目中我都写了日志打印的代码,自己写的,因为完全没接触到过日志框架这种东西,当然一方面我的需求也不是特别的高,都是很基础的日志打印,在这两个项目中我都是采用写一个标准日志打印函数,这个函数接受一个字符串,即日志内容,然后函数内部获取当前时间并添加到字符串首部,打印到控制台,或者同时写入日志文件保存。挺简单的,毕竟日志数量并不算多,也不会有太大的性能问题。

前些天突然了解到日志框架这种概念,就特意去了解了 C++ 的日志框架,挺多的,比如 glogspdloglog4cplus 等等。

没有做详细的对比,我直接选了 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 ,即只需将要打印的内容作为参数传入即可,默认同时输出到终端和文件,也可以指定 savefalse ,即仅打印到终端而不写入文件。

#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::stringchar * 是支持的,所有 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 是可以支撑服务端快速打印大量日志的,简言之就是它的能力很强,而我的小程序对日志打印的速度并没有太高的要求😉

最后,开源的世界真好 (≧∇≦)ノ 感谢开发者们!