日志应通过组合式日志器(如spdlog自定义sink)实现多目标输出,避免硬编码分支;数据库sink须异步、复用连接、批量提交,并设队列上限防内存溢出。

日志同时输出到多个目的地,核心是用组合式日志器而非硬编码分支
直接在每个 log() 调用里写三段逻辑(std::cout、std::ofstream、SQL 插入)不仅难维护,还会让错误处理和格式同步变得脆弱。C++ 没有内置多目标日志,但标准库的 std::ostream 抽象和 RAII 特性足以支撑干净的组合设计。
关键不是“怎么分别写”,而是“怎么把控制台、文件、数据库封装成统一的 std::ostream 或等效接口”。比如用 boost::iostreams::tee 可以分流到两个流,但它不支持异步或带格式的数据库写入——所以更推荐自定义 sink(接收器)模式。
用 spdlog 实现三路输出:控制台 + 文件 + 自定义数据库 sink
spdlog 是最轻量且可扩展的选择。它原生支持控制台(stdout_color_mt)和文件(basic_file_sink_mt),数据库需自己实现 spdlog::sink 子类。注意:不要在 sink 的 log() 方法里做阻塞 SQL 执行,否则会拖慢整个日志链。
- 数据库 sink 必须继承
spdlog::sinks::base_sink<mutext></mutext>,重写sink_it_()和flush_() - 在
sink_it_()中只做线程安全的队列 push(例如用moodycamel::ConcurrentQueue),另起一个后台线程消费并执行 SQL - 避免在 sink 里打开/关闭数据库连接——连接应复用或由连接池管理,初始化时传入 sink 实例
- 示例片段:
class db_sink : public spdlog::sinks::base_sink<:mutex> { protected: void sink_it_(const spdlog::details::log_msg &msg) override { _queue.enqueue(msg); } void flush_() override {} private: moodycamel::ConcurrentQueue<:details::log_msg> _queue; };</:details::log_msg></:mutex>
手动组合 std::ostream 时,文件和控制台容易,数据库必须绕开流接口
std::ostream 无法自然对接数据库,因为 SQL 写入需要结构化字段(时间戳、级别、消息体、线程ID),而流只提供字符串。强行派生 std::streambuf 去拦截 xsputn() 并解析日志行,既不可靠(换行符可能被缓冲拆分),又丧失结构信息。
更可行的做法是:用 std::stringstream 格式化一次,再分别喂给不同目标:
- 控制台:直接
std::cout - 文件:用
std::ofstream写入ss.str() - 数据库:解析
ss.str()(或更好——从原始log_entry结构体提取字段),调用sqlite3_bind_text()或mysql_stmt_bind_param() - 务必检查
ss.str().size()是否超长(尤其 MySQL 的TEXT字段有长度限制),避免截断
性能陷阱:同步写数据库会让日志变慢,甚至卡死主线程
哪怕用了连接池,单次 INSERT 的网络往返和事务开销也远高于内存写或磁盘追加。实测中,未异步化的数据库 sink 可使日志吞吐下降 10 倍以上。
- 绝对不要在日志主路径中调用
mysql_query()或sqlite3_exec() - 后台线程消费队列时,建议批量提交(例如每 10 条或 100ms 合并为一个
INSERT ... VALUES (...), (...), (...)) - 数据库写入失败不能丢弃日志——至少要 fallback 到本地文件,并记录错误(但这个 fallback 日志不能再写数据库,否则死循环)
- 如果数据库暂时不可用,队列需设上限(如 1000 条),超限时丢弃旧日志并告警,而非无限堆积内存
真正麻烦的从来不是“怎么把日志发出去”,而是“怎么确保它不拖垮系统,又不丢关键错误”。数据库 sink 的健壮性,往往比控制台和文件加起来还难搞。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











