spdlog 实战:C++ 工业边缘的高性能日志

C++ 项目里,日志往往是排障时唯一可靠的线索。spdlog 是目前最主流的 C++ 日志库——header-only、性能高、API 友好。本文用可直接落地的代码,走一遍从安装到异步、多 sink、结构化输出的完整实战。

一、为什么选 spdlog

  • 高性能:格式化基于 fmt,速度快;支持异步模式,业务热路径几乎零阻塞。
  • 易用:header-only,spdlog::info(...) 一行即可用,API 直观。
  • 功能完整:多 sink、按大小/按天滚动、自定义 pattern、结构化输出一应俱全。
  • 生态活跃:社区维护活跃,版本迭代稳定,文档齐全。

二、安装与集成

vcpkg install spdlog
# CMake
find_package(spdlog REQUIRED)
target_link_libraries(myapp PRIVATE spdlog::spdlog)

三、基础用法

#include <spdlog/spdlog.h>

int main() {
    spdlog::info("Hello {}", "world");
    spdlog::warn("warning");
    spdlog::error("error {}", 42);

    spdlog::set_level(spdlog::level::debug); // 默认 info,需显式放开 debug
    spdlog::debug("debug detail");

    return 0;
}

trace / debug / info / warn / error / critical 六个级别,通过 set_level 控制输出下限。

四、格式化输出

spdlog::info("device {} voltage {:.2f}V", "dev_001", 220.5);
spdlog::warn("high latency: {:>5}ms", 1234);
spdlog::error("failed: {} attempts", 3);

// 自定义类型:特化 fmt::formatter
template <>
struct fmt::formatter<Device> {
    constexpr auto parse(format_parse_context& ctx) { return ctx.begin(); }
    template <typename FormatContext>
    auto format(const Device& d, FormatContext& ctx) const {
        return fmt::format_to(ctx.out(), "Device({})", d.id);
    }
};

spdlog 内置了 fmt,直接特化 fmt::formatter 即可让自定义类型参与格式化。若项目额外链接了外部 fmt,注意版本要与 spdlog 自带版本保持一致,否则容易编译冲突。

五、多 sink

一条日志同时写控制台和文件:

#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/daily_file_sink.h>

auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
auto file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "logs/edge.log", 10485760, 5   // 单文件 10MB,最多保留 5 份
);

std::vector<spdlog::sink_ptr> sinks{console, file};
auto logger = std::make_shared<spdlog::logger>("edge", sinks.begin(), sinks.end());
spdlog::register_logger(logger);
spdlog::set_default_logger(logger);

多线程务必用 *_mt 结尾的 sink,*_st 版本非线程安全。

六、异步日志

#include <spdlog/async.h>

spdlog::init_thread_pool(8192, 1); // 队列 8192,1 个后台线程
auto async = spdlog::create_async_nb<spdlog::sinks::rotating_file_sink_mt>(
    "edge_async",
    "logs/edge.log",
    10485760,
    5
);
async->info("async log");
spdlog::flush_every(std::chrono::seconds(3)); // 每 3 秒定时刷盘

create_async_nb(non-blocking)在队列满时会丢弃新日志、不阻塞业务;如果不能丢日志,用 create_async,队列满时会阻塞等待。工业边缘的高频热路径通常选前者。

七、自定义 pattern

spdlog::set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%t] [%n] %v");

// JSON 格式,便于日志采集/检索
spdlog::set_pattern(
    R"({"time":"%Y-%m-%dT%H:%M:%S.%e%z","level":"%l","thread":%t,"logger":"%n","msg":"%v"})"
);

%^ %$ 之间是着色区间,%l 级别、%t 线程、%n logger 名、%v 消息体。用 JSON pattern 时注意:消息里若含引号等特殊字符,需要在业务侧自行转义,否则会破坏 JSON。

八、多 logger 管理

按模块(如 Modbus、IEC 104)分别落到独立文件:

class LogManager {
public:
    static void init() {
        auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();

        auto modbus = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
            "logs/modbus.log", 10485760, 5
        );
        auto modbus_logger = std::make_shared<spdlog::logger>(
            "modbus", spdlog::sinks_init_list{console, modbus}
        );

        auto iec104 = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
            "logs/iec104.log", 10485760, 5
        );
        auto iec104_logger = std::make_shared<spdlog::logger>(
            "iec104", spdlog::sinks_init_list{console, iec104}
        );

        spdlog::register_logger(modbus_logger);
        spdlog::register_logger(iec104_logger);
    }
};

auto modbus = spdlog::get("modbus");
modbus->info("read holding register");

九、业务实践(工业边缘场景)

实践 1:统一结构化格式
关键字段(设备号、点位、耗时)固定顺序输出,方便后续被采集端解析和检索。

实践 2:按模块分 sink
协议、采集、上云各写各的文件,互不淹没,定位问题时能快速缩小范围。

实践 3:热路径走异步
高频采集与协议报文用异步 logger,避免磁盘 IO 阻塞主循环。

实践 4:覆盖测试
对日志初始化、文件滚动、异步丢弃等行为做单元测试与压力测试,别等上线才发现丢日志。

实践 5:分级与留存
线上默认 info,现场排障时临时切 debug;文件按大小滚动 + 保留 N 份,控制磁盘占用。

十、几个常见的坑

坑 1:同步阻塞
默认同步 sink 在高频写入时会阻塞业务线程。
应对:热路径改用异步(init_thread_pool + create_async_nb)。

坑 2:忘记 flush
异步/缓冲模式下崩溃,可能丢掉最后一段日志。
应对:flush_on(spdlog::level::err) + flush_every 定时刷盘。

坑 3:sink 用错线程版本
多线程误用 *_st sink 会踩数据竞争。
应对:统一使用 *_mt 版本。

坑 4:fmt 版本不一致
spdlog 自带 fmt 与项目外部 fmt 版本冲突会编译报错。
应对:优先使用 spdlog 内置 fmt,或锁定版本。

坑 5:日志过量
DEBUG 全开会拖慢系统、撑爆磁盘。
应对:合理分级 + 采样 + 滚动保留策略。

十一、运行时层面的角色

在工业边缘,日志往往要和协议运行时一起设计。以协议运行时 Zenova EdgeOS 为例,其日志实践通常包括:按协议/设备分模块输出、热路径异步、结构化字段便于上云采集、崩溃前刷盘以保证现场可追溯。这种「运行时 + 日志」一体化的做法,比单独接一个日志库更容易在现场把问题定位清楚。

十二、TL;DR

C++ 工业边缘用 spdlog:

  • 基础info / warn / error 等级别宏,set_level 控制下限。
  • 格式化:fmt 风格占位符 + 自定义 formatter
  • 多 sink:console + rotating + daily。
  • 异步init_thread_pool + create_async_nb
  • patternset_pattern,可输出 JSON。
  • 多 loggerregister_logger + get
  • 实践:结构化格式 / 分模块 sink / 异步 / 测试 / 分级留存。
  • :同步阻塞 / 忘 flush / sink 线程版本 / fmt 版本 / 日志过量。

下一步建议

  1. 先跑通基础用法
  2. 按模块拆分多 sink
  3. 热路径接入异步
  4. 完整集成 + 测试
  5. 制定长期的分级与留存规范

如果想看协议运行时里「日志 + 采集 + 上云」怎么一体化落地,可以参考 Zenova EdgeOS 的运行时日志方案

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值