007-spdlog-日志库

一、简介、安装和测试

日志应用场景:

  1. 程序输出业务信息——最终用户;
  2. 输出运行信息——维护人员。

spdlog库的特点:将各种日志通过日志记录器(logger)吐到各种槽当中

  • 标准输出槽、标准带色输出槽、标准错误输出槽;
  • 单一文件槽、序号轮换文件槽、“日抛型”文件槽;
  • 各系统的日志槽;
  • 网络tcp/udp槽;
  • 内存流槽;
  • 其他自定义槽。

工程最佳实践1:日志仅包含程序运行的信息,不要包含多余的内容。

测试

创建cmake项目时需要注意包含以下3行:

set(CMAKE_EXE_LINKER_FLAGS "-static") #方便程序在外部控制台独立运行
target_link_libraries(${PROJECT_NAME} PRIVATE fmt) #spdlog无需预编译为库即可使用,但是其依赖fmt库
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_WCHAR_TO_UTF8_SUPPORT) #输出到windows系统事件记录表中需用到汉字编码

cmake文件:

cmake_minimum_required( VERSION 3.10)

project(helloSpdlog VERSION 1.0 LANGUAGES CXX C)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED True)

set(CMAKE_EXE_LINKER_FLAGS "-static")

add_executable(helloSpdlog main.cpp)

target_link_libraries(${PROJECT_NAME} PRIVATE fmt)
target_compile_definitions(${PROJECT_NAME} PRIVATE SPDLOG_WCHAR_TO_UTF8_SUPPORT)

cpp文件:

#include <cstdlib>
#include <iostream>

#include <spdlog/spdlog.h>

int main()
{
#if defined(_WIN32)
    std::system("chcp 65001 > nul");
#endif

    spdlog::info("Welcome to spdlog version {}.{}.{}  !", 1, 14, 1);

    std::system("pause");
}

输出结果:

[2025-07-09 17:53:04.117] [info] Welcome to spdlog version 1.14.1  !

二、日志记录器logger

工程最佳实践2:优先使用全局记录器,实现统一的日志管理策略(简单-确定-统一)。

核心对象:logger

  • spdlog::default_logger():获取默认记录器;
  • spdlog::set_default_logger(newLogger):替换默认记录器。

1.替换默认日志记录器

1752056194313

void spdlogColorlessOutput()
{
    spdlog::info("这是一个信息日志");
    spdlog::warn("这是一个警告日志");

    auto colorless = spdlog::stdout_logger_mt("colorless");
    spdlog::set_default_logger(colorless);

    spdlog::info("这是一个信息日志");
    spdlog::warn("这是一个警告日志");
}

输出:

1752057227242

变化:

  • 输出的日志有了名字;
  • 日志等级没有颜色。

2.记录器分类

2.1标准控制台(stdout)记录器

2.1.1不带色

头文件:<spdlog/sinks/stdout_sinks.h>

工厂方法:

spdlog::stdout_logger_mt(std::string const& logger_name);
spdlog::stdout_logger_st(std::string const& logger_name);

补充说明:为什么要输入logger的名字作为参数?

方便随时随地通过名字取得logger:auto logger = spdlog::get("name")

2.1.2带色

头文件:<spdlog/sinks/stdout_color_sinks.h>

工厂方法:

spdlog::stdout_color_mt(std::string const& logger_name);
spdlog::stdout_color_st(std::string const& logger_name);

2.2错误控制台(stderr)记录器

// 不带色
spdlog::stderr_logger_mt(std::string const& logger_name);
spdlog::stderr_logger_st(std::string const& logger_name);

// 带色
spdlog::stderr_color_mt(std::string const& logger_name);
spdlog::stderr_color_st(std::string const& logger_name);

2.3文件记录器

2.3.1 basic_logger单一文件
// 工厂函数
basic_logger_mt(std::string const& logger_name,
		std::string const& filename, //日志文件名
		bool truncate = false //false:追加内容;true:清空文件内容然后写入
		);
basic_logger_st(...);// 参数同上
2.3.2 rotating_file_sink回滚序号多文件
rotating_file_sink_mt(std::string const& logger_name,
			std::string const& filename, //基础文件名
			std::size_t max_size, //单个文件允许的最大字节数
			std::size_t max_files, //最多文件个数
			bool rotate_on_open = false, //首次打开立即滚号
			std::string const& extension //文件拓展名
			);
rotating_file_sink_mt(...);

1752069469110

2.3.3更多文件日志类型
  • daliy_logger_mt/st:按日分割
  • hourly_logger_mt/st:按小时分割

工程最佳实践3:在Linux下,建议使用单一文件+追加策略,而将文件滚动拆分交给OS来实现——

  • 实现更简单:仅需配置;
  • 功能更强:如自动压缩;
  • 更灵活:更换滚动策略而无需修改程序。

