PowerToys 日志系统实战指南:日志存储路径、spdlog 与 ManagedCommon Logger 的完整实现
本文以 PowerToys 仓库的开发者日志文档(logging.md)为核心,系统讲解 PowerToys 中日志文件的存储位置、C++ 侧基于 spdlog 的日志集成方式、C# 侧 ManagedCommon Logger 类的完整用法,并结合仓库源码深入剖析日志初始化、级别控制、按天滚动保留以及 BugReportTool 日志收集的实现细节。读完后你将能够在新 PowerToy 模块中正确接入日志、按模块定位日志文件,并理解每条日志落盘背后的调用链。
日志文件存储在哪里
PowerToys 的日志文件分布在两个根目录下,区分依据是进程的权限级别:
- 普通权限进程:大多数日志保存在
%LOCALAPPDATA%\Microsoft\PowerToys下; - 低权限进程(如注册为预览处理器的 preview handler):日志保存在
%USERPROFILE%\AppData\LocalLow\Microsoft\PowerToys下。
日志通常按模块名存放在各自的子文件夹中。当需要反馈问题时,BugReportTool 会同时从这两个位置收集日志。
从源码结构看,这个"双根目录"的约定在 C# 侧 Logger 类中有明确实现。Logger.cs 的 LogDirectoryPath 方法根据 isLocalLow 参数选择基础路径:
public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false)
{
string basePath;
if (isLocalLow)
{
basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
}
else
{
basePath = Constants.AppDataPath() + applicationLogPath;
}
string versionedPath = Path.Combine(basePath, Version);
return versionedPath;
}
除了权限维度,日志路径还叠加了版本号维度:InitializeLogger 会在模块日志目录下再拼上当前程序集的版本号(AssemblyFileVersionAttribute),得到形如 %LOCALAPPDATA%\Microsoft\PowerToys\<模块>\Logs\<版本号>\ 的最终目录,日志文件则命名为 Log_yyyy-MM-dd.log(见 Logger.cs)。这意味着升级 PowerToys 后新旧版本日志天然隔离,InitializeLogger 还会在后台任务中清理旧版本日志文件夹,仅删除最旧的 3 个旧版本目录(见 Logger.cs 的 DeleteOldVersionLogFolders)。
C++ 项目接入日志:spdlog 与 Logger 静态类
通过 spdlog.props 引入 spdlog
在 C++ 项目中,PowerToys 使用 spdlog 库做日志,库本身位于 deps 目录。在 .vcxproj 中导入 spdlog.props 即可完成集成:
<Import Project="..\..\..\deps\spdlog.props" />
它会自动添加所需的 include 目录并链接库二进制。查看当前仓库的 spdlog.props 可以看到,它现在负责为所有 spdlog 消费者注入三个预处理器定义:
<PreprocessorDefinitions>SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;%(PreprocessorDefinitions)</PreprocessorDefinitions>
其中 SPDLOG_WCHAR_TO_UTF8_SUPPORT 与 SPDLOG_WCHAR_FILENAMES 保证 Windows 宽字符路径/消息能正确写入日志文件,SPDLOG_COMPILED_LIB 表示链接预编译库而非 header-only 使用。从该文件的注释可以推断,vcpkg 集成本身(VcpkgRoot、manifest install 等)已上移到 Cpp.Build.props,spdlog.props 只保留与旧 in-tree 构建一致的编译期定义。
Logger 封装类:接口与约束
spdlog 之上,仓库提供了统一的封装类 Logger(位于 src/common/logger/ 目录,由 logger.vcxproj 构建为静态库)。该类禁止实例化(Logger() = delete),对外暴露一组静态模板方法,透传到内部的 spdlog::logger:
static void trace(const FormatString& fmt, const Args&... args);
static void debug(const FormatString& fmt, const Args&... args);
static void info(const FormatString& fmt, const Args&... args);
static void warn(const FormatString& fmt, const Args&... args);
static void error(const FormatString& fmt, const Args&... args);
static void critical(const FormatString& fmt, const Args&... args);
static void flush();
需要注意源码中每处 API 上方的明确注释:"log message should not be localized"(日志消息不应本地化)。这与 C# 侧日志同样不做本地化的做法一致,目的是保证日志内容跨语言版本可比对,便于检索和问题定位。
初始化细节:按天滚动、调试双写与失败兜底
Logger::init(loggerName, logFilePath, logSettingsPath) 的核心实现在 logger.cpp:
- 日志级别来自 JSON 配置:
getLogLevel从配置文件读取logLevel字段,支持trace、debug、info、warn、err、critical、off七个取值;配置缺失或取值非法时回退到默认级别trace(映射表见 logger.cpp)。配置文件的读写由 logger_settings.cpp 完成——首次调用时若文件不存在会自动创建默认配置,所以每个日志根目录下的配置 JSON 是运行时自动生成的。 - 按天滚动的文件 sink:使用
daily_file_sink_mt创建 logger,保留天数由LogSettings::retention控制,当前值为 30 天(见 logger_settings.h)。 - 调试器附加时双写:当
IsDebuggerPresent()为真时,额外挂一个msvc_sink_mt,把同一份日志同时输出到 VS 输出窗口,方便开发期实时观察。 - 统一格式与自动刷盘:日志格式为
[%Y-%m-%d %H:%M:%S.%f] [p-%P] [t-%t] [%l] %v,包含毫秒级时间戳、进程 ID、线程 ID 和级别;并设置flush_on(logLevel),保证每条日志立即落盘。 - 失败兜底:若创建 logger 抛异常,会退化为
null_logger_mt(日志静默丢弃),并通过logFailedShown环境变量防止弹窗重复轰炸,仅在首次失败时弹出"Logger cannot be initialized"错误框。
Runner 主进程的实际调用在 main.cpp:
Logger::init(LogSettings::runnerLoggerName, logFilePath.wstring(), PTSettingsHelper::get_log_settings_file_location());
各模块的 logger 名称与日志路径集中定义在 logger_settings.h 中,例如 runner 日志为 RunnerLogs\runner-log.log、FancyZones 为 fancyzones-log.log、预览/缩略图处理器统一放在 logs\FileExplorer_localLow\ 子目录下(因为它们是低权限进程,落在 LocalLow 路径树中)。此外,Logger::init 还有一个 std::vector<spdlog::sink_ptr> 重载(见 logger.cpp),允许调用方完全自定义 sink 组合,为非常规输出场景预留了扩展点。
C# 项目接入日志:ManagedCommon 的 Logger 类
C# 项目使用 Managed Common 中的静态 Logger 类(src/common/ManagedCommon/Logger.cs)。接入步骤:
- 给项目添加对
ManagedCommon的项目引用; - 在需要使用日志的文件中加
using ManagedCommon;; - 在
Main函数(或App.xaml.cs中的App等入口函数)调用InitializeLogger,传入相对模块名的日志子路径(注意开头带反斜杠的路径方案):
Logger.InitializeLogger("\\FancyZones\\Editor\\Logs");
- 低权限进程(例如以低权限运行的文件预览组件)必须把第二个参数置为
true,日志才会落到 LocalLow 根目录:
Logger.InitializeLogger("\\FileExplorer\\Monaco\\Logs", true);
InitializeLogger 内部会创建版本化日志目录、把 TextWriterTraceListener 挂到 System.Diagnostics.Trace 上,并开启 Trace.AutoFlush,日志底层即由 Trace.WriteLine 输出。
可用的日志函数
Logger 提供以下日志函数(签名与文档一致,见 Logger.cs):
// Logs an error that the utility encountered
Logger.LogError(string message);
Logger.LogError(string message, Exception ex);
// Logs an error that isn't that grave
Logger.LogWarning(string message);
// Logs what the app is doing at the moment
Logger.LogInfo(string message);
// Like LogInfo just with infos important for debugging
Logger.LogDebug(string message);
// Logs the current state of the utility.
Logger.LogTrace();
结合源码可以进一步理解每个函数的行为细节:
LogError(message, ex):除消息外还会记录异常类型、HResult、消息文本,若存在InnerException会一并展开,最后附上完整的 Stack trace;LogDebug:受#if DEBUG条件编译保护,发布构建中调用不会写入任何内容,是零成本的调试开关;LogTrace:无消息参数,专门用于标记"程序当前状态点",适合在关键路径上打桩;- 调用者信息自动注入:所有函数通过
[CallerMemberName]、[CallerFilePath]、[CallerLineNumber]自动捕获调用位置,日志头部形如[时间] [级别] 文件名::成员名::行号,无需调用方手工传参; - 每条日志行格式为
"[HH:mm:ss.fff] [级别] CallerInfo",正文换行缩进输出(见 Logger.cs 的Log私有方法)。
用 BugReportTool 收集并上报日志
排查线上问题时不必手动翻找目录。BugReportTool 执行时会把两个根目录的日志都打包带走:它先把 %LOCALAPPDATA%\Microsoft\PowerToys 设置根目录整体复制到临时报告目录,再把 LocalLow 路径下的 logs 子树复制进来(见 Main.cpp),随后附上安装结构、Windows 设置、显示器信息、系统版本等诊断内容,调用系统自带的 tar.exe 压缩为 zip(路径解析见 zipfolder.cpp)。打包前会通过 HideUserPrivateInfo 对报告目录做隐私脱敏,压缩产物默认保存在桌面(可通过命令行参数自定义保存路径),并顺带收集 PowerToysMSIInstaller_* 前缀的安装器日志。
小结
PowerToys 的日志体系可以归纳为三层:存储约定(LOCALAPPDATA 与 LocalLow 双根目录 + 模块子目录 + 版本化目录)、C++ 侧 spdlog 封装(按天滚动、30 天保留、调试双写、级别由 JSON 配置驱动)、C# 侧 ManagedCommon Logger(版本隔离、自动调用者信息、旧版本目录清理)。新模块开发时,按本文路径接入对应一侧的 Logger 即可;排障时则直接到对应根目录的模块子文件夹下按日期查找日志文件,或用 BugReportTool 一键打包完整日志集。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00