010-spdlog:一篇搞懂C++日志编程-C++开源库108杰

  • 六个小节
  • 六段视频
  • 十个练习
  • 十个项目最佳实践

从入门到精通,一节课搞定 C++ 日志编程

1. 简介、安装、测试

Windows msys2 下安装( 以 urct64 环境为例):

pacman -S mingw-w64-ucrt-x86_64-spdlog

UBUNTU Linux:

sudo apt install libspdlog-dev

苹果 MacOS

Homebrew:brew install spdlog 
//或:
MacPorts:sudo port install spdlog

spdlog 本身可直接使用头文件使用(相当于静态库,但每次编译会比较拖速度),也可使用安装时编译好的动态库(但视频中介绍到 SPDLOG_WCHAR_TO_UTF8_SUPPORT 无法对已经编译好的库发挥作用 )。

  • 视频1: spdlog 简介、安装与试用

015-spdlog-1.简介与试用

2. 独立使用日志记录器

spdlog 库中最重要的两个概念:日志记录器(logger,也称日志对象) 和 槽(sink)。日常输出日志,主要通过 logger 来实现。

  • 视频2:  spdlog 独立使用日志记录器

016-spdlog-2.日志记录器

3. 多个记录器,多个槽

将一行日志,同时输出到多个目标位置(屏幕、文件、网络等),是日志编程的最佳实践,在 spdlog 中,该功能被实现为:一个日志记录器(logger),可以挂接多个槽(sinks)。

那么,同一个程序中,什么时候需要使用多个日志记录器呢?

  • 视频3: 多个记录器,多个槽

017-spdlog-3.日志槽

4. 日志级别控制

记住:最重要的控制,来自对程序员的控制,因为,代码中所有日志,都是程序员写的。源头没控制好,后面再多技术手段也救不了。

首先要理解什么叫日志的 “主观记录” ,什么叫 “客观记录”。

  • 视频4: 日志级别控制

018-spdlog-4.日志级别控制

  • 各级日志的 WHAT 与 WHY
等级偏向WHATWHY
trace客观记录记录基于实现的步骤信息(偏重记录正确路径)展现代码的工作机制
debug主观记录只要有助于排查,记什么都行排查特定BUG
info客观记录基于业务的过程信息展现业务的日常运转过程
warn主观记录暂不影响系统运行的反常信息提前发现问题
err客观记录出错信息及时发现问题
critical客观记录会千万特定功能甚至系统整体失效的问题补救
  • 静态控制

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

比如,希望程序以 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_LEVEL 是 SPDLOG_LEVEL_INFO,所以,上面代码中,比如 INFO 级别低的 1、2 两行代码(宏),会在编译期间就直接被无视(抛弃),仿佛它们从来没有来到过这人间……

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

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

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

5. 日志格式控制

想对日志输出内容与格式做完全控制(或彻底的定制),需派生 spdlog 的 format 接口,不过,多数情况下,我们就是使用 spdlog 默认提供的内容与格式。默认日志记录器输出的内容,长这样子:

[2025-06-24 00:07:32.644] [info] 服务器已在 36.251.248.218 : 80 开始监听

包含时间、等级等。偶尔想调整,只需使用日志记录器或槽的 set_pattern() 方法即可。

  • 视频5: 日志格式控制

019-spdlog-5.日志格式控制

  • 常用格式控制串
    • %Y-%m-%d %H:%M:%S:表示日期和时间。​
    • %l:%l表示日志级别
    • %^%$:用于设置颜色作用在二者之间的内容,仅对控制台有效,截止1.15.2 版本,只能用一次
    • %n:记录器名称(如前所述,通常取业务或层次名称)
    • %v:原始日志内容

6. 异步记录器

在使用之前,请先了解异步记录器的短处:

  1. 异步记录时,日志记录的延迟更大(从日志产生到目标位置,异步记录包含更长的执行路径,包括队列操作、线程切换、线程间数据交换等费时操作);
  2. 如发生程序意外退出,丢失的日志可能更多(队列 + 系统缓冲区间的内容);
  3. 占用更多的内存(队列中积压的日志);
  4. 更复杂的参数优化调节:(后台线程池三大参数、运行环境性能配置、业务负载)。