2.4文件记录器示例

void functionNeedFileLogger()
{
    auto logger = spdlog::get("file_logger");
    assert(logger);

    logger->info("{} 函数中,通过名字为 {} 的记录器,输出本日志。", __FUNCTION__, logger->name());
}

void spdlogFileLogger()
{
    spdlog::info("这是一个信息日志");

    auto fileLogger = spdlog::basic_logger_mt("file_logger", "logs/log.txt");

    fileLogger->info("这是一个信息日志");
    fileLogger->warn("这是一个警告日志");
    fileLogger->error("这是一个错误日志");
    fileLogger->critical("这是一个关键日志");

    functionNeedFileLogger();
}

日志信息:

[2025-07-09 22:12:06.852] [file_logger] [info] 这是一个信息日志
[2025-07-09 22:12:06.852] [file_logger] [warning] 这是一个警告日志
[2025-07-09 22:12:06.852] [file_logger] [error] 这是一个错误日志
[2025-07-09 22:12:06.852] [file_logger] [critical] 这是一个关键日志
[2025-07-09 22:12:06.852] [file_logger] [info] functionNeedFileLogger 函数中,通过名字为 file_logger 的记录器,输出本日志。

三、日志槽

1.一个记录器搭配多个槽

1752070655995

方法1:

  1. 创建多个槽;注:槽(sink)也用 shared_ptr来管理
  2. 【可选]】清空默认记录器原来的槽;
  3. 把创建的槽,加入(挂接)到默认记录器中。

方法2:

  1. 创建多个槽;
  2. 使用创建的槽,创建一个新的记录器;
  3. 【可选】使用新的记录器,替换全局默认记录器。

工程最佳实践4:在系统启动时,业务服务“窗口”未“开张”之前,在进程的主函数(通常就是在主函数开始),先初始化好日志记录器。换句话来说:不要在多线程语义环境内初始化日志记录器。

工程最佳实践5

  1. 一个记录器同时挂接stdout和stderr两个槽;
  2. 让stdout输出普通信息+出错信息的日志;
  3. 让stderr仅输出出错日志(警告、错误、危急);
  4. 将stdout和stderr分别重定向到文件(因此会有两个日志文件,其中一个是完整日志,一个是仅含错误的日志)。

1752071389604

1752071422239

void spdlogRedirectOutput()
{
    auto stdoutSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto stderrSink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();

    stderrSink->set_level(spdlog::level::warn);

    auto defaultLogger = spdlog::default_logger();
    defaultLogger->sinks().clear();
    defaultLogger->sinks().push_back(stdoutSink);
    defaultLogger->sinks().push_back(stderrSink);

    spdlog::info("这是一个信息日志");
    spdlog::warn("这是一个警告日志");
    spdlog::error("这是一个错误日志");
    spdlog::critical("这是一个关键日志");
}

输出结果:

1752072399830

接下来演示重定向:

在cmd中输入:.\helloSpdlog.exe 1>stdout.txt 2>stderr.txt

其中,“1”代表标准输出,“1>stdout.txt”表示将标准输出重定向到stdout.txt文件(即输出到该文件);“2”代表标准错误输出,表示将错误输出到stderr.txt文件中。

2.多个记录器,多个槽

1752159270097

工程最佳实践6:如何划分logger?

  • 线程:每个工作线程使用一个日志记录器
  • 业务:主业务、后台管理、统计业务、监控业务
  • 层次:
    • 业务层
    • 网络层
    • 设备层
  • 错误做法:按业务流程步骤划分

工程最佳实践7:同一业务跨进程、跨主要实现时,应采用相同的日志记录策略,再通过后台脚本程序,实现合并。

实例:

创建一个项目,记录器与槽的关系如下:

  • 默认(主业务)记录器 -> 带颜色的控制台槽 + 序号回滚文件槽
  • 监控业务记录器 -> 普通控制台槽 + 单一文件槽 + windows系统事件槽

要点:

  1. 槽需要创建为智能指针 shared_ptr<sink>()
  2. 记录器拥有一个 std::vector<T>动态数组,T就是智能指针
  3. spdlog没有为windows系统事件提供记录器工厂函数,需要先创建sink:win_eventlog_sink_mt/st,再加入某个记录器
void multLoggerWithMultSink()
{
    spdlog::info("全局日志记录器即将新增回滚编号文件槽");

    // 1.全局日志记录器 - 颜色控制台槽 + 回滚编号文件槽
    auto rotatingFileSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/rotatingLog.txt", 1024 * 1024 * 2,
        4);
    auto defaultLogger = spdlog::default_logger();
    defaultLogger->sinks().push_back(rotatingFileSink);

    spdlog::info("全局日志记录器已新增回滚编号文件槽");

    for (int i = 0; i < 10000; ++i) {
        spdlog::info("全局日志记录器输出第{}条日志。", i + 1);
        spdlog::warn("全局日志记录器输出第{}条日志。", i + 1);
        spdlog::error("全局日志记录器输出第{}条日志。", i + 1);
    }

    // 2.专门用于监控业务的日志记录器
    auto colorlessOutSink = std::make_shared<spdlog::sinks::stdout_sink_mt>();

    // 创建普通文件槽
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/monitorLog.txt");

