简介CuteLogger 是一套面向 Qt 开发者的轻量级日志库用于替代 qDebug 等原生输出解决多线程工程中日志信息不全、格式不统一、扩展性不足等问题。它几乎能记录文件名、源代码行与函数签名并通过附加程序机制灵活输出到文件、控制台甚至 Android logcat也支持自定义附加程序与输出格式同时内置日志分类、耗时统计线程安全可直接替换现有 qDebug 调用适合中高级 Qt 工程师集成到桌面端或移动端项目。压缩包共 20 个文件以 10 个头文件与 9 个实现文件为主再配合 1 个 pri 工程文件完成模块组织整体仅 29KB结构清晰便于引入现有工程后按需裁剪。资源已有 925 人学习下载源码中附加程序接口、字符串格式化基类与管理器的实现能帮助读者理解日志框架的扩展点并据此定制符合自己项目的输出格式与分类策略。对希望摆脱调试输出、建立规范日志体系的 Qt 开发者而言这是一份小巧实用的参考实现。1. 拿到 CuteLogger.rar 之后先搞清楚它替你解决了什么第一次拿到 CuteLogger.rar 这个压缩包时大多数人和我一样以为只是又一个“写日志更漂亮”的库——用起来才发现它真正解决的是日志这条链路的确定性谁、在哪个线程、什么时候、以什么级别、写到了哪里。某次排查一个多线程采集程序的崩溃现场旧日志只有时间戳和一行文字线程交错完全看不出竞态换上 CuteLogger 后线程 ID、分类名、滚动文件同时到位崩在哪个调用立刻清晰。它适合三类人被 Qt 项目日志乱序折磨的开发、需要在嵌入式与服务端保留现场的人、以及想把日志输出做成可插拔组件而不是焊死在代码里的团队。说白了它解决的不是“有没有日志”而是“日志能不能当证据用”。2. 编译与集成把 CuteLogger 接进 Qt 项目的最小步骤2.1 解压后先认目录哪些文件是必须的不管你手上的 CuteLogger.rar 是从哪个渠道拿到的、压缩包内部目录长什么样第一件事永远是先解压、再看文件清单。Linux 或 macOS 下我一般用 unrar 或 unarWindows 直接右键解压也能处理。解压命令如下unrar x CuteLogger.rar # 如果系统没有 unrar可以用 unar 替代输出结果相同 ls -la解压后你会看到一堆 .h 和 .cpp。这套包里装的是源码工程不是编译好的二进制所以“解压即用”是不存在的必须先参与编译。需要认出的核心文件大致是这样几类文件作用Logger.h / Logger.cpp日志入口对外提供流式写日志接口LogManager.h / LogManager.cpp管理所有 Logger 实例按名字复用AbstractAppender.h / .cpp所有输出端的抽象基类AbstractStringAppender.h / .cpp支持格式字符串的输出基类ConsoleAppender.h / .cpp控制台输出端FileAppender.h / .cpp固定文件输出端RollingFileAppender.h / .cpp按大小滚动的文件输出端CMakeLists.txt 或 .pri构建脚本决定怎么把库编出来我见过有人拿到包后直接朝 main.cpp 里 include 了一堆 Logger.h然后发现链接失败——因为他只拷贝了头文件没让源文件参与编译。这个包的集成本质是把上面这些文件里的核心代码编译进你的程序或者编成独立库再链进来。先认清文件归属后面才不容易乱。2.2 CMake 接入与源码拷贝两种集成方式的取舍最常见的做法是把解压出来的整个目录放进你工程的 third_party 或 3rdparty 下然后在 CMakeLists.txt 里通过 add_subdirectory 把它作为子目录编进来。前提是包里自带可用的 CMakeLists.txt构建目标和目标名需要看一眼再写通常会有一个库目标名字类似 CuteLogger。完整写法如下cmake_minimum_required(VERSION 3.16) project(Demo) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) # 关键Logger 继承自 QObject必须开启自动 moc find_package(Qt5 COMPONENTS Core QUIET) # Qt6 改成 find_package(Qt6 ...) add_subdirectory(third_party/CuteLogger) add_executable(Demo main.cpp) target_link_libraries(Demo PRIVATE CuteLogger Qt::Core)这里有两处容易翻车。第一CMAKE_AUTOMOC 必须打开。CuteLogger 内部有 QObject 子类如果你自己把源码拷进 target 而不开 AUTOMOC编译会报一堆“未定义虚表”或 moc 文件找不到的错误走 add_subdirectory 时如果子目录 CMakeLists 里自己开了你这边不用管但直接混合编译时一定要显式打开。第二target_link_libraries 里的 Qt::Core 要和你的 find_package 版本对应Qt5 就链 Qt5::CoreQt6 就链 Qt6::Core混用会链接失败。如果你不想引入一个子目录工程也可以直接把 src 下的 .cpp 全部加进自己的可执行文件 target相当于“源码拷贝”集成。这种方式少一次库封装改动直观但会让你的构建脚本变得啰嗦而且每次升级这个日志库都要手动替换文件。我会这么写set(CUTE_LOGGER_SRC third_party/CuteLogger/Logger.cpp third_party/CuteLogger/LogManager.cpp third_party/CuteLogger/AbstractAppender.cpp third_party/CuteLogger/AbstractStringAppender.cpp third_party/CuteLogger/ConsoleAppender.cpp third_party/CuteLogger/FileAppender.cpp third_party/CuteLogger/RollingFileAppender.cpp ) add_executable(Demo main.cpp ${CUTE_LOGGER_SRC}) target_include_directories(Demo PRIVATE third_party/CuteLogger)要点是 target_include_directories 必须指到放头文件的那个目录否则 include Logger.h 找不到。两种方式我的建议是只在一个项目里用选第一种 add_subdirectory干净、方便升级需要同时给三四个工程共用就先把 CuteLogger 编成静态库再用 find_package 或直接 link 进来避免每个工程重复编译同一批源码。2.3 与 Qt 的耦合AUTOMOC、构建器和导出宏的边界CuteLogger 是围绕 Qt 核心类设计的库QString、QDateTime、QFile 是它的底层依赖所以不要试图在纯标准库环境里硬编它。实际集成时你要么用 Qt 5 的某个 LTS 版本要么用 Qt 6CMake 里通过 find_package 把对应模块找出来。编译器的选择上Windows 下常见的是 MSVC 或 MinGW两者都能编但有个细节如果你这侧程序用 MSVC 编CuteLogger 也必须用同编译器混用 MinGW 编的静态库再让 MSVC 链接会出现符号兼容问题这类问题通常到运行期才爆不好查。这里还需要留意导出宏。Windows 上动态库会通过导出宏控制哪些符号对外可见CuteLogger 的头文件里通常有 CUTE_LOGGER_EXPORT 之类的宏定义如果你选择静态编译记得把对应的宏开关关掉或按包内说明改成空定义否则链接期会要求你额外带一堆导入库。具体宏名以你拿到的包为准我习惯是打开它的全局头文件搜 EXPORT 关键字确认一遍别猜。如果你是从 qmake 工程接它包里可能自带 .pri 文件在 .pro 里 include 对应 .pri 路径即可。遇到过问题.pri 里的源码路径是相对路径你把包放到别的目录层级路径不匹配会导致 file(GLOB) 拿不到文件。处理办法是调整 include 的相对路径或者干脆改用上面 CMake 方式路径关系由你自己控制。3. 核心协作关系Logger、LogManager、Appender 如何各司其职3.1 一条日志消息的完整旅程从流式写入到文件落盘先看一段最常见的调用代码然后跟着它把链路走一遍#include Logger.h #include ConsoleAppender.h #include FileAppender.h void handleLogin(int uid) { Logger *logger Logger::logger(login); logger-info() user login, uid uid; }这段代码里你只看到 logger-info() 一个调用实际上背后发生了四层动作。第一层Logger::info() 返回一个临时的流式对象operator 把你的参数逐个拼接起来第二层这个临时对象在析构时把整条消息连同当前时间戳、线程 ID、级别、文件名、行号、函数名、Logger 名字组装成一个待写入的消息体第三层Logger 遍历自己已经注册的 Appender 列表逐个调用 write第四层AbstractStringAppender 按你设置的格式串把消息体的字段替换成最终字符串再写到控制台或文件。这个设计的价值在于业务代码只负责“我要记一条日志”至于记到哪、用什么格式、断没断文件都是后话由 Appender 决定。你需要理解的核心机制是LoggerStream 生命周期结束才真正触发写动作。所以不要写出这样的代码logger-info() begin; // 这里构造完临时对象析构还没发生某些极端场景下行为会变得 tricky写成常规一条语句一个日志即可。消息组装发生在析构阶段意味着如果那条语句所在的函数异常返回临时对象依然会被析构日志还是会写出去这点比手动拼字符串安全得多。3.2 流式接口与级别过滤为什么 logger-debug() 可能什么都不打印CuteLogger 的级别体系一共六档Trace、Debug、Info、Warning、Error、Fatal。每个 Logger 实例可以设置自己的最低输出级别低于这个级别的日志直接不进入 Appender 分发。这是很多人第一次用时最容易误解的地方你以为集成好了就能看到全部日志结果 debug() 和 trace() 一条都不出来原因就是 Logger 的默认级别过滤把它们拦掉了。处理方式很直接Logger *logger Logger::logger(main); logger-setLevel(Logger::Trace); // 全量输出适合调试期 // logger-setLevel(Logger::Warning); // 生产环境只留警告和错误级别过滤发生在消息组装完成之后、分发到 Appender 之前而不是发生在 Appender 内部。这意味着即使你挂了十个 Appender级别判断也只做一次不会因为输出端多而重复计算。CuteLogger 也提供了一组宏用起来能自动带上文件名和行号LOG_TRACE(logger) trace message; LOG_DEBUG(logger) debug message; LOG_INFO(logger) info message; LOG_WARNING(logger) warning message; LOG_ERROR(logger) error message; LOG_FATAL(logger) fatal message;流式接口的好处是不需要做字符串格式化QString 的数字转换、指针地址输出都很自然。要注意的是这些宏和 logger-info() 成员函数是两条平行路径宏内部会补全 file/line/function 信息成员函数路径在某些版本里不会补全函数名。所以我自己的习惯是代码里一律用 LOG_xxx 宏统一拿文件名和行号不依赖成员函数的行为差异。3.3 用 Logger 名字做分类一套日志体系如何分片管理LogManager 按名字维护着 Logger 实例。同样一个名字Logger::logger(network) 在程序里调用一百次返回的是同一个实例。这带来两个实用效果。第一Appender 注册一次所有通过 Logger::logger(network) 写日志的地方都会复用同一组输出端第二不同名字的 Logger 拥有独立的 Appender 列表互相不影响。我的常用做法是给每个业务模块分配独立 Logger 名字比如 ui、network、db、scheduler。每个模块日志可以挂不同的 Appender实现“网络模块单独落一个文件业务日志进滚动文件告警日志直接走控制台”这种细粒度管理。示例Logger::logger(ui)-registerAppender(consoleAppender); Logger::logger(network)-registerAppender(networkFileAppender); Logger::logger(db)-registerAppender(dbFileAppender); Logger::logger(scheduler)-registerAppender(rollFileAppender);这样当线上出问题时我只需要盯 network.log 或 db 对应的文件不用在总日志里大海捞针。你也可以把分类名挂在格式串里用 %{category} 占位符打印出来这样即使所有日志都落同一个文件也能用文本工具按分类名过滤。分类这套东西很轻但它是日志体系能否用于事后分析的关键不要省。4. 落地输出控制台、文件、滚动文件的参数与格式定制4.1 ConsoleAppender调试窗口里的第一现场ConsoleAppender 是把日志打印到标准输出流的输出端适合开发调试期、以及没有持久化需求的服务端简单场景。注册方式相当简单#include ConsoleAppender.h ConsoleAppender *console new ConsoleAppender; console-setFormat(%{timestamp} [%{level}] %{category} %{message}); logger-registerAppender(console);setFormat 里的占位符会在每次写日志时被替换成实际内容。第一次接入时我建议只保留 timestamp、level、message 三项字段越少肉眼扫日志越快。控制台输出的典型限制是没有持久化、重启即丢失、大量输出时会拖慢终端所以它适合“调试期看现场”不适合“生产环境留证据”。如果程序是 daemon 方式跑在后台控制台日志基本等于没用这种情况直接把 ConsoleAppender 从注册列表里去掉或者让它和文件 Appender 并存不要只挂控制台。还有个细节值得注意ConsoleAppender 内部用的是标准输出流而某些框架会把 stdout 重定向到日志文件这样你注册了控制台 Appender 但日志却出现在别的文件里。遇到这种“控制台没东西但程序没报错”的情况先检查进程的标准输出被谁接管了别急着怀疑 Appender 失效。4.2 FileAppender固定文件写入与打开方式FileAppender 把日志写进一个固定文件不切割、不轮转。代码写法#include FileAppender.h FileAppender *file new FileAppender; file-setFileName(/var/log/app/runtime.log); file-setFormat(%{timestamp} [%{level}] %{category} %{threadid} %{message}); logger-registerAppender(file);写文件时的注意点集中在路径上。第一目录必须提前存在FileAppender 不会帮你递归创建目录目录不存在时它会静默失败你在业务代码里完全看不到错误提示日志文件也不会生成。第二建议写绝对路径相对路径受工作目录影响程序被 service 拉起时工作目录往往不是你预期的那个。第三文件权限要够跑在系统服务账户下时目标目录可能不可写同样表现为静默失败。我一般会在程序启动后主动探测一下文件 Appender 是否真的写出来了用一个非 debug 级别的日志写一条启动标记然后立刻去文件系统确认。这样的主动验证 30 秒就能做完能省掉后面几小时的排查。固定文件适合短期调试或临时任务长时间跑的系统别只用 FileAppender否则单文件会无限膨胀后面要用编辑器打开一个 2GB 的日志文件光加载就够受的。4.3 RollingFileAppender按大小滚动才是长跑方案长跑程序建议用 RollingFileAppender它会按文件大小自动切分并保留有限个历史文件。常见用法#include RollingFileAppender.h RollingFileAppender *rolling new RollingFileAppender; rolling-setFileName(/var/log/app/service.log); rolling-setFormat(%{timestamp} [%{level}] %{category} %{function} %{message}); rolling-setMaxFileSize(10 * 1024 * 1024); // 单文件最大 10MB rolling-setMaxLogFiles(5); // 保留 5 个历史文件 logger-registerAppender(rolling);关键参数是两个maxFileSize 控制单个文件的上限maxLogFiles 控制保留的历史文件数量。写满上限后库会把当前文件改成 service.log.1再新开 service.log 继续写旧文件依次后移超过保留数量的历史文件会被删除。这样磁盘占用被锁在一个可控范围10MB 乘 5 个加当前文件最多 60MB。这两个参数的设置要看你的日志量和磁盘空间。日志量大、又要长期排查问题时单文件 100MB、保留 10 个能覆盖更长时间线日志只是留痕用单文件 5MB、保留 3 个就够。我的习惯是先用一个月的数据量估算如果一天产生约 200MB 日志单文件 100MB、保留 5 个能覆盖约两天半的现场再配一个定期归档任务避免重要日志被滚动删除。4.4 Format 字符串与常用占位符的匹配关系格式串是 CuteLogger 定制输出最常用、也最需要确认的一项能力。不同小版本对占位符的支持可能有细微差异拿包后以 AbstractStringAppender 源码为准但下面这些是绝大多数版本通用的占位符含义示例输出%{timestamp}日志产生时间2025-05-18 14:23:01.123%{level}日志级别INFO%{category}Logger 分类名network%{threadid}线程 ID0x16d8%{file}源文件名handleLogin.cpp%{line}行号37%{function}函数名handleLogin(int)%{message}日志正文user login, uid123实际排格式串时我建议固定顺序时间、级别、分类、线程 ID、文件名行号、正文。时间在前方便按时间线排序检索线程 ID 放在前面方便定位并发问题正文放最后避免被前面的长字段挤到不可读。格式串里不建议塞太多占位符尤其是 file/line/function 三者同时打满时一行日志会变得很长终端里看还好落到文件里会增加不少体积分析时也不利于一眼抓住重点。还有一个习惯值得养成把 format 抽成一个公共函数或常量所有 Appender 复用不要每个 Appender 各写一份。这样后续要加字段只改一处所有输出端都同步更新不会出现某个文件缺字段、某个文件多字段的混乱局面。5. 避坑CuteLogger 常见的 5 个集成交警及排查思路5.1 编译过了但日志一条都没有现象程序编译链接全通过运行也不报错控制台和文件里都看不到任何日志输出。原因大概率不是库坏了而是级别过滤或 Appender 注册时机出了问题。Logger 默认级别可能把 debug 以下全部过滤或者你注册 Appender 的代码写在某次条件分支里那个分支没执行再或者你拿到的 logger 实例和写日志时用的不是同一个名字注册挂在 main 上写日志却用 network两套实例互不相通。解决先做最小验证——在 main 最开始用 LOG_ERROR 打一条固定消息级别最高不会被过滤同时确认注册代码一定早于所有写日志代码。如果 ERROR 能出、INFO 不出就是级别问题把 Logger 级别调到 Trace 再看。如果 ERROR 都不出优先怀疑 Appender 没注册成功直接打印 Appender 数量或检查注册返回值。这里的关键是“分步排除”把级别过滤、实例复用、注册时机三个变量逐个固定住。5.2 中文日志乱码或者干脆是问号现象程序里写的中文日志在控制台显示乱码在文件里打开看是问号或空白。原因没有直接证据指向 CuteLogger 本身有编码 bug绝大多数情况是三层编码不一致源文件编码、Qt 内部 QString 编码、输出端落盘时的编码。Qt 的 QString 内部是 UTF-16你的中文字符串字面量如果源文件是 GBK 编码被 Qt 读进来时转码结果就是错的后面再怎么设置格式串都救不回来。解决所有源文件统一 UTF-8 编码且在编译器里指定 UTF-8 处理输出文件侧FileAppender 写出时把它配置成 UTF-8文本编辑器也用 UTF-8 打开。Windows 控制台乱码是另一个独立问题它和系统代码页有关不要和文件乱码混在一起排查。先确认文件里写出来的字节流是否正确再去看控制台显示层否则会在错误的方向上浪费很多时间。优先保证落盘文件编码正确——那是证据控制台只是临时看的。5.3 滚动文件没有按预期切分文件无限增大现象明明设置了 maxFileSize日志文件却持续增长始终没有出现 .1、.2 这类滚动文件。原因最常见的是参数理解错误——maxFileSize 的单位可能导致你填的数偏大比如你以为填的是 MB实际库按字节处理填了 10 结果上限是 10 字节或者反过来填了 10MB 但库按 KB 解释上限变成 10GB。另一种情况是文件被其他进程占用了Windows 下日志文件被杀软或另一个打开句柄锁住库尝试重命名文件时失败只能继续往原文件写。解决先做对照试验把 maxFileSize 设成一个很小的值比如 1024 字节写几十条日志看它滚不滚。能滚说明机制正常问题在参数数值不滚说明库本身或文件占用有问题换目录、确认没有其他进程打开该文件后再试。别在生产日志上做这个实验单独开一个调试目录日志轮转失败的原因还能顺带在目录里查到迹象。5.4 高并发下日志丢失或顺序错乱现象多线程同时写日志跑一段时间后发现总数对不上或者日志行之间的先后顺序和实际执行顺序不一致。原因先区分“丢”和“乱”是两种问题。丢日志常见于你把日志写操作放在一个会提前返回的路径里临时对象析构前程序就退出了也可能是 Appender 内部有队列队列满时策略是丢弃而不是阻塞。顺序乱则通常是多线程下锁的粒度问题不同线程的消息在竞争同一把锁时先获得锁的线程反而可能后进入日志调用导致记录的先后顺序不等于实际执行顺序。解决给每条日志带一个自增序号或时间戳到毫秒级才能在分析时还原真实顺序。如果确认是同一 logger 实例并发写入先做压力测试比如八个线程各写一万条结束后统计文件总行数是否等于八万用这个手段确认有没有丢。丢的话检查是否把 logger 局部化了——每个线程各自创建了 Logger 实例导致 Appender 重复注册或互斥失效。并发量再往上走就要考虑自定义异步 Appender把写入丢到独立线程处理避免业务线程阻塞在日志写盘上。5.5 链接期符号冲突或运行期双份实例现象程序里既有你集成进来的 CuteLogger又通过某个依赖库间接带了一份 CuteLogger链接时出现符号重定义或者运行时日志行为诡异——你注册的 Appender 不生效但另一个模块的日志在打印。原因两份源码编译进了同一个进程导出符号完全一样链接器随机选择其中一个实现导致你调用的和你配置的不是同一份代码。这个问题在大型项目里很隐蔽因为不是每次构建都会报错有时候只是运行期行为不一致。解决在项目里维护且只维护一份 CuteLogger 源码统一通过同一个构建目标引用。集成前先搜一下依赖树里有没有其他模块也 include 了 Logger.h发现重复就删掉一方把它们的引用指向同一份。如果条件不允许动依赖库可以给其中一份源码整体加命名空间或改导出宏但这种方式后续升级维护成本高只能算最后一招。检查方法也简单程序里主动查询 Logger 实例个数和 Appender 注册数量和预期核对数量对不上基本就是双份了。6. 进阶定制 Appender 与验证你的日志体系当你在几个输出端之间反复切换、觉得仍然不够用时就该动手定制 Appender 了。CuteLogger 的输出端全部继承自 AbstractAppender你只需要实现一个 write 虚函数就能定义任意输出行为。下面是一个只放行 Warning 及以上级别的告警 Appender可以把关键日志额外送进单独的告警文件或推给上层逻辑#include AbstractAppender.h class AlertAppender : public AbstractAppender { protected: void write(const QDateTime timestamp, Logger::LogLevel level, const char *file, int line, const char *function, const QString category, const QString message) override { if (level Logger::Warning) { return; // 只关心告警级别以上 } // 这里可以把 message 推到内存环形缓冲、上报服务或写独立告警文件 QFile alertFile(/var/log/app/alert.log); if (alertFile.open(QIODevice::Append | QIODevice::Text)) { QTextStream stream(alertFile); stream timestamp.toString(Qt::ISODate) [ category ] message \n; } } };使用方式和内置 Appender 一致注册给对应 Logger 即可。这里有一个性能教训write 回调是在 Logger 的锁保护下执行的如果你在里面对一个远程服务做同步上报所有共享同一个 Logger 的线程都会被拖住。走自定义 Appender 且涉及耗时操作时正确做法是只做内存写入或投递到自己的工作线程队列不要在 write 里阻塞。定制完别急着上线先验证功能正确性。我的验证习惯是写一个多线程压力小用例N 个线程各写 M 条带编号的日志结束之后统计文件行数确认等于 N 乘 M再把每行末尾的编号取出来排序查重确认没有丢失也没有重复。命令行里一行 awk 就能完成统计wc -l /var/log/app/stress.log awk -F| {print $NF} /var/log/app/stress.log | sort -n | uniq | wc -l两个数字相等说明日志链路在并发下是可靠的这时候才敢把日志体系真正交到生产环境。我自己的习惯是每次集成一个像 CuteLogger 这样的外部库先搭一个最小可运行工程验证生命周期再做并发压测最后才接进业务代码。为了一点日志功能不值得在生产环境里试错。这套路子走通之后你手里就不只是“一个日志库”而是一套有分类、有滚动、有告警、经得起并发验证的日志基础设施。希望帮到你。本文还有配套的精品资源点击获取