异步记录这么多缺点?所以,通常我们就使用同步模式,但是,同步模式能满足性要求吗?什么情况下适合切换到异步?切换前后,需要注意哪些事项?

020-spdlog-5.异步日志对象

7. 代码

  • CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

project(helloSpdlog VERSION 0.1.0 LANGUAGES CXX C)

set(CMAKE_CXX_STANDARD 17)
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)
  • main.cpp
#include <cassert>
#include <cstdlib>
#include <iostream>
#include <chrono>

#include <spdlog/spdlog.h>

#include <spdlog/sinks/stdout_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/win_eventlog_sink.h>

#include <spdlog/async.h>

void main1_HelloSpdLog()
{
    char const* lib7 = "spdlog";
    spdlog::info("Hello {} !", lib7); // Hello spdlog!
}

void main2_替换全局日志记录器()
{
    spdlog::info("替换之前,等级显示是带颜色的!");
    spdlog::warn("这是警告!,颜色应该更‘娇艳’!");

    // 创建新的记录器(不带颜色的标准输出)
    auto colorlessLogger = spdlog::stdout_logger_mt("Colorless");

    spdlog::set_default_logger(colorlessLogger);

    // 用新的全局记录器输出:
    spdlog::info("替换之后,等级显示不带颜色!");
    spdlog::warn("这是警告!,一切都很清心寡欲。");
}

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

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

void main3_使用文件日志记录器()
{
    spdlog::info("下面有些内容,我们只输出到日志文件");

    auto fileLogger = spdlog::basic_logger_mt("FileLogger", "log/file-log.txt");

    fileLogger->warn("完蛋,用户太帅了,我有心动的感觉!");

    spdlog::error("无法识别人脸,用户,请正面面对摄像头!");

    functionNeedFileLogger();
}

void main4_重定向标准输出()
{
    // 创建标准输出(带彩色)槽
    auto stdoutSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

    // 创建一个标准错误输出(带彩色)槽
    auto stderrSink = std::make_shared<spdlog::sinks::stderr_color_sink_mt>();

    // 设置 stderr 槽只输出警告及以上级别的日志(建议:在挂接之前设置好)
    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("1、我们一定要团结一致!");
    spdlog::error("2、公司人事真是大聪明!连续安排程序员三个周末都加班?!");
    spdlog::info("3、今天天气,哈哈哈~");
    spdlog::critical("4、今天老板娘和老板在办公室打起来了!");
}

void main5_多个记录器()
{
    spdlog::info("全局日志记录器即将新增回滚编号文件槽");

    // 1. 全局日志记录器 - 颜色控制台槽 + 回滚编号文件槽
    auto rotatingFileSink = std::make_shared<spdlog::sinks
		::rotating_file_sink_mt>("log/main-rotating.txt"
                , 1024 * 1024 * 5, 9);

    // 取默认记录器
    auto defaultLogger = spdlog::default_logger();
    defaultLogger->sinks().push_back(rotatingFileSink); // 加入新槽

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

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

    // 创建普通文件槽
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log/monitor.txt");
    
    #ifdef _WIN32
    // 创建 Windows OS 的事件记录槽
    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(colornessOutSink);
    monitorLogger->sinks().push_back(fileSink);

    #ifdef _WIN32
    monitorLogger->sinks().push_back(winEvtSink);
    #endif

    monitorLogger->info("监控日志记录器已经就绪!它有 {} 个槽", monitorLogger->sinks().size());
}