// 创建windows OS下使用的槽
#if _WIN32
    auto winEvtSink = std::make_shared<spdlog::sinks::win_eventlog_sink_mt>("HelloSpdlog");
#endif

    // 创建一个全新的日志记录器,取名要能体现其业务
    auto monitorLogger = std::make_shared<spdlog::logger>("MonitorLogger");
    monitorLogger->sinks().push_back(colorlessOutSink);
    monitorLogger->sinks().push_back(fileSink);
#if _WIN32
    monitorLogger->sinks().push_back(winEvtSink);
#endif

    monitorLogger->info("监控日志槽已创建,他有{}个槽。", monitorLogger->sinks().size());

    for (int i = 0; i < 100; ++i) {
        monitorLogger->info("监控日志记录器输出第{}条日志。", i + 1);
        monitorLogger->warn("监控日志记录器输出第{}条日志。", i + 1);
        monitorLogger->error("监控日志记录器输出第{}条日志。", i + 1);
    }
}

效果:

仅展示windows日志系统的效果:

右键单击win,选择事件查看器,在应用程序日志中,可以看到日志信息:

1752162139603

四、日志级别控制

工程最佳实践8:如何确认一条日志的级别?

主观记录:注重为社么

客观记录:注重记什么

1.trace 跟踪

  • 记什么:记录基于(技术)实现的步骤信息(通常偏重记录正确路径)
  • 为社么:明确当前代码的工作机制

2.debug 调试

  • 为什么:找到bug
  • 记什么:任何可能导致bug的原因
  • 注:当前代码的bug完全解决后,删除debug信息

3.info 信息

  • 记什么:记录基于业务的过程信息(里程碑、关键节点、状态)
  • 为什么:帮助系统发现运行状态是正常的

4.warn 警告

  • 为什么:系统的每一笔业务都能正常的执行,但是出现了反常信息
  • 记什么:不影响系统业务进行的每一笔反常信息

5.error 错误

  • 为什么:某一笔业务出错了
  • 记什么:业务执行出错的原因

6.critical 危急

  • 记什么:能够让系统某个功能直接罢工的事件
  • 为什么:错误危害到了系统的正常运行

7.spdlog的级别控制

采用串联过滤:记录器过滤掉低级别的日志后,传递给后续的槽,再由槽进行二次过滤

1752310547946

新建的槽默认使用最低级别trace,所有的记录器和槽都调用 set_level函数设置日志级别,如果需要完全关闭日志,则调用:

logger->set_level(level::off);

同时:

  • level::to_string_view():得到级别枚举值名称
  • level::to_short_c_str():得到大写单字母名称

工程最佳实践9

  • 将槽加入到记录器之前,先设置它的级别;
  • 用常量定义sink的加入顺序,后续如确实需要调整级别,使用该常量下标;
  • sink加入logger后,如需关闭其输出,使用set_level(off),而不是删除sink。

示例:

void setLevelTest()
{
    int const IDX_CONSOLE_SINK = 0;
    int const IDX_FILE_SINK = 1;

    auto levelLogger = spdlog::stdout_color_mt("LevelLogger");

    // 创建文件槽
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/levelTestLog.txt");
    // 设置日志级别
    fileSink->set_level(spdlog::level::warn);
    // 加入到记录器
    levelLogger->sinks().push_back(fileSink);

    // 查看记录器的级别
    levelLogger->info("记录器级别为:{}", spdlog::level::to_string_view(levelLogger->level()));

    // 查看各个槽的级别
    for (auto sink : levelLogger->sinks()) {
        levelLogger->info("槽的级别为:{}", spdlog::level::to_string_view(sink->level()));
    }

    levelLogger->info("记录器输出一条信息日志");
    levelLogger->debug("记录器输出一条调试日志");

    // 调整记录器级别
    levelLogger->set_level(spdlog::level::debug);

    levelLogger->debug("记录器的级别已经调整到了 debug");
    levelLogger->warn("不过 debug 和 info 级别的日志依然不会输出到文件");

    levelLogger->sinks()[IDX_FILE_SINK]->set_level(spdlog::level::debug);
    levelLogger->debug("文件槽的级别也被调整为了 debug");
}

8.日志级别的静态控制

一些实时(通常是近乎实时)的系统,比如各类证券交易系统,核心既必须记录日志,又对日志输出性能有着极为严苛的要求,这时可以考虑将代码中的全部或部分日志记录,改为静态模式。

