📚 系列:C++ 工程实践 第 02 篇 ⏱ 阅读约 9 分钟
📌 一句话总结
本文用「先跑通 → 再拆解 → 后封装」三步讲透 spdlog,读完后你能:① 5 分钟跑通异步彩色控制台日志;② 掌握 7 个级别、pattern 格式与多种 sink;③ 拿到一份可直接落地的 Logger 封装(单例 + 宏 + 双 sink + 异步)。
文章目录
-
- [1. 为什么需要日志:日志是排障的第一现场](#1. 为什么需要日志:日志是排障的第一现场)
- [2. spdlog 是什么:高性能是它的第一标签](#2. spdlog 是什么:高性能是它的第一标签)
- [3. 环境准备与最小示例:先让日志跑起来](#3. 环境准备与最小示例:先让日志跑起来)
- [4. 日志级别与格式化:让日志有「可读性」](#4. 日志级别与格式化:让日志有「可读性」)
-
- [4.1 七个级别](#4.1 七个级别)
- [4.2 格式化 pattern](#4.2 格式化 pattern)
- [5. Sink 与异步日志:一份日志,多个去处](#5. Sink 与异步日志:一份日志,多个去处)
-
- [5.1 常用 sink](#5.1 常用 sink)
- [5.2 多 sink 组合](#5.2 多 sink 组合)
- [5.3 异步日志](#5.3 异步日志)
- [6. 二次封装:把 spdlog 变成项目里「敢直接用」的组件](#6. 二次封装:把 spdlog 变成项目里「敢直接用」的组件)
- [7. 落地与总结:日志策略比 API 更重要](#7. 落地与总结:日志策略比 API 更重要)
1. 为什么需要日志:日志是排障的第一现场
线上出问题时,你能依赖的东西只有三样:监控指标、链路追踪、日志。前两个告诉你「哪里不对」,日志告诉你「为什么不对」。
- 调试:本地断点能解决的事,到了生产就只能靠日志。
- 排查:用户说「下单失败」,日志里能看到入参、下游返回、耗时。
- 监控:把 error 日志接进告警,比等用户投诉早半小时。
常见 C++ 日志库怎么选:
| 库 | 依赖方式 | 性能 | 异步 | 上手成本 | 适合场景 |
|---|---|---|---|---|---|
| spdlog | header-only 或编译库 | 高 | 原生支持 | 低 | 大多数 C++ 项目 |
| glog | 需编译 | 中 | 无(要自己包) | 中 | Google 系老项目 |
| log4cpp | 需编译 | 较低 | 有限 | 中 | 传统企业项目 |
| Boost.Log | 依赖 Boost | 中 | 支持 | 高 | 已重度使用 Boost 的项目 |
选 spdlog 的理由很直接:头文件即用、fmt 风格格式化、内置异步、sink 可插拔。小工具不需要额外依赖,大服务也扛得住。
ℹ️ 版本说明
本文以 spdlog 1.x 为例(1.9 及以上均可编译)。不同发行版打包方式不同,链接参数见第 3 节。
2. spdlog 是什么:高性能是它的第一标签
spdlog 是一个 header-only 的 C++ 日志库(也可以编译成静态/动态库使用),主打「快」和「够用」。
| 特点 | 说明 | 对你的价值 |
|---|---|---|
| 高性能 | 异步 + 无锁队列 + 编译期格式检查 | 高频日志不拖慢业务 |
| 易用 | spdlog::info("x={}", 1) 一行输出 |
不用自己拼字符串 |
| 多 sink | 控制台、文件、轮转、按天、网络等 | 一份日志多处落地 |
| 异步日志 | 后台线程写盘,业务线程只入队 | 高并发场景友好 |
| 线程安全 | _mt 后缀的 sink/logger 支持多线程 |
不用自己加锁 |
| 格式化 | 基于 fmt,{} 做参数替换 + pattern |
输出统一、可读 |
它的结构可以一句话概括:logger 负责收日志,sink 负责写到哪里,异步队列负责削峰。
#mermaid-svg-7MwLFGKk3t0qik1D{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7MwLFGKk3t0qik1D .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7MwLFGKk3t0qik1D .error-icon{fill:#552222;}#mermaid-svg-7MwLFGKk3t0qik1D .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7MwLFGKk3t0qik1D .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7MwLFGKk3t0qik1D .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7MwLFGKk3t0qik1D .marker.cross{stroke:#333333;}#mermaid-svg-7MwLFGKk3t0qik1D svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7MwLFGKk3t0qik1D p{margin:0;}#mermaid-svg-7MwLFGKk3t0qik1D .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7MwLFGKk3t0qik1D .cluster-label text{fill:#333;}#mermaid-svg-7MwLFGKk3t0qik1D .cluster-label span{color:#333;}#mermaid-svg-7MwLFGKk3t0qik1D .cluster-label span p{background-color:transparent;}#mermaid-svg-7MwLFGKk3t0qik1D .label text,#mermaid-svg-7MwLFGKk3t0qik1D span{fill:#333;color:#333;}#mermaid-svg-7MwLFGKk3t0qik1D .node rect,#mermaid-svg-7MwLFGKk3t0qik1D .node circle,#mermaid-svg-7MwLFGKk3t0qik1D .node ellipse,#mermaid-svg-7MwLFGKk3t0qik1D .node polygon,#mermaid-svg-7MwLFGKk3t0qik1D .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7MwLFGKk3t0qik1D .rough-node .label text,#mermaid-svg-7MwLFGKk3t0qik1D .node .label text,#mermaid-svg-7MwLFGKk3t0qik1D .image-shape .label,#mermaid-svg-7MwLFGKk3t0qik1D .icon-shape .label{text-anchor:middle;}#mermaid-svg-7MwLFGKk3t0qik1D .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7MwLFGKk3t0qik1D .rough-node .label,#mermaid-svg-7MwLFGKk3t0qik1D .node .label,#mermaid-svg-7MwLFGKk3t0qik1D .image-shape .label,#mermaid-svg-7MwLFGKk3t0qik1D .icon-shape .label{text-align:center;}#mermaid-svg-7MwLFGKk3t0qik1D .node.clickable{cursor:pointer;}#mermaid-svg-7MwLFGKk3t0qik1D .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7MwLFGKk3t0qik1D .arrowheadPath{fill:#333333;}#mermaid-svg-7MwLFGKk3t0qik1D .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7MwLFGKk3t0qik1D .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7MwLFGKk3t0qik1D .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7MwLFGKk3t0qik1D .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7MwLFGKk3t0qik1D .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7MwLFGKk3t0qik1D .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7MwLFGKk3t0qik1D .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7MwLFGKk3t0qik1D .cluster text{fill:#333;}#mermaid-svg-7MwLFGKk3t0qik1D .cluster span{color:#333;}#mermaid-svg-7MwLFGKk3t0qik1D div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7MwLFGKk3t0qik1D .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7MwLFGKk3t0qik1D rect.text{fill:none;stroke-width:0;}#mermaid-svg-7MwLFGKk3t0qik1D .icon-shape,#mermaid-svg-7MwLFGKk3t0qik1D .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7MwLFGKk3t0qik1D .icon-shape p,#mermaid-svg-7MwLFGKk3t0qik1D .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7MwLFGKk3t0qik1D .icon-shape .label rect,#mermaid-svg-7MwLFGKk3t0qik1D .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7MwLFGKk3t0qik1D .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7MwLFGKk3t0qik1D .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7MwLFGKk3t0qik1D :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务代码
logger
console sink
rotating file sink
async queue
后台线程写盘
图 1:spdlog 的 logger / sink / 异步队列关系
适用场景:后端服务、客户端、日志采集、压测工具、需要长期保留现场的任何程序。
3. 环境准备与最小示例:先让日志跑起来
3.1 安装
| 系统 / 方式 | 命令 |
|---|---|
| Ubuntu / Debian | sudo apt install libspdlog-dev |
| CentOS / RHEL | sudo yum install spdlog-devel |
| macOS | brew install spdlog |
| Windows | vcpkg install spdlog |
| header-only | 下载源码,把头文件目录加进 include 路径即可 |
CMake 工程里最省事:find_package(spdlog CONFIG REQUIRED),然后链接 spdlog::spdlog 目标。
cmake
cmake_minimum_required(VERSION 3.16)
project(log_demo CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(spdlog CONFIG REQUIRED)
add_executable(log_demo main.cc)
target_link_libraries(log_demo PRIVATE spdlog::spdlog)
不用 CMake 时直接 g++:
bash
g++ -std=c++17 main.cc -o log_demo -lspdlog -lfmt -pthread
⚠️ 注意
发行版的 spdlog 常依赖外部 fmt,需要加 -lfmt;如果链接报找不到 fmt,说明该版本内置 fmt,去掉 -lfmt 即可。header-only 模式则不需要链接 spdlog。
3.2 最小示例:stdout.cc
先看一个「能跑」的版本,它把异步、彩色控制台、级别、格式全用上了:
cpp
// stdout.cc
#include <spdlog/async.h>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
int main() {
spdlog::init_thread_pool(8192, 1); // ① 异步线程池
auto logger = spdlog::stdout_color_mt<spdlog::async_factory>("stdout_logger"); // ② 异步彩色控制台
logger->set_level(spdlog::level::err); // ③ 只输出 err 及以上
logger->set_pattern("[%H:%M:%S][%-7l]: %v"); // ④ 自定义格式
for (int i = 0; i < 3; ++i) {
logger->error("hello world - {}", i); // ⑤ 输出日志
}
spdlog::shutdown(); // ⑥ 退出前刷盘
return 0;
}
编译运行:
bash
g++ -std=c++17 stdout.cc -o stdout_demo -lspdlog -lfmt -pthread
./stdout_demo
输出:
text
[21:35:02][error ]: hello world - 0
[21:35:02][error ]: hello world - 1
[21:35:02][error ]: hello world - 2
3.3 逐行拆解
| 行 | 代码 | 作用 | 不写会怎样 |
|---|---|---|---|
| ① | init_thread_pool(8192, 1) |
建 8192 长度的异步队列 + 1 个后台线程 | 有默认池也能跑,但队列大小不可控 |
| ② | stdout_color_mt<async_factory> |
创建异步 + 多线程安全的彩色控制台 logger | 默认创建同步 logger,性能差一截 |
| ③ | set_level(level::err) |
只放行 err / critical | 默认 info,会刷屏 |
| ④ | set_pattern("[%H:%M:%S][%-7l]: %v") |
时间 + 级别左对齐 + 内容 | 使用默认格式,信息少且不好对齐 |
| ⑤ | logger->error("...{}", i) |
fmt 风格格式化输出 | 手写 sprintf 容易越界 |
| ⑥ | spdlog::shutdown() |
等待队列写完、释放资源 | 异步模式下最后几条日志可能丢 |
✅ 最佳实践
正式项目里 shutdown 放进程退出路径上(或 RAII 守卫),保证异步队列刷干净再退出。
4. 日志级别与格式化:让日志有「可读性」
4.1 七个级别
spdlog 的级别从低到高,设置某个级别后,低于它的日志会被直接丢弃:
| 级别 | 典型内容 | 建议开启环境 |
|---|---|---|
trace |
函数进出、变量快照 | 仅本地排障,短时间开 |
debug |
分支细节、配置加载 | 开发 / 预发 |
info |
服务启动、关键业务节点 | 生产默认 |
warn |
可恢复异常、慢查询 | 生产 |
err |
错误,功能受影响 | 生产 + 告警 |
critical |
致命错误,服务不可用 | 生产 + 告警 |
off |
全部关闭 | 压测、性能测试 |
示例里 set_level(spdlog::level::err),意味着 info / debug / warn 都不会输出------这是演示「级别过滤」,生产里通常默认 info,排查时临时调到 debug。
cpp
logger->set_level(spdlog::level::debug); // 临时排查,运行时可改
logger->info("now you can see me");
4.2 格式化 pattern
pattern 决定日志长什么样,常用字段:
| 字段 | 含义 | 示例输出 |
|---|---|---|
%Y-%m-%d |
日期 | 2026-10-19 |
%H:%M:%S |
时分秒 | 21:35:02 |
%e |
毫秒 | 481 |
%l |
级别名 | error |
%-7l |
级别名左对齐、宽度 7 | error |
%v |
日志内容 | hello world - 0 |
%t |
线程 id | 12345 |
%n |
logger 名 | stdout_logger |
三种常用格式,按场景选:
cpp
// 1) 极简控制台:看时间、级别、内容
logger->set_pattern("[%H:%M:%S][%-7l]: %v");
// 2) 生产文件:精确到毫秒 + 线程 + 级别
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] [%l] %v");
// 3) 多服务:把 logger 名也带上
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [%t] [%l] %v");
[%Y-%m-%d %H:%M:%S.%e] [%t] [%l] %v 的输出长这样:
text
[2026-10-19 21:35:02.481] [12345] [info] service started, pid=1234
[2026-10-19 21:35:02.482] [12345] [warn] slow query: 1200 ms
[2026-10-19 21:35:02.483] [12345] [error] connect mysql failed, retry=3
5. Sink 与异步日志:一份日志,多个去处
5.1 常用 sink
| sink | 写到哪里 | 关键参数 |
|---|---|---|
stdout_color_sink_mt |
控制台(彩色) | 无 |
basic_file_sink_mt |
单个文件追加 | 文件名、是否截断 |
rotating_file_sink_mt |
按大小轮转 | 文件名、单文件大小、保留个数 |
daily_file_sink_mt |
按天轮转 | 文件名、时、分 |
null_sink_mt |
直接丢弃 | 压测屏蔽日志用 |
每个 sink 可以单独设级别,实现「控制台少、文件全」:
cpp
auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console->set_level(spdlog::level::warn); // 控制台只看 warn 及以上
auto file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
"logs/app.log", 20 * 1024 * 1024, 5); // 20MB 一个,保留 5 个
file->set_level(spdlog::level::trace); // 文件记全量
5.2 多 sink 组合
把多个 sink 塞进同一个 logger:
cpp
std::vector<spdlog::sink_ptr> sinks{console, file};
auto logger = std::make_shared<spdlog::logger>("app", sinks.begin(), sinks.end());
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] [%l] %v");
spdlog::register_logger(logger); // 注册后可用 spdlog::get("app")
5.3 异步日志
同步日志在业务线程里直接格式化 + 写盘,日志一多就卡业务;异步日志把「写」交给后台线程:
cpp
spdlog::init_thread_pool(8192, 1); // 队列长度 8192,1 个后台线程
auto logger = std::make_shared<spdlog::async_logger>(
"app",
sinks.begin(), sinks.end(),
spdlog::thread_pool(),
spdlog::async_overflow_policy::block); // 队列满时阻塞,不丢日志
logger->flush_on(spdlog::level::err); // 错误日志立即刷盘
队列满时怎么办,取决于业务:
| 策略 | 行为 | 适合 |
|---|---|---|
block |
阻塞业务线程,等队列有空位 | 业务日志,不能丢 |
overrun_oldest |
覆盖最旧的日志 | 高频指标,允许丢一部分 |
discard_new |
丢弃新日志 | 尽力而为的调试日志 |
🚫 不要无脑选 block
异步队列设得太小、又用 block,高并发下业务线程会被日志反压拖住。先估算峰值日志量,再决定队列大小和溢出策略。
6. 二次封装:把 spdlog 变成项目里「敢直接用」的组件
直接在每个文件里写 spdlog::info(...) 的问题:sink 配置散落、格式不统一、换库要全项目改。做一层薄封装就能解决。
设计目标:
| 目标 | 做法 |
|---|---|
| 统一入口 | 单例 Logger + LOG_* 宏 |
| 双通道 | 控制台 warn 以上 + 文件 trace 以上 |
| 异步 | init_thread_pool + async_logger |
| 格式统一 | 初始化时 set_pattern 一次 |
| 可配置 | Init(service, log_dir, level) |
| 安全退出 | 提供 Shutdown() 刷盘 |
| 未初始化兜底 | 返回默认 logger,避免空指针 |
6.1 接口:logger.h
cpp
#pragma once
#include <memory>
#include <string>
#include <spdlog/spdlog.h>
namespace cm {
class Logger {
public:
static Logger& Instance();
// service 用于文件名,log_dir 用于落盘目录
void Init(const std::string& service,
const std::string& log_dir,
spdlog::level::level_enum level = spdlog::level::info);
// 未初始化时返回默认 logger,保证 LOG_* 不会空指针
std::shared_ptr<spdlog::logger> Get() const;
void Shutdown();
private:
Logger() = default;
std::shared_ptr<spdlog::logger> logger_;
};
} // namespace cm
#define CM_LOG_TRACE(...) ::cm::Logger::Instance().Get()->trace(__VA_ARGS__)
#define CM_LOG_DEBUG(...) ::cm::Logger::Instance().Get()->debug(__VA_ARGS__)
#define CM_LOG_INFO(...) ::cm::Logger::Instance().Get()->info(__VA_ARGS__)
#define CM_LOG_WARN(...) ::cm::Logger::Instance().Get()->warn(__VA_ARGS__)
#define CM_LOG_ERROR(...) ::cm::Logger::Instance().Get()->error(__VA_ARGS__)
#define CM_LOG_CRITICAL(...) ::cm::Logger::Instance().Get()->critical(__VA_ARGS__)
6.2 实现:logger.cc
cpp
#include "logger.h"
#include <spdlog/async.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <vector>
namespace cm {
Logger& Logger::Instance() {
static Logger instance;
return instance;
}
void Logger::Init(const std::string& service,
const std::string& log_dir,
spdlog::level::level_enum level) {
spdlog::init_thread_pool(8192, 1); // 队列 + 后台线程
auto console = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
console->set_level(spdlog::level::warn); // 控制台只看 warn 以上
auto file = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
log_dir + "/" + service + ".log", 20 * 1024 * 1024, 5);
file->set_level(spdlog::level::trace); // 文件记全量
std::vector<spdlog::sink_ptr> sinks{console, file};
logger_ = std::make_shared<spdlog::async_logger>(
service, sinks.begin(), sinks.end(),
spdlog::thread_pool(), spdlog::async_overflow_policy::block);
logger_->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%t] [%^%l%$] %v");
logger_->set_level(level);
logger_->flush_on(spdlog::level::err); // 错误立即刷盘
spdlog::register_logger(logger_);
}
std::shared_ptr<spdlog::logger> Logger::Get() const {
return logger_ ? logger_ : spdlog::default_logger();
}
void Logger::Shutdown() {
if (logger_) {
logger_->flush();
}
spdlog::shutdown();
}
} // namespace cm
6.3 使用:main.cc
cpp
#include "logger.h"
int main() {
cm::Logger::Instance().Init("order_service", "./logs", spdlog::level::debug);
CM_LOG_INFO("service started, pid={}", 1234);
CM_LOG_DEBUG("load config from {}", "./conf/app.conf");
CM_LOG_WARN("slow query: {} ms", 1200);
CM_LOG_ERROR("connect mysql failed, retry={}", 3);
cm::Logger::Instance().Shutdown(); // 退出前刷盘
return 0;
}
控制台只会看到 warn 及以上:
text
[2026-10-19 21:35:02.481] [12345] [warning] slow query: 1200 ms
[2026-10-19 21:35:02.482] [12345] [error] connect mysql failed, retry=3
文件 logs/order_service.log 则包含 info / debug 全量,20MB 轮转、保留 5 个。
6.4 编译
bash
g++ -std=c++17 -Iinclude main.cc src/logger.cc -o app -lspdlog -lfmt -pthread
6.5 几个设计细节
ℹ️ 为什么用异步 logger + 单例
单例保证全局只有一个日志器、一套配置;异步 logger 让业务线程只做入队。两者叠加,日志代码对业务几乎零侵入。
- 初始化顺序 :
Logger::Init要放在 main 开头、其他组件初始化之前;没初始化时宏走默认 logger(输出到控制台),不会崩。 - 退出顺序 :
Shutdown放在所有业务线程 join 之后,避免线程还在写日志时 logger 被销毁。 - 运行时调级别 :
spdlog::set_level/logger->set_level是线程安全的,适合做「临时打开 debug」的运维开关。 - 编译期裁剪 :发布版可以定义
SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_INFO,配合SPDLOG_LOGGER_DEBUG(...)宏把 debug 日志在编译期去掉。
7. 落地与总结:日志策略比 API 更重要
API 会用了,项目里更容易翻车的是策略:
| 策略 | 建议 |
|---|---|
| 默认级别 | 生产 info,排查时临时 debug |
| 文件轮转 | 单文件 20~50MB,保留 5~10 个 |
| 刷盘 | flush_on(err),重要业务 flush_on(info) |
| 脱敏 | 密码、token、身份证、手机号一律不打印 |
| 性能 | 高频路径避免 trace/debug;大对象不要直接打 |
| 保留 | 至少留 7 天,方便回溯 |
⚠️ 三个最常见的事故
① 忘了 shutdown,最后几条错误日志丢了;② 文件不轮转,把磁盘写满;③ 把密码打进了日志,安全审计直接不过。
📝 小结
① 先用起来 :stdout.cc 那几行就是最小可用集合------异步、级别、格式、shutdown。
② 再做封装 :单例 + 宏 + 双 sink + 异步,业务只依赖 CM_LOG_*,底层随时可换。
③ 最后定策略:级别、轮转、刷盘、脱敏,比记住多少 API 更重要。
🔜 下一篇
《gtest 实战:从断言到测试夹具,给 C++ 项目补上单元测试》------顺手把日志组件也补上单元测试。关注专栏不迷路。
🔗 参考
1 spdlog GitHub 仓库:https://github.com/gabime/spdlog
2 spdlog 官方 wiki:https://github.com/gabime/spdlog/wiki
3 fmt 格式化库:https://github.com/fmtlib/fmt