带了一圈新项目,日志这块居然成了第一个卡点。项目用C++写Windows桌面客户端,VS开发,日志方案原本是printf加OutputDebugString混着来,查问题的时候恨不得把DBGVIEW的滚动条拉断。后来换成了glog,整个排查路径清晰了不止一个级别。这篇就把在VS的C++工程里接入glog的完整过程写下来,从选方案到配置,从编译踩坑到日常调参,基本照着做就能跑起来。
先说清楚glog是干什么的。它是Google开源的C++日志库,功能很直接:分级日志、条件日志、崩溃时的栈信息输出、日志按大小分片。对一个C++桌面项目来说,这几个能力基本覆盖了绝大部分日志需求。尤其对于长期运行、需要在用户机器上排查问题的Windows程序,日志要是做不好,出了问题只能干瞪眼。
我折腾下来最大的感受是:glog的引入本身不难,难的是在VS的C++项目里把库文件、宏定义、运行库这些零碎配置一次性配对。很多人卡住不是因为glog复杂,而是被MSVC的工程配置坑了,所以下面会把选型、配置、校验、实战调优一路讲透。
1. 在动手之前,先搞清楚glog到底替你干了什么
1.1 为什么C++项目需要一个像样的日志库
有人会问:我自己写个宏,格式化一下OutputDebugString不就行了?小项目确实可以,但一旦项目超过两万行、模块超过三个、需要追查的问题出现在客户机器上,自己造轮子的日子就不好过了。日志需要解决的其实是一组问题:日志分级、日志回滚、崩溃时留证据、性能损耗可控。你写一个宏解决不了所有问题,写了十几个宏之后,这个"日志系统"本身就成了需要维护的负担。
glog天然就把这套东西做好了。它把日志分成INFO、WARNING、ERROR、FATAL四个级别。其中FATAL级别会直接终止程序,这个特性非常关键:程序跑挂了,日志里必然有一条FATAL记录,而且还能配合崩溃信号处理打出调用栈。对于线上问题排查,这套东西的价值远超自己写的临时方案。
1.2 glog的日志格式和使用手感
很多第一次用glog的人,看到日志输出会一愣:
txt复制I20240720 14:30:00.123456 1234 main.cpp:10] hello glog
格式从左到右拆开看:I代表INFO级别,后面是年月日时分秒微秒,再后面是线程ID,然后是源文件名和行号,中括号后面才是真正的日志正文。这个格式初看有点啰嗦,但排查多线程问题的时候非常有用,文件行号直接定位,连IDE断点都不用加。
写日志的语法和流式输出很像:
cpp复制LOG(INFO) << "user login, user_id=" << user_id << ", ip=" << ip;
LOG(WARNING) << "config file missing, use default config";
LOG(ERROR) << "db connection failed, errno=" << errno;
这种流式写法最大的好处是不用操心格式化字符串和参数类型匹配,int、double、std::string直接往里扔,类型安全,也不容易写错占位符。
对于一个C++项目来说,开发效率、排障效率、稳定性,这三样glog都能照顾到。接下来重点就是怎么把它装进VS工程里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 引入glog的三条路线:vcpkg、源码编译、NuGet到底怎么选
2.1 三条路线的横向对比
把glog接进VS的C++工程,不是只有一种办法。我分别试过vcpkg、源码编译、NuGet三种方式,它们各有各的适用场景。
| 引入方式 | 上手难度 | 可控性 | 适合场景 |
|---|---|---|---|
| vcpkg | 低 | 中 | 喜欢用包管理、希望VS自动感知库路径 |
| 源码编译 | 中高 | 高 | 需要修改glog源码、需要自定义构建选项 |
| NuGet | 低 | 低 | 快速实验、临时项目、不想装vcpkg |
2.2 vcpkg是当下最顺手的路子
如果项目是单机开发、没有特别奇怪的构建要求,vcpkg是目前最省心的方案。它在Windows下首次安装可能要花点时间,但后续的库版本升级、依赖管理都是自动的。尤其在VS里配合打开CMake工程或者MSBuild工程,基本能做到"装完库直接能用"。
vcpkg装的不只是glog本身,还会把它的依赖一并解决。glog在Windows下依赖gflags,虽然现在glog 0.6+对gflags的依赖已经弱化了,但vcpkg帮你把这些都处理清楚,省得手动找库的时候发现少了一个传递依赖,编译出来一堆莫名其妙的错误。
2.3 源码编译适合需要改库源码的情况
源码编译适合什么情况?举几个真实场景:某些公司内部的构建服务器不允许联网拉取第三方依赖;或者你打算魔改glog的日志格式,比如把时间戳格式改成特定样式的需求;再或者你需要在日志系统底层叠加一个内部的上报SDK。这些场景下,从源码编译是唯一可控的方案。
2.4 NuGet方案只建议用来快速验证
NuGet在VS里一条命令就能装glog,但它有几个现实问题:包版本通常落后于上游、可选的静态/动态库形态比较受限、出了问题不好定位。我建议把它定位成"临时写个小Demo验证glog语法"的工具,不太建议直接用在正式交付项目里。因为正式打包时你会突然发现NuGet塞进构建产物里的dll路径、版本和你的打包脚本各种不配合。
2.5 我这次的实际选型
这次的项目是典型的Windows桌面端、VS 2022开发、不跨平台,最后选了vcpkg。原因是整个团队都在Windows下,vcpkg的库目录统一,CI环境下只要恢复vcpkg清单就能自动装齐依赖,省得每个人都手工翻GitHub编译一遍。而且VS对vcpkg的集成度已经很高,装上之后基本不用手填包含目录。
3. vcpkg路径下的完整实操:从装包到第一个日志跑起来
3.1 vcpkg安装和glog包安装
先把vcpkg克隆到本地,建议放在一个不带中文和空格的路径下,比如D:\vcpkg。
bat复制git clone https://github.com/microsoft/vcpkg.git
cd vcpkg
bootstrap-vcpkg.bat
接下来安装glog。这里要特别注意arch triplet的选择:项目如果是x64 Debug/Release,安装x64版本;项目如果默认Win32,就是x86版本。选错了后面VS工程匹配不上,链接阶段会报LNK1112之类的架构不匹配错误。
bat复制vcpkg install glog:x64-windows
提示:vcpkg安装glog后会同时处理gflags等依赖。如果公司网络拉取GitHub慢,可以配置镜像源,或者直接在离线环境下载好vcpkg的包缓存再内网安装。
3.2 VS工程自动集成和手工配置的取舍
vcpkg安装完成后,两条路可以走。
第一条是CMake工程路线。VS 2017之后的版本内置了CMake支持,打开项目根目录的CMakeLists.txt即可。vcpkg给CMake提供了toolchain文件,在CMakeLists.txt里通过CMAKE_TOOLCHAIN_FILE指向它,然后find_package(glog CONFIG REQUIRED),再链接glog::glog即可:
cmake复制cmake_minimum_required(VERSION 3.15)
project(GlogDemo)
# vcpkg toolchain
set(CMAKE_TOOLCHAIN_FILE "D:/vcpkg/scripts/buildsystems/vcpkg.cmake")
find_package(glog CONFIG REQUIRED)
add_executable(GlogDemo main.cpp)
target_link_libraries(GlogDemo PRIVATE glog::glog)
用VS打开这个CMake目录后,VS会自动读取CMake配置,生成缓存,然后就能直接编译了。这个方法的好处是构建逻辑全部写在CMakeLists.txt里,换机器、换人接手都能稳定复现。
第二条是传统MSBuild工程路线。如果你现在用的是.vcxproj老工程,不想迁移到CMake,那么执行一次:
bat复制vcpkg integrate install
这个命令会把vcpkg的库搜索路径注册到VS里。之后绝大多数情况下,你的.vcxproj工程会自动感知include目录和lib目录。但我在多个项目里遇到过vcpkg集成后依然找不到头文件的情况,这种时候别慌,打开项目属性,在"VC++目录"里手动检查包含目录是否自动添加了vcpkg的include路径,没有的话补上即可。
3.3 宏定义:这是新手最容易漏的一步
glog在Windows下使用动态库时,要求使用方定义GLOG_USE_GLOG_EXPORT宏。这个宏的作用是让glog头文件里的函数声明带上dllimport属性,正确地从glog.dll里导入符号。
如果你不定义这个宏,最典型的错误是链接阶段报一堆无法解析的外部符号,比如:
txt复制无法解析的外部符号 "__declspec(dllimport) public: ... google::LogMessage::LogMessage(...)"
在VS里配置:
- 项目右键 -> 属性 -> C/C++ -> 预处理器 -> 预处理器定义
- 加上
GLOG_USE_GLOG_EXPORT - 如果安装的是静态库版本,则改成
GLOG_STATIC_DEFINE
如果用的是CMake和glog::glog target,那vcpkg的配置文件已经帮你处理好了这套宏,不需要手动加。这就是我推荐CMake路线的原因之一。
3.4 Debug和Release的链接库切换
vcpkg安装的glog默认同时生成了Debug和Release的库文件,文件名不同,VS的链接器在选择时依赖你当前的活动配置。
| 配置 | 库文件 |
|---|---|
| Debug | glogd.lib + glogd.dll |
| Release | glog.lib + glog.dll |
在使用CMake的glog::glog target时,这个切换是自动的。但如果你是手工配置.vcxproj,就需要在"链接器 -> 输入 -> 附加依赖项"里分别给Debug和Release填不同的库名。很多人把Release的glog.lib填在了Debug配置下,结果编译通过、运行时因为glogd.dll缺失直接崩,非常隐蔽。
3.5 第一个能跑起来的示例
到这里配置就齐了,写一个最小示例验证:
cpp复制#include <glog/logging.h>
int main(int argc, char* argv[]) {
google::InitGoogleLogging(argv[0]);
FLAGS_log_dir = "./logs";
FLAGS_alsologtostderr = true;
LOG(INFO) << "glog init success";
LOG(WARNING) << "this is a warning";
LOG(ERROR) << "this is an error";
google::ShutdownGoogleLogging();
return 0;
}
这段代码有几个点要解释。InitGoogleLogging建议在程序入口就调用,它会记录程序名,用于日志文件命名。FLAGS_log_dir指定日志输出目录,这个目录必须已经存在,glog不会自动创建多级目录,我第一次用的时候没建./logs,结果INFO日志怎么都不落盘,一度以为配置错了。FLAGS_alsologtostderr是让日志同时打到终端和文件,开发调试时非常有用,日志文件里留底,终端上看实时输出。
编译后运行,日志文件会生成在./logs目录下,命名格式类似:
txt复制GlogDemo.主机名.用户名.log.INFO.20240720-143000.1234
看到这个文件,就说明glog已经成功跑起来了。
4. 源码编译方式:用CMake生成VS工程,再把链接细节接好
4.1 为什么有人偏偏要源码编译
虽然vcpkg方便,但有一种需求它满足不了:改库。比如想把glog默认的<<日志输出改成JSON格式,或者想调整日志前缀的分隔符,再或者需要在库里加一层定时上报的钩子。这种时候拿现成的二进制库没法操作,必须从源码编译。
从GitHub拉glog源码,然后直接通过CMake生成VS工程:
bat复制git clone https://github.com/google/glog.git
cd glog
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
-G指定VS生成器版本,-A x64指定目标架构。生成完成后,在build目录下能找到glog.sln,用VS打开可以进一步编译,也可以直接在上面这条命令行里完成编译。
4.2 源码输出的文件要认清楚
编译完成后,Release配置会在build/Release目录下生成这几个关键文件:
glog.dll:运行时动态库glog.lib:链接用的导入库glog.dll.lib:和glog.lib实际上是同一类东西,对应动态库的导入符号
在你的工程里,需要做三件事:把源码的src目录加进附加包含目录;把build/Release目录加进附加库目录;把glog.lib写进附加依赖项。同时,预处理器定义依然要加GLOG_USE_GLOG_EXPORT。如果编译时选择了Debug配置,注意链接的是glogd.lib。
4.3 动态库还是静态库,怎么选
源码编译方式给了你一个vcpkg不直接给的选择:动态库还是静态库。CMake配置里可以指定:
bat复制cmake -B build -G "Visual Studio 17 2022" -A x64 -DGLOG_STATIC_LINKING=ON
静态库模式下,运行时不再需要glog.dll,部署包少一个文件。但代价是:如果多个模块都静态链接了一份glog,每个模块内部各自的LOG宏会复制出来同一套全局状态,日志会出现莫名串台。所以多模块项目我一般都建议用动态库,glog只有一份,全局FLAGS也只有一个实例。
动态库方式部署时要记得把glog.dll和exe放在一起,或者放到系统PATH环境变量能找得到的地方。glog.dll本身编译时没有额外配置的话,不依赖其他第三方运行时,大体上拷贝就能用,但仍建议把VC运行时库也一并带上,避免目标机器缺运行环境。
4.4 源码编译时常见的几个坑
源码编译有几个vcpkg方式不需要操心的坑,这里集中说一下。
第一个坑是CMake配置时默认会去拉取gtest和google benchmark,如果网络不好会卡住。可以在CMake配置时加参数关掉:
bat复制cmake -B build -G "Visual Studio 17 2022" -A x64 -DBUILD_TESTING=OFF
不关掉的话,构建时gtest那堆编译错误很容易让你误以为glog本身有问题。
第二个坑是Windows SDK版本。VS 2019和VS 2022项目在生成时,CMake默认找最新安装的Windows SDK,如果机器上有多个版本,可能出现SDK版本不匹配的警告。如果遇到,可以在CMake配置时指定-DCMAKE_SYSTEM_VERSION=10.0.xxxxx.0,强制使用你想用的SDK版本。
第三个坑是多线程运行库的选择。代码里如果用/MT,那么glog编译时也要用/MT,否则链接器会报LNK2038 运行时库不匹配。这个坑在第5章重点讲。
5. 实测中遇到最多的四类问题:链接、运行库、目录、模式
5.1 LNK2038运行时库不匹配:glog最常见的失败方式
这是我把glog推给同事时出现频率最高的问题。链接时报错长这样:
txt复制LNK2038: 检测到“RuntimeLibrary”的不匹配项: 值“MD_DynamicRelease”不匹配值“MT_StaticRelease”
原因不复杂。VS工程默认C++运行库是/MD(多线程DLL),但团队某些老工程为了减少DLL依赖,改成了/MT(多线程静态链接)。glog在vcpkg里默认按动态CRT编译,两边CRT不一致,链接器直接拒绝干活。
解决办法按你的实际诉求来:
- 如果你希望项目用
/MT,那么在vcpkg里安装静态CRT版本:vcpkg install glog:x64-windows-static - 如果你希望保持
/MD,那就把VS工程的项目属性 -> C/C++ -> 代码生成 -> 运行库改回/MD,或者确保用的是动态库版本。
vcpkg的triplet里,x64-windows默认是动态CRT,x64-windows-static是静态CRT。这个对应关系搞清楚,LNK2038基本不会再找上你。
5.2 无法解析的外部符号:多半是宏没定义
用动态库却没有定义GLOG_USE_GLOG_EXPORT时,链接器会报大量无法解析的外部符号,而且报错的行号全部指向glog/logging.h内部。初学者很容易懵,以为是头文件没配对。
这个问题的本质是Windows下符号导入导出的规则:没有dllimport属性时,编译器认为这个函数在当前模块内实现,链接器自然找不到。定义宏之后,头文件里的函数声明会带上__declspec(dllimport),链接器就会去导入库glog.lib里找。
我的建议是在项目预处理器定义里统一加,而不是在代码里到处#define。这样团队成员接手时一眼能看到项目用了什么宏,不会把宏写在某个角落文件里变成隐性知识。静态库用户则反过来,要加GLOG_STATIC_DEFINE。
5.3 日志文件没有生成:先检查目录和初始化
日志没生成,有一种情况是文件写到了预期之外的路径。glog在没有设置FLAGS_log_dir时,默认日志写到当前工作目录(CWD)。VS调试时会话的工作目录默认是工程目录,但直接双击exe运行的话是exe所在目录,两者不一致,日志自然"不见了"。
建议代码里初始化时顺手把日志目录固定住,并且主动创建目录。C++ 17可以用filesystem,老项目就手动建目录:
cpp复制#include <filesystem>
namespace fs = std::filesystem;
fs::create_directories("./logs");
FLAGS_log_dir = "./logs";
另外如果用了FLAGS_logtostderr = true,那么所有日志输出到stderr,文件里什么都不会有。调试的时候想同时兼顾文件和终端,用FLAGS_alsologtostderr = true,这个问题我之前专门排查过,一度以为库坏了。
5.4 Debug和Release混用:dll加载失败和写入失败
把Debug配置的exe配了Release的glog导入库,这种情况编译可能通过,但运行时会报找不到glog.dll或无法定位程序输入点,因为Debug库文件名是glogd.lib和glogd.dll,和你手填的glog.lib不是同一个文件。
稳妥的做法是:链接器输入项里用$(SolutionDir)Libs\$(Configuration)\这种按配置区分的路径写法,或者依赖CMake target自动切。源码编译场景下尤其要注意,Debug和Release两套build输出目录不在一起,别链接错。
6. 跑通之后,把glog调成真正趁手的样子
6.1 日志分级和条件日志:别一股脑全INFO
glog的四级日志已经覆盖了大多数场景,但很多人直接就把什么都写INFO,结果DEBUG日志和重要错误混在一起,想单独盯ERROR却刷不过来。我的分法是这样:
- INFO:程序生命周期关键节点,比如启动、退出、配置加载完成
- WARNING:不影响主流程但值得关注的异常,比如配置文件缺失后走默认值
- ERROR:功能失败的路径,比如数据库连接失败、接口返回错误码
- FATAL:不可恢复的致命错误,比如核心数据结构初始化失败
glog还提供了一组条件日志宏,非常适合控制日志量:
cpp复制// 仅当指定的用户ID命中实验组时打日志
LOG_IF(INFO, user_id % 100 == 0) << "sampled user login, user_id=" << user_id;
// 同一个语句每10次执行才打一次日志
LOG_EVERY_N(INFO, 10) << "polling status, pending_count=" << count;
// 结合条件和频率
LOG_IF_EVERY_N(WARNING, count > 100, 20) << "queue too large, count=" << count;
这些宏比在业务代码里自己包一层if加计数器要直观得多,而且glog内部对频率控制做了原子操作,多线程环境下不会因为计数器竞争出问题。
6.2 FLAGS全局配置:日志切割、错误级别过滤
glog有一批全局FLAGS,运行时改起来很方便。我实际用得最多的几个:
| FLAGS | 作用 | 推荐值 |
|---|---|---|
FLAGS_log_dir |
日志输出目录 | 程序根目录下logs |
FLAGS_max_log_size |
单个日志文件大小上限,单位MB | 10~100 |
FLAGS_stop_logging_if_full_disk |
磁盘写满时停止写日志,防止拖垮系统 | true |
FLAGS_minloglevel |
低于该级别的日志不输出 | 0 |
FLAGS_v |
VLOG的详细级别 | 按模块需求 |
FLAGS_max_log_size一到阈值,glog会自己切新文件,文件名里带上新的时间戳。这个切割粒度对日常排查足够用了,不需要再写外部脚本去分割日志。
6.3 崩溃栈信息:InstallFailureSignalHandler的价值
Windows下程序崩溃,默认弹一个"已停止工作"对话框,但调用栈信息经常拿不到。glog自带信号处理函数:
cpp复制google::InstallFailureSignalHandler();
调用之后,程序收到段错误、非法指令、abort等信号时,glog会在崩溃点把当前线程调用栈写到FATAL日志里。这个栈在Release版里虽然只有地址,但配合map文件或第三方符号解析工具,定位原生崩溃问题的时间和成本能降一个量级。
对于客户端程序,这行代码一定要加。它相当于程序坠毁前的黑匣子,很多线上疑难crash就是靠这一条日志定位到具体代码行的。
6.4 日志的清理策略:glog不管这个,得自己管
glog不会自动删除旧日志,跑久了logs目录会越来越大。这不是bug,日志库一般只管写不管清理。我的做法是在程序启动时顺手清理超过保留天数的日志文件:
cpp复制void CleanOldLogs(const std::string& dir, int keep_days) {
namespace fs = std::filesystem;
auto deadline = fs::file_time_type::clock::now() - std::chrono::hours(24 * keep_days);
for (const auto& entry : fs::directory_iterator(dir)) {
if (entry.path().extension() == ".log") {
auto last_write = fs::last_write_time(entry.path());
if (last_write < deadline) {
fs::remove(entry.path());
}
}
}
}
注意这里只删以.log结尾的文件,而且要判断目录路径是否合法,别把别的文件当成日志删了。定时任务或者程序启动时调用一次都行,我倾向于启动时清理一次,避免运行运行时做文件遍历影响性能。
6.5 日志性能:别在热点路径上写大日志
glog虽然做了缓冲,但每一条日志最终都要落盘。在高频循环里逐帧打日志,即使分级正确也会拖慢主线程。我在实际项目里总结出的经验是:日志量超过每秒几百条的场景,要么用LOG_EVERY_N做采样,要么把日志内容先拼到字符串缓冲里,异步线程统一写。
另外glog的LOG(INFO)是有开销的,就算不打到文件,构造LogMessage对象本身也有成本。所以判断日志等级的宏还是要用起来,比如:
cpp复制if (VLOG_IS_ON(2)) {
VLOG(2) << "sensor raw data: " << sensor_dump;
}
这段代码在VLOG(2)不开启时连字符串拼接都不会执行,适合批量输出调试信息的场合。
写在最后
我接手这个项目时,日志系统还是个半成品,用的宏满天飞,输出格式五花八门。换到glog之后,顺手把初始化入口统一封装了一下,团队成员现在只要按格式写LOG就行,排查问题直接翻logs目录按级别过滤,效率确实上来了。
最后分享一个实际操作中的小经验:glog的配置最好收敛到一个工具模块里,不要在Main函数外散落各处FLAGS赋值。整个项目只有一处InitLogging(),日志目录、保留天数、崩溃处理器、分片大小全部在里面设置好,后续调整参数时只改一处,团队成员也好理解。如果哪天要把glog替换成别的日志库,也只需要动这一个文件,动一次就知道全局还有没有遗漏的引用。