比如,希望程序以 INFO 级别记录日志,可以先在项目中全局定义以下宏的值:

#define SPDLOG_ACTIVE_LEVEL SPDLOG_LEVEL_INFO

然后,将记录日志的代码,由原来的 spdlog::info( ... )spdlog::err( ... ),改成如下:

auto logger = spdlog::default_logger();

SPDLOG_LOGGER_TRACE(logger, "这是一条跟踪日志");    //1
SPDLOG_LOGGER_DEBUG(logger, "这是一条调试日志");    //2
SPDLOG_LOGGER_INFO(logger, "这是一条信息日志");     //3
SPDLOG_LOGGER_WARN(logger, "这是一条警告日志");     //4
SPDLOG_LOGGER_ERROR(logger, "这是一条出错日志");    //5
SPDLOG_LOGGER_CRITICAL(logger, "这是一条危急日志"); //6

因为此时我们设置的 SPDLOG_ACTIVE_LEVELSPDLOG_LEVEL_INFO,所以,上面代码中,比如 INFO 级别低的 1、2 两行代码(宏),会在编译期间就直接被无视(抛弃),仿佛它们从来没有来到过这人间……

这样做的好处,就是可以保证:不需要输出的日志操作,在代码中完全不存在,从而将它们对性能的负面影响,完全消除。

这样的坏处,也很明显:当你需要调节日志输出级别,你的唯一办法是:修改全局宏 SPDLOG_ACTIVE_LEVEL 的值(SPDLOG_LEVEL_XXX),然后重新编译生成程序。

注意,静态和动态可以混合使用,你可以只在性能超级敏感的模块中,使用静态日志输出。

五、日志格式控制

严格来说,仅是“模式匹配”

1.使用 {}作为占位符

2.自定义输出内容匹配模式(pattern)

logger/sink->set_pattern("格式指定串")

例如:

logger->set_pattern("[%Y年%m月%d日 %H:%M:%S]- 【%l】 %n::%^%v%$");
  • %Y-%m-%d %H:%M:%S:表示日期和时间;
  • %l:表示日志级别;
  • %^%$:用于设置颜色作用在二者之间的内容,仅对控制台有效;
  • %n:记录器名称;
  • %v:原始日志内容。
void setSpdlogFormat()
{
    spdlog::info("原有格式");
    spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);

    auto mainLogger = spdlog::stdout_color_mt("mainLogger");

    mainLogger->set_pattern("[%Y年-%m月-%d日 %H:%M:%S.%e] [%^%l%$] %v");

    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/formatTestLog.txt");
    fileSink->set_pattern("[%Y年-%m月-%d日 %H:%M:%S.%e] [%^%l%$] %v");
    mainLogger->sinks().push_back(fileSink);

    mainLogger->set_level(spdlog::level::trace);

    spdlog::set_default_logger(mainLogger);

    spdlog::info("修改后的格式");
    spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);

    spdlog::warn("服务已在 {}:{} 开始监听");
    spdlog::debug("服务已在 {}:{} 开始监听");
    spdlog::critical("服务已在 {}:{} 开始监听");
    spdlog::error("服务已在 {}:{} 开始监听");
}

效果:

1752502968514

六、异步记录器

1.同步记录器

1752503128290

  1. 同步≠立即写入:虽然是同步模式,在目标有缓冲区,且数据未灌满缓冲区时,日志并未输出到指定位置,仍有可能丢失;
  2. 后续步骤需要等待:同步模式下,必须等到写日志完事后,才能继续后续语句。

为什么不立即写入?

1752503377852

绝大多数情况下,同步写入就足够满足要求。

补丁:

  • 对特定步骤下的关键日志借助 flush()强制输出;
  • 启用 spdlog::flush_every(时长),定期强制输出。
void spdlogBufferFlush()
{
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/bufferFlushLog.txt");
    spdlog::default_logger()->sinks().push_back(fileSink);

    spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);

    std::system("pause");

    spdlog::default_logger()->flush();
    spdlog::info("服务已在 {}:{} 开始监听", "127.0.0.1", 8080);
}

2.异步记录器

1752504109602

1752504299140

相比于同步,异步记录的劣势:

  • 日志记录的延迟更大(更长的路径,包括线程切换);
  • 如发生程序意外退出,丢失的日志内容会更多;
  • 占用更多的内存;
  • 更复杂的参数调节。

优势:

  • 大压力下,对业务线程(日志生产者)的负面影响更低

工程最佳实践10

  • 优先使用同步日志记录器。当实际日志量超过1w条/秒时,再考虑使用异步记录模式,并通过数据对比;
  • 主要对比数据:1.业务自身的处理能力能否得到提升?2.每秒日志记录量是否显著提升?
  • 异步参数调参依据:业务线程cpu占用率是否下降?积压日志的任务是否容易超限?进程内存是否吃得住?