std::cout和spdlog::info默认非结构化,因其输出为格式化字符串而非键值对;结构化日志需显式构造JSON对象(如用nlohmann::json),确保机器可解析。

为什么 std::cout 和 spdlog::info 默认不是结构化的
结构化日志的核心是「机器可解析」——每条日志必须是明确字段+值的组合(比如 {"level":"info","service":"auth","duration_ms":42,"user_id":1001}),而不是拼接字符串。但 spdlog 的 info("user {} logged in", user_id) 输出的是纯文本,JSON 解析器根本分不出哪个是字段、哪个是值。本质问题在于:日志库默认走的是格式化字符串路径,不是键值对序列化路径。
关键判断:C++ 本身没有语言级结构化日志支持,必须靠日志库显式提供结构化接口或手动构造 JSON。
-
spdlog4.0+ 提供了spdlog::source_loc和自定义 sink,但原生不带字段注入 API;需配合nlohmann/json手动组装 -
g3log支持LOGF(INFO, "msg", "user_id", user_id, "action", "login")这类语法,但底层仍需 sink 转成 JSON - 直接用
fmt::format拼 JSON 字符串容易出错(引号转义、类型不一致、嵌套对象漏处理)
用 nlohmann/json + spdlog 手动构造结构化日志
这是目前最可控、兼容性最好的做法:把日志内容先构造成 nlohmann::json 对象,再序列化为字符串发给 spdlog。避免字符串拼接,也绕过日志库对结构化字段的缺失支持。
实操要点:
立即学习“C++免费学习笔记(深入)”;
- 所有字段名必须是字符串字面量(
"user_id"),不能是变量名自动推导(C++ 没反射) - 注意类型一致性:
int64_t、double、std::string可直传;const char*需转std::string或用json::string_t - 时间戳建议用
std::chrono::system_clock::now()转time_t,别依赖spdlog的内置时间(它只输出格式化字符串,无法提取字段) - 示例:
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
nlohmann::json log_entry = {
{"level", "info"},
{"service", "auth"},
{"event", "user_login"},
{"user_id", user.id},
{"ip", std::string(remote_ip)},
{"ts", std::time(nullptr)}
};
spdlog::info("{}", log_entry.dump());
这样输出就是标准 JSON 行,ELK 或 Loki 能直接解析字段。
避免在高并发场景下频繁构造 nlohmann::json 对象
nlohmann/json 构造和 dump() 是动态内存分配操作,在 QPS 上万的服务里可能成为瓶颈。不是不能用,而是要注意位置和粒度。
- 不要在 tight loop 里每轮都建新
json对象(比如单次请求里记录 50 条 debug 日志) - 高频日志(如指标打点)改用预分配 buffer + 手写 JSON 序列化(如
simdjson的 on-demand API 或自定义char[]格式化) - 低频关键日志(错误、登录、支付)才用完整
nlohmann::json,确保字段齐全、可审计 - 考虑复用
json对象:声明为局部静态或线程局部变量,每次clear()后重填,减少 malloc 压力
分布式上下文字段(TraceID / SpanID)怎么注入到每条结构化日志
结构化日志失去 trace 上下文就等于丢了一半价值。C++ 没有类似 Go 的 context 或 Java 的 MDC,必须自己维护线程局部存储(TLS)或显式传递。
- 推荐用
thread_local存储当前 trace 上下文,但注意:协程切换(如boost::asio或libco)会破坏 TLS,此时必须显式传参 - 封装一个
log_structured辅助函数,自动合并 TLS 中的trace_id、span_id、service_name - 不要在每个日志调用处重复写
{"trace_id":..., "span_id":...}—— 容易漏、难维护 - 示例模式:
thread_local std::string current_trace_id;
thread_local std::string current_span_id;
void log_structured(const nlohmann::json& fields) {
nlohmann::json full = fields;
if (!current_trace_id.empty()) full["trace_id"] = current_trace_id;
if (!current_span_id.empty()) full["span_id"] = current_span_id;
spdlog::info("{}", full.dump());
}
真正麻烦的不是加字段,而是确保 current_trace_id 在异步回调、线程池任务、定时器触发时依然正确 —— 这部分必须和你的 RPC 框架或协程库深度耦合,没有银弹。


