void main6_日志级别调整()
{
    int const IDX_CONSOLE_SINK = 0; // 控制台槽的下标
    int const IDX_FILE_SINK = 1; // 文件槽的下标

    auto levelsLogger = spdlog::stdout_color_mt("LevelsLogger");

    // 创建文件槽
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log/levels.txt");
    // 修改文件槽的级别:
    fileSink->set_level(spdlog::level::warn); 
    // 加入到记录器
    levelsLogger->sinks().push_back(fileSink);

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

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

    levelsLogger->info("这行日志只会在屏幕显示");
    levelsLogger->debug("这行调试日志,在屏幕和文件都不会显示");

    // 先调整记录器的级别 到 debug 
    levelsLogger->set_level(spdlog::level::debug); 

    levelsLogger->debug("本记录器等级已调整为 debug");
    levelsLogger->warn("不过,debug 和 info 级别的日志仍然不会输出到文件");

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

void main7_修改Pattern()
{
    spdlog::info("原有格式");
    spdlog::info("服务已经在 {} : {} 开始监听", "36.251.248.218", 8090);
    spdlog::info("开始修改 Pattern");

    // 先创建一个带颜色的控制台日志记录器
    auto mainLogger = spdlog::stdout_color_mt("主站");

    // 修改它的 Pattern
    mainLogger->set_pattern("[%Y年%m月%d日 %H:%M:%S]-%^〚%l〛%n::%v%$");

    // 创建一个文件 sink 
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
		"log/pattern.txt");
    fileSink->set_pattern("[%Y-%m-%d %H:%M:%S] >%l< [%n] %v");
    mainLogger->sinks().push_back(fileSink);

    // 调整为最低级别
    mainLogger->set_level(spdlog::level::trace);

    // 取代默认
    spdlog::set_default_logger(mainLogger);

    spdlog::info("自定义格式起作用了!");
    spdlog::info("服务已经在 {} : {} 开始监听", "36.251.248.218", 8090);
    spdlog::warn("服务器感觉有点卡卡的");
    spdlog::error("服务器无法连接数据库了!");
    spdlog::critical("糟糕,好像机房着火了!!!");
    spdlog::trace("再见,我先走了");
}

void main8_flush缓冲区()
{
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
            "log/flush.txt");
    spdlog::default_logger()->sinks().push_back(fileSink);

    spdlog::info("写入日志,请对比屏幕输出和 flush.txt 的内容");
    
    std::system("pause");

    spdlog::default_logger()->flush();
    spdlog::info("已经强制刷新日志缓冲区,请重新观察");

    std::system("pause");
}

void main9_flush_every缓冲区()
{
    // 每三秒强制清空缓冲区一次
    spdlog::flush_every(std::chrono::seconds(3));

    auto fileSink = std::make_shared<
        spdlog::sinks::basic_file_sink_mt>("log/flush_every.txt");
    spdlog::default_logger()->sinks().push_back(fileSink);

    spdlog::info("写入日志,请打开 flush_every.txt 并静待 3 秒");
    spdlog::info("写入日志,请打开 flush_every.txt 并静待 2 秒");
    spdlog::info("写入日志,请打开 flush_every.txt 并静待 1 秒");

    std::system("pause");
}

void main10_异步日志记录器()
{
    spdlog::init_thread_pool(1025 * 10, 1); 

    // 为异步日志记录器的工厂类型,取简短的别名:
    //using async_factory = 
	spdlog::async_factory_impl<spdlog::async_overflow_policy::block>;
    //using async_factory_nb = 
	spdlog::async_factory_impl<spdlog::async_overflow_policy::overrun_oldest>;

    // 创建带颜色的控制台日志记录器-使用指定异步工厂
    // 非堵塞:async_factory_nonblock
    auto asyncColorLogger = spdlog::stdout_color_mt<spdlog::async_factory>("AsyncLogger"); 

    ////// 下面的操作(记录日志),就跟同步的日志记录器完全一样了 ///////

    // 创建文件 sink 
    auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>(
		"log/async-file.txt");
    asyncColorLogger->sinks().push_back(fileSink);

    // 替换默认:
    spdlog::set_default_logger(asyncColorLogger);

    spdlog::info("异步日志记录器已经就绪!,它有 {} 个 槽"
		, spdlog::default_logger()->sinks().size());
}

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

    main1_HelloSpdLog();
    // main2_替换全局日志记录器();
    // main3_使用文件日志记录器();
    // main4_重定向标准输出();
    // main5_多个记录器();
    // main6_日志级别调整();
    // main7_修改Pattern();
    // main8_flush缓冲区();
    // main9_flush_every缓冲区();
    // main10_异步日志记录器();
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南郁

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值