# xzlog **Repository Path**: zlzelu/log ## Basic Information - **Project Name**: xzlog - **Description**: 基于 C++17 的单头文件跨平台(Windows/Linux)高性能日志库。 - **Primary Language**: C++ - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2025-11-02 - **Last Updated**: 2026-08-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 多设计模式同步-异步日志系统 基于 C++17 的**单头文件**(`log.hpp`,header-only)跨平台(Windows/Linux)高性能日志库。融合策略模式(落地方向可替换)、门面模式(统一日志器)、单例模式(等级注册器)、装饰器模式(异步包装)、无锁编程(CAS 环形队列)等设计,覆盖同步写入、无锁异步、批量异步、双缓冲异步全场景。 ## 一、特性 - **单头文件**:`#include "log.hpp"` 即用,无外部依赖、无需链接 - **三种异步落地器**:队列逐条(`asyncSink`)、无锁批量(`batchSink`)、有界双缓冲批量(`doubleBufferSink`),按场景选型 - **同步/异步策略化**:日志器不区分模式,挂不同落地器即得不同行为,可混合 - **渲染**:渲染在日志器(生产线程)完成,渲染开销分摊到 N 个生产线程,消费端纯 IO - **键值化消息**:标准字段槽位化(O(1) 渲染)+ `logger::logFields` 每条日志扩展字段 - **原子 copy-on-write 配置**:格式/转换表可与日志输出并发修改 - **强落盘语义**:`flush()` 排空缓冲后转发内层 flush;析构自动排空(零丢失) - **跨平台**:Windows(`CreateFileMapping`)/ Linux(`mmap`)条件编译 ## 二、架构总览 ``` src/log.hpp(单头文件,header-only) │ ├── namespace tool 基础工具 │ ├── mpscQueue 多生产者单消费者无锁队列(有界环形,CAS 槽位) │ ├── lockFreeAppendBuffer 无锁追加缓冲(预分配 + 原子计数,双缓冲用) │ ├── Mmap 跨平台内存映射文件(Windows/Linux 条件编译) │ └── nowTime() 秒级时间戳 │ ├── enum class Level 默认等级(ALL/DEBUG/INFO/WARN/ERROR/FATAL/OFF) │ ├── namespace base 核心层 │ ├── logMessageBase 日志消息(标准字段槽位 + 自定义字段扩展 + 渲染) │ └── logEnumRegister 等级注册器(单例,类型擦除,支持自定义枚举) │ ├── namespace logger 日志器层 │ ├── formatConfig 渲染配置(格式模板 + 占位符转换表,copy-on-write) │ └── logger 统一日志器门面(构造消息 → 提前渲染 → 逐落地器 send) │ └── namespace sink 落地器层(策略模式:同步/异步自由组合) ├── logSink 抽象接口(send / flush) ├── logOutputStdout 控制台(同步直写) ├── logOutputFile 文件(同步直写,二进制追加) ├── logOutputMmapFile 内存映射文件(同步直写,高性能) ├── asyncSink 异步装饰器(MPSC 队列 + 条件变量,逐条消费) ├── batchSink 批量异步装饰器(无锁队列 + 批量消费) └── doubleBufferSink 有界双缓冲异步装饰器(muduo 模型 + 生产者背压) ``` **数据流**:`logger::operator() → 构造 logMessageBase → 提前渲染(B 方案)→ 逐 sink->send(已渲染字符串) → 落盘` ## 三、环境与构建 - **编译器**:C++17(GCC 8+ / MSVC 2017+ / Clang) - **平台**:Windows(原生 API)/ Linux(POSIX) - **构建**:头文件即库。测试与基准: ```bash powershell -ExecutionPolicy Bypass -File test/run_tests.ps1 # Windows:编译并运行全部测试 bash test/run_benchmark.sh # 编译并运行性能基准 g++ -std=c++17 -O2 -I . test/logger_test.cpp -o logger_test && ./logger_test # 功能测试 ``` - **Windows 注意**:包含本头文件后 `ERROR` 宏被 `#undef`(与 `Level::ERROR` 冲突);`min/max` 宏已通过 `NOMINMAX` 抑制 ## 四、快速开始 ```cpp #include "src/log.hpp" //1. 同步日志器:控制台 + 文件多路输出 auto fileSink = std::make_shared("./app.log"); auto consoleSink = std::make_shared(); logger::logger syncLog("app", { fileSink, consoleSink }); syncLog("用户登录成功", (int)Level::INFO, __FILE__, __LINE__); //2. 异步批量日志器(高吞吐):batchSink 装饰 logger::logger batchLog("app", std::make_shared(fileSink, 16384, 1024, 100)); batchLog("异步日志", (int)Level::WARN, __FILE__, __LINE__); //3. 双缓冲异步(大消息/顺序敏感) logger::logger dbLog("app", std::make_shared(fileSink, 100000, 100)); dbLog("大消息日志", (int)Level::ERROR, __FILE__, __LINE__); //4. 多线程使用(operator() 线程安全) std::thread t([&] { for (int i = 0; i < 1000; i++) batchLog("t" + std::to_string(i), (int)Level::INFO, __FILE__, __LINE__); }); t.join(); //5. 每条日志扩展字段 logger::logFields fields{ { "%u", std::string("user_007") }, { "%r", 42 }, }; syncLog.setFormat("[%p][%u][%r] %m%n"); syncLog("带业务字段", (int)Level::INFO, __FILE__, __LINE__, fields); //6. 退出前强制落盘(排空已提交日志后转发内层 flush) syncLog.flush(); ``` ## 五、API 参考 ### 5.1 `logger::logger`(统一日志器门面) | 接口 | 说明 | |---|---| | `logger(name, sink)` / `logger(name, vector)` | 构造,单/多落地方向 | | `operator()(data, level, fileName, line)` | 日志输出(线程安全),level 为 int(可传自定义枚举值) | | `operator()(data, level, fileName, line, logFields)` | 输出带每条日志扩展字段的消息 | | `setFormat(format)` | 设置格式模板(copy-on-write) | | `formatChange(key, fun)` | 覆盖/新增占位符转换函数(copy-on-write) | | `enumRegisterDevice(fun)` / `enumOutDevice()` | 注册/注销自定义等级枚举 | | `addSink(sink)` | 追加落地方向 | | `flush()` | 强制所有落地器落盘(排空后转发) | ### 5.2 `sink::logSink`(落地器抽象) ```cpp virtual void send(std::string log) = 0; //接收已渲染日志(值传递,move 语义) virtual void flush() {} //强制落盘(默认空实现) ``` 同步落地器:`logOutputStdout()`、`logOutputFile(path)`(二进制追加 "ab",构造失败抛异常)、`logOutputMmapFile(path)`。三者均自持互斥锁保证多线程顺序一致。 ### 5.3 异步装饰器(均包装任意 `logSink`) | 落地器 | 构造参数 | 消费模式 | |---|---|---| | `asyncSink(inner, capacity=16384)` | MPSC 队列 | 逐条消费;空闲时条件变量阻塞 | | `batchSink(inner, capacity=16384, batchSize=1024, intervalMs=100)` | 无锁队列 | 批量:积累 ≥ batchSize 即时通知 OR interval 超时兜底 | | `doubleBufferSink(inner, bufferSize=100000, intervalMs=100, maxPendingBuffers=8)` | 有界双缓冲 + 锁内交换 | 写满即换 OR interval 兜底;达到待处理缓冲上限时阻塞生产者 | 三者析构时自动排空;`flush()` 均等待已提交条目处理完成后转发内层。后台自定义 Sink 抛出的异常会被隔离,失败条目不会导致进程 `terminate`。 ### 5.4 `tool::mpscQueue`(无锁队列) ```cpp void push(T&&/const T&); //阻塞入队(满时自旋退避) bool tryPush(T&&/const T&); //非阻塞入队,满返回 false bool tryPop(T& out); //非阻塞出队,空返回 false bool pop(T& out, size_t timeoutMs = 0); //阻塞出队(0=无限) void tryPopAll(std::vector& out); //批量取出全部可用(消费者专用) size_t size() / capacity() / empty(); ``` T 需默认构造 + 移动赋值;容量向上取整 2 的幂。内存序:release-acquire + release sequence(槽位复用安全)。 ### 5.5 `base::logMessageBase`(日志消息) ```cpp void setMessage(data, level, loggerName, fileName, line); //批量注入标准字段 template void setPlaceholder(key, value); //自定义字段(支持 operator<<) template void setPLaceholder(key, value); //旧拼写兼容接口 template bool get(key, out) const; //读取字段(强类型) static std::vector parseFormat(format); //解析模板(静态) const std::string& format(pattern, funMap); //渲染(函数表优先 → 标准槽位 → 自定义 map) void clear(); //清字段(保留配置) bool hasPLaceholder(key) const; ``` ### 5.6 `base::logEnumRegister`(等级注册器,单例) ```cpp static logEnumRegister& Init(); //全局唯一(内置 Level 转换) template void registerDevice(fun); //注册枚举→字符串 template void logoutDevice(); //注销 template std::string toString(v); //按类型转换 std::string toString(int level); //按 Level 转换(%p 用) ``` ### 5.7 `tool::Mmap`(内存映射文件) `Mmap(path)` 构造映射(失败抛异常);`write(str)` 追加(自动扩容,失败返回 false);`flush()` 强制落盘;析构恢复文件大小为内容大小。扩容按需增长(页对齐,最小 1MB 步进)。 ## 六、格式与占位符 默认格式:`[%d][%t][%p][%c][%f:%l] %m %n` | 占位符 | 含义 | 来源 | |---|---|---| | `%d` | 时间戳(秒级) | 标准槽位(`setMessage` 自动注入) | | `%t` | 线程 ID | 标准槽位 | | `%l` | 行号 | 标准槽位 | | `%p` | 等级 | 标准槽位(int),默认经 `logEnumRegister` 转字符串 | | `%f` | 文件名 | 标准槽位 | | `%m` | 日志内容 | 标准槽位 | | `%c` | 日志器名称 | 标准槽位 | | `%n` | 换行 | 默认注册(`formatChange` 可覆盖) | | `%T` | 缩进 | 需自定义注册 | | 任意 `%x` | 自定义 | `formatChange`、`setPlaceholder` 或 `logger::logFields` | **渲染优先级**:占位符转换函数(`formatChange`)> 标准字段槽位 > 自定义字段 map > 空。 使用 `%%` 输出字面量 `%`;格式末尾的孤立 `%` 会原样保留。 ## 七、自定义能力 ```cpp //1. 自定义格式模板 log.setFormat("[%d][%p] %m (%f:%l)%n"); //2. 覆盖默认占位符(如 %t 哈希渲染) log.formatChange("%t", [](const base::logMessageBase& msg) { std::thread::id id; msg.get("%t", id); return "tid:" + std::to_string(std::hash{}(id)); }); //3. 自定义等级枚举(注册 + %p 切换) enum class CustomLevel : uint8_t { TRACE = 0, NOTICE, CRITICAL }; log.enumRegisterDevice([](const CustomLevel& l) -> std::string { /* ... */ }); log.formatChange("%p", [](const base::logMessageBase& msg) { int v = 0; msg.get("%p", v); return base::logEnumRegister::Init().toString(static_cast(v)); }); log.enumOutDevice(); //注销 //4. 自定义落地方向:继承 sink::logSink class logOutputNetwork : public sink::logSink { public: void send(std::string log) override { /* 网络发送 */ } void flush() override {} }; //5. 每条日志业务字段:支持字符串、数值及自定义 operator<< 类型 logger::logFields fields{ { "%u", std::string("user_007") }, { "%r", requestId }, }; log.setFormat("[%p][%u][%r] %m%n"); log("request accepted", (int)Level::INFO, __FILE__, __LINE__, fields); ``` ## 八、线程安全模型 | 接口 | 线程安全 | 说明 | |---|---|---| | `operator()` | ✅ | 消息局部构造 + 原子读取配置快照 + sink 内部同步 | | `setFormat` / `formatChange` | ⚠️ | 与 `operator()` 并发安全;多个配置写线程之间仍需外部互斥,避免更新覆盖 | | `addSink` | ⚠️ | 配置接口,须在日志输出前调用 | | `enumRegisterDevice` / `enumOutDevice` | ⚠️ | 配置期调用,运行期只读 | | `flush()` | ✅ | 与 `operator()` 并发安全 | | 异步落地器析构 | ⚠️ | **析构前须停止生产者**(与旧体系一致) | ## 九、可靠性语义 - **正常关闭排空**:停止生产者后析构异步落地器,会处理队列/缓冲中的剩余条目再 `join` - **强 drain + flush**:`flush()` 等待已提交条目完成 `inner->send` 后再转发内层 `flush` - **背压**:队列/缓冲满时生产者阻塞(`batchSink` 无锁自旋退避、`doubleBufferSink` 锁内等待)——内存有界,不会无限增长 - **顺序**:同步落地器锁内写入 = 调用顺序;`doubleBufferSink` 前端锁内写入同样保序 - **写失败**:内置落地器写入失败忽略;后台自定义 Sink 异常被隔离并丢弃当前条目;构造失败(文件打不开)抛异常 ## 十、与主流日志库的性能对比 ### 对比日志库 spdlog 1.16.0 Quill 11.0.1 glog 0.7.1 ### 环境 i9-14900HX(24 核/32 逻辑) Windows 11 (10.0.26200) MSYS2 UCRT64 GCC 15.2.0(C++17、Release、`-O3`) NTFS 上执行统一文本日志基准。 方法约束:各库接收相同预构造消息; 统一输出格式为「正文 + 换行」; 异步队列全部使用阻塞不丢日志策略; 生产者时间到 join 结束、端到端时间到 flush/drain 返回; 文件输出逐行校验`BENCH_MSG|` 计数。每个配置重复 5 次,以下为中位数; 表格为**producer / 端到端**。 ### 10.1 框架吞吐(null sink,256B,端到端 Mlogs/s) | 实现 | 1 线程 | 4 线程 | 8 线程 | |---|---:|---:|---:| | `ours-sync` | **3.16** | **6.15** | **4.19** | | `ours-batch` | 2.52 | 4.46 | 3.53 | | `ours-double` | 2.58 | 3.31 | 2.67 | | `ours-async` | 1.26 | 2.36 | 1.92 | | `quill-async` | 1.74 | 1.47 | 1.39 | | `spdlog-async` | 1.56 | 0.99 | 0.22 | | `spdlog-sync` | 10.26 | 37.40 | 61.61 | ### 10.2 文本文件吞吐(256B,端到端 Mlogs/s) | 实现 | 1 线程 | 4 线程 | 8 线程 | |---|---:|---:|---:| | `ours-batch` | **2.59** | **4.58** | **3.64** | | `ours-double` | 2.25 | 3.09 | 2.46 | | `ours-sync` | 2.28 | 1.90 | 1.66 | | `ours-async` | 1.28 | 1.68 | 1.78 | | `spdlog-sync` | 1.12 | 0.91 | 0.88 | | `quill-async` | 1.00 | 0.77 | 0.70 | | `glog-sync` | 1.00 | 0.73 | 0.76 | | `spdlog-async` | 0.98 | 0.33 | 0.15 | ### 10.3 文件吞吐与消息长度(4 线程,端到端 MiB/s) | 实现 | 64B | 256B | 1024B | |---|---:|---:|---:| | `ours-batch` | **297.0** | **1117.9** | **1541.7** | | `ours-double` | 208.7 | 754.2 | 1257.0 | | `ours-sync` | 148.2 | 464.8 | 887.6 | | `ours-async` | 110.7 | 408.9 | 1108.8 | | `spdlog-sync` | 162.1 | 221.2 | 273.1 | | `glog-sync` | 73.3 | 178.1 | 261.2 | | `quill-async` | 109.2 | 187.3 | 237.5 | | `spdlog-async` | 57.9 | 79.5 | 169.3 | ### 10.4 前端调用延迟(128B,微秒/次) null sink: | 实现 | avg | p50 | p99 | p99.9 | max | |---|---:|---:|---:|---:|---:| | `ours-sync` | 0.508 | 0.5 | 0.6 | 1.6 | 45.4 | | `ours-double` | 0.564 | 0.5 | 0.7 | 7.4 | 105.7 | | `ours-batch` | 0.560 | 0.5 | 1.3 | 4.6 | 88.4 | | `ours-async` | 1.013 | 0.6 | 3.4 | 10.8 | 107.2 | | `spdlog-sync` | 0.082 | 0.1 | 0.1 | 0.1 | 25.2 | | `quill-async` | 0.407 | 0.0 | 0.1 | 0.1 | 21252.0 | 文本文件: | 实现 | avg | p50 | p99 | p99.9 | max | |---|---:|---:|---:|---:|---:| | `ours-sync` | 0.565 | 0.5 | 0.6 | 16.2 | 134.7 | | `ours-double` | 0.557 | 0.5 | 0.7 | 8.8 | 90.8 | | `ours-batch` | 0.545 | 0.5 | 1.2 | 5.4 | 51.1 | | `ours-async` | 0.971 | 0.6 | 3.2 | 10.3 | 77.1 | | `spdlog-sync` | 0.501 | 0.1 | 7.5 | 49.8 | 1013.9 | | `quill-async` | 0.670 | 0.1 | 0.1 | 0.1 | 30918.6 | ### 10.7 选型结论 - 文件输出最快:`batchSink`(4 线程 256B 端到端 4.58 Mlogs/s, 1024B 1541.7 MiB/s)。 - 大消息与顺序敏感场景:`doubleBufferSink`(1024B 1257.0 MiB/s)。 - 同步模式框架吞吐 4 线程 6.15 Mlogs/s,与 `spdlog-sync`(37.4) 仍有约 6 倍差距,剩余开销集中在消息字段构造与复制路径,即 计划中暂缓的队列结构方向(SPSC)。 - 异步模式必须看 flush 后的端到端结果,不能把 producer enqueue 吞吐当作磁盘吞吐。 原始数据:`test/results/comparison_20260804_160110/raw.csv`(每次正式 运行生成 `test/results/comparison_<时间戳>/` 下的 `raw.csv`、 `report.md`、`metadata.json`)。复现命令见「十一、测试与基准」。 ## 十一、测试与基准 ``` test/ ├── mpsc_test.cpp 无锁队列正确性(并发 8×20万/背压/超时/无限等待) ├── logger_test.cpp 日志器 + 落地器全家族(22 组用例) ├── benchmark.cpp 性能基准(--threads / --sinks null,mmap,file / --total / --no-latency) ├── benchmark_spdlog.cpp spdlog 基线对比(需安装 mingw-w64-ucrt-x86_64-spdlog) ├── comparison/ vcpkg 多日志库统一基准、CMake 工程与汇总脚本 ├── run_tests.ps1 Windows 一键编译 logger/mpsc/benchmark,并运行功能测试 ├── run_benchmark.sh 一键编译运行基准(--quick 快速模式) └── results/ 报告输出(benchmark_*.txt / spdlog_bench_*.txt) ``` ```bash powershell -ExecutionPolicy Bypass -File test/run_tests.ps1 .\test\comparison\run_comparison.ps1 -Quick -Repeats 1 .\test\comparison\run_comparison.ps1 -Repeats 5 bash test/run_benchmark.sh # 完整矩阵约 2-3 分钟 bash test/run_benchmark.sh --quick # 快速验证 ``` ## 十二、注意事项 1. **`ERROR` 宏**:Windows 下包含本头文件后 `ERROR` 宏被 `#undef`(与 `Level::ERROR` 冲突),Windows API 代码中勿依赖该宏 2. **编码**:源文件统一 UTF-8 with BOM(MSVC 与 GCC 均自动识别) 3. **文件落地器**:二进制追加模式(`"ab"`),Windows 上不会产生 CRLF 转换,跨平台字节一致