PowerToys 日志与遥测体系解析:从 spdlog/Trace 文件日志到 ETW 遥测与 BugReportTool
PowerToys 作为包含 40 余个模块的 Windows 生产力工具集,其可观测性由四套互补机制构成:文本文件日志、ETW 遥测、事件查看器日志与 Watson 崩溃报告。本文以仓库中的 开发文档 为主线,结合 src/common 下的日志、遥测实现源码,完整讲清日志的存放位置、C++/C# 双栈的接入方式、日志级别设置、遥测的注册与用户控制,以及 Bug Report Tool 的触发路径,帮助你在二次开发或排查问题(包括低权限组件)时快速定位日志、正确使用 Logger 与遥测 API。
一、PowerToys 的四种日志机制
PowerToys 的日志/诊断能力分为四类,各机制面向不同场景:
- 文本文件日志:应用直接写入本地文件,是日常排查问题的主要手段,C# 与 C++ 组件都使用;
- 遥测/诊断数据(Telemetry):基于 Windows 的 ETW(Event Tracing for Windows)发送,与文本文件日志是完全独立的一套体系;
- 事件查看器(Event Viewer)日志:部分工具如 Mouse Without Borders 使用;
- Watson 报告:发送给微软的崩溃报告。
理解这四点的关键在于区分「本地文件日志」与「ETW 遥测」:前者写盘、用户可直接阅读,后者经过注册表开关控制、最终服务于产品改进,两者的 API、开关、存储位置互不相同。
二、日志文件存放位置:三条路径规则
常规日志(%LOCALAPPDATA%)
- 位置:
%LOCALAPPDATA%\Microsoft\PowerToys\Logs - 按工具(utility)划分目录,有时再按版本细分
- 典型内容:PowerToys Run 日志、模块接口日志(LogsModuleInterface)
- C# 与 C++ 组件均写入此处
低权限日志(LocalLow)
预览处理器(Preview Handler)与缩略图提供器(Thumbnail Provider)由 Explorer 进程拉起,运行在低权限上下文,无法访问 %LOCALAPPDATA%,因此写入:
- 位置:
%USERPROFILE%/AppData/LocalLow/Microsoft/PowerToys - 典型示例:Monaco 预览处理器日志
这一点在 C# 的 Logger 实现中有直接印证。Logger.cs 的 LogDirectoryPath 方法根据 isLocalLow 参数选择基路径(约 L75-L89):
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(path, true) 会落到 LocalLow,默认则落在 AppDataPath() 之下。此外,日志目录还会再拼接一层版本子目录(Version 取自程序集的 AssemblyFileVersionAttribute),这就是文档中「有时按版本组织」的来源。
C++ 侧的低权限日志路径常量
C++ 侧的路径并非散落各处,而是集中在 logger_settings.h 中。可以看到各预览/缩略图组件都使用 logs\FileExplorer_localLow\ 前缀,例如:
inline const static std::wstring monacoPrevLogPath =
L"logs\\FileExplorer_localLow\\MonacoPrevHandler\\monaco-prev-handler-log.log";
inline const static std::wstring svgPrevLogPath =
L"logs\\FileExplorer_localLow\\SvgPreviewHandler\\svg-prev-handler-log.log";
同一文件还集中定义了各模块的日志名与路径,如 launcherLogPath = L"LogsModuleInterface\\launcher-log.log"、awakeLogPath = L"Logs\\awake-log.log"、fancyZonesLogPath = L"fancyzones-log.log" 等,覆盖 launcher、FancyZones、Keyboard Manager、Hosts、RegistryPreview、Workspaces 等各模块——排查某个模块时,先来这里查它的日志文件名是最快的方式。
模块日志的 per-user 特性
- 无论安装方式如何(用户级或机器级安装),模块日志始终存放在当前用户的 AppData 下;
- 每个模块创建自己的日志文件;
- 机器级安装下日志仍然是 per-user 的,不同用户看到的日志可能不同。
这意味着排查「某台装了 PowerToys 的机器」的问题时,必须确认是以哪个 Windows 用户身份运行模块进程,日志目录可能各不相同。
三、C++ 日志实现:spdlog 封装
接入方式
C++ 项目使用 spdlog 库(在 deps 目录管理;仓库中可通过 deps/vcpkg-overlays/spdlog 查看其 vcpkg 覆盖配置)。按文档说明,在自己的 .vcxproj 中导入 spdlog.props 即可:
<Import Project="..\..\..\deps\spdlog.props" />
它会加入所需 include 目录并链接库本身。查看 spdlog.props 的当前内容可以发现,它携带了三个关键预处理器定义,这直接解释了日志为什么能处理中文路径与宽字符消息:
<PreprocessorDefinitions>
SPDLOG_WCHAR_TO_UTF8_SUPPORT;SPDLOG_COMPILED_LIB;SPDLOG_WCHAR_FILENAMES;
%(PreprocessorDefinitions)
</PreprocessorDefinitions>
- 项目还需把公共的 logging 项目(
src/common/logger)作为依赖; - 日志在主文件中一次性初始化(
init_logger()),之后任何文件都可直接使用日志接口。
初始化源码:Logger::init
实际封装类位于 logger.h,它基于 spdlog::logger 提供 trace/debug/info/warn/error/critical 六个格式化日志方法,并在源码注释中明确要求「log message should not be localized」(日志消息不要本地化)。核心的 Logger::init 实现了几件事:
auto sink = make_shared<daily_file_sink_mt>(logFilePath, 0, 0, false,
LogSettings::retention);
if (IsDebuggerPresent())
{
auto msvc_sink = make_shared<msvc_sink_mt>();
msvc_sink->set_pattern("[%Y-%m-%d %H:%M:%S.%f] [%n] [t-%t] [%l] %v");
logger = make_shared<spdlog::logger>(loggerName, sinks_init_list{ sink, msvc_sink });
}
// ...
logger->set_level(logLevel);
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%f] [p-%P] [t-%t] [%l] %v");
logger->flush_on(logLevel); // Auto flush on every log message.
从中可以读出几个实现细节:
- 按天滚动文件:
daily_file_sink_mt每天切分日志文件,且保留天数由LogSettings::retention控制——在 logger_settings.h 中定义为retention = 30,即旧日志文件保留 30 天; - 调试双通道:
IsDebuggerPresent()为真时额外挂一个msvc_sink_mt,让断点调试时日志同时出现在 VS 输出窗口; - 统一格式:
[时间] [进程ID] [线程ID] [级别] 消息; - 每级自动 flush:
flush_on(logLevel)保证低级别日志不丢缓冲; - 失败兜底:初始化异常时回退到
null_logger并弹出一次「Logger cannot be initialized」错误框(用环境变量logFailedShown避免重复弹窗)。
此外还有一个 Logger::init(std::vector<spdlog::sink_ptr> sinks) 重载,允许调用方完全自定义 sink 组合,例如设置 UI 等不写文件的组件。
日志级别与 settings.json
文档指出:日志级别设置保存在 settings.json 中,但并非所有 API 都遵循这些设置。C++ 侧的解析逻辑在 logger_settings.cpp 的 get_log_settings:读取 JSON 中的 logLevel 字段,文件不存在时会自动创建一份默认设置文件。
级别映射在 logger.cpp 中定义:
const std::unordered_map<std::wstring, level_enum> logLevelMapping = {
{ L"trace", level_enum::trace }, { L"debug", level_enum::debug },
{ L"info", level_enum::info }, { L"warn", level_enum::warn },
{ L"err", level_enum::err }, { L"critical", level_enum::critical },
{ L"off", level_enum::off },
};
从源码看,LogSettings 的 defaultLogLevel 是 L"trace"——即默认按 trace 级别记录,所以 info 及以上级别的日志默认都会写入文件;若用户把 logLevel 调成 info/warn,Logger::debug/trace 的调用会被 spdlog 过滤掉。这与文档「默认写 info 级日志、debug/trace 可能不写」的描述一致:级别由 settings.json 的 logLevel 决定,取值可用上表七个字符串。
四、C# 日志实现:ManagedCommon 的静态 Logger
C# 项目统一使用 ManagedCommon 中的静态类 Logger(src/common/ManagedCommon/Logger.cs)。接入方式:
- 项目引用
ManagedCommon; - 使用日志的文件加上
using ManagedCommon;; - 在
Main(或App.xaml.cs中的App等入口)调用InitializeLogger,路径按模块自定义:
Logger.InitializeLogger("\\FancyZones\\Editor\\Logs");
低权限进程(如预览处理器)把第二个参数设为 true:
Logger.InitializeLogger("\\FileExplorer\\Monaco\\Logs", true);
InitializeLogger 做了什么
结合 Logger.InitializeLogger 的源码:
string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
// 1. 不存在则创建版本化日志目录
// 2. 生成当日日志文件:Log_yyyy-MM-dd.log
var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", ...) + ".log";
Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
Trace.AutoFlush = true;
// 3. 后台任务清理旧版本的日志目录
Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
底层是 System.Diagnostics.Trace + TextWriterTraceListener:每条日志写入当日的 Log_2026-09-05.log 一类文件,Trace.AutoFlush = true 保证实时落盘。值得注意的是 DeleteOldVersionLogFolders:它按创建时间排序、保留当前版本目录,删除最旧的若干旧版本日志目录(当前实现为 Take(3)),这正是文档所说「部分模块有社区贡献的旧日志清理、但并非全局实现」的具体例证——该清理逻辑目前只作用于 C# Logger 覆盖的模块。
日志函数与自动采集的调用方信息
Logger 提供的函数与文档一致:
Logger.LogError(string message); // 工具遇到的错误
Logger.LogError(string message, Exception ex); // 附带异常的错误
Logger.LogWarning(string message); // 不那么严重的错误
Logger.LogInfo(string message); // 当前行为记录
Logger.LogDebug(string message); // 仅 DEBUG 构建生效
Logger.LogTrace(); // 状态跟踪点
源码中有两个实现细节值得注意:
LogDebug包在#if DEBUG中(Logger.cs),Release 构建下该调用直接不产生日志——这解释了文档「Debug 和 trace 日志默认可能不写入」;- 所有日志方法通过
[CallerMemberName]、[CallerFilePath]、[CallerLineNumber]自动附加调用方信息(Logger.cs),每行日志形如[hh:mm:ss.fff] [Info] 文件名::方法名::行号,无需手动传上下文即可回溯到源码位置。异常重载还会展开异常类型、HResult、InnerException 与完整堆栈。
五、日志文件管理现状
- 目前多数日志不会被自动清理;C# Logger 的版本目录清理(上文
DeleteOldVersionLogFolders)与 C++ 侧daily_file_sink_mt的 30 天保留是两个局部的例外; - 不同模块的清理策略不一致,排查磁盘占用时以实际目录为准;
- 级别过滤依赖 settings.json 的
logLevel,但「不是所有 API 都遵循这些设置」,例如 C# 的LogDebug由编译配置而非logLevel决定。
六、遥测(Telemetry):基于 ETW 的独立体系
总体机制与密钥管理
遥测使用 Windows 的事件跟踪(ETW),与文件日志完全解耦。把数据发送到正确服务器需要一组密钥,仓库对这些密钥做了特殊处理:
- 密钥不存储在仓库中;
- 公开代码里以混淆形式出现;
- 发布流程中会被真实值替换;
- 发布构建时存放于私有 NuGet 包中。
因此公开仓库中的遥测代码可以正常编译,但只有在发布流水线替换密钥后才能把数据上报到微软的服务器。
C++ 遥测:trace_base.h
C++ 侧遥测由 TraceBase.h 管理,职责正如文档所述:注册 provider、检查用户是否禁用诊断、各模块再基于其定义事件。源码中可以看到核心逻辑:
#define TraceLoggingWriteWrapper(provider, eventName, ...) \
if (IsDataDiagnosticsEnabled()) \
{ \
TraceLoggingWrite(provider, eventName, __VA_ARGS__); \
}
TraceBase::RegisterProvider() / UnregisterProvider() 负责 TraceLoggingRegister / TraceLoggingUnregister;IsDataDiagnosticsEnabled()(TraceBase.h)读取注册表:
- 键:
HKCU\Software\Classes\PowerToys - 值:
AllowDataDiagnostics(DWORD)
也就是说,用户在设置页关闭遥测后,该值变为 0,C++ 侧所有 TraceLoggingWriteWrapper 包裹的写入点都会直接短路——这与文档「检查用户是否禁用诊断」完全对应。模块侧的使用示例(来自 Always On Top):
Trace::AlwaysOnTop::Enable(true);
ETW 定义相关的基础设施集中在 src/common/Telemetry 目录(ProjectTelemetry.h、TraceLoggingDefines.h、TelemetryBase.cs 以及 ETW 工程 EtwTrace 与采集模板 PowerToys.wprp)。
C# 遥测
C# 侧使用 PowerToys.Telemetry 项目中的 PowerToysTelemetry 类,通过 WriteEvent 发送事件,项目引用该项目后即可调用。例如 launcher 的展示事件:
PowerToysTelemetry.Log.WriteEvent(new LauncherShowEvent(hotKey));
基础实现见 TelemetryBase.cs,与 C++ 侧共同构成同一套 ETW provider 体系。
用户控制与查看遥测数据
- 设置页允许用户开关遥测发送,以及开启遥测数据本地查看;
- 打开「Enable viewing」后,PowerToys 启动 ETW tracing,把 ETL 文件保存 28 天(超过 28 天的文件自动删除);
- 大多数工具的 ETL 位于:
%LOCALAPPDATA%\Microsoft\PowerToys\ETL,低权限组件保存在另一位置; - 设置页提供按钮,把 ETL 转换为 XML 供用户阅读。选择 XML 格式是为了遵循 Windows Subsystem for Android 已批准的合规模式。
七、Bug Report Tool:一键打包现场
tools/BugReportTool 用于在上报问题时自动收集日志与系统信息。触发方式有两种:
- 右键任务托盘中的 PowerToys 图标 → Report Bug;
- 左键托盘图标 → 打开设置 → Bug Report Tool。
它会在桌面生成 PowerToys_Report_[date]_[time].zip,内含相关日志与系统信息,免去用户手动找 %LOCALAPPDATA%\Microsoft\PowerToys\Logs 各目录的麻烦。工具本身的详细用法见 Bug Report Tool 文档;runner 侧的调用入口可在 bug_report.cpp 中查看。
小结:面向开发者与排障者的速查
| 场景 | 去哪里看 | 关键源码 |
|---|---|---|
| 模块常规日志 | %LOCALAPPDATA%\Microsoft\PowerToys\Logs(按模块/版本分目录) |
logger_settings.h |
| 预览/缩略图等低权限组件 | %USERPROFILE%\AppData\LocalLow\Microsoft\PowerToys |
Logger.cs |
| 调整 C++ 日志级别 | settings.json 的 logLevel(trace/debug/info/warn/err/critical/off) |
logger.cpp |
| 查看 28 天遥测 ETL | %LOCALAPPDATA%\Microsoft\PowerToys\ETL,设置页可转 XML |
TraceBase.h |
| 收集问题现场 | 托盘 → Report Bug,生成桌面 zip | tools/BugReportTool |
整体设计上,PowerToys 用「spdlog(C++)+ Trace(C#)」统一本地文件日志并集中管理路径常量,用「ETW + 注册表开关」实现可被用户完全关闭的遥测,再用 BugReportTool 把分散在多处的日志一键打包——这三层分别对应日常开发调试、产品级诊断、问题上报三个层次,构成了一套边界清晰、per-user 友好的可观测性体系。
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