首页
/ PowerToys 日志与遥测体系解析:从 spdlog/Trace 文件日志到 ETW 遥测与 BugReportTool

PowerToys 日志与遥测体系解析:从 spdlog/Trace 文件日志到 ETW 遥测与 BugReportTool

2026-09-05 09:10:19作者:温玫谨Lighthearted

PowerToys 作为包含 40 余个模块的 Windows 生产力工具集,其可观测性由四套互补机制构成:文本文件日志、ETW 遥测、事件查看器日志与 Watson 崩溃报告。本文以仓库中的 开发文档 为主线,结合 src/common 下的日志、遥测实现源码,完整讲清日志的存放位置、C++/C# 双栈的接入方式、日志级别设置、遥测的注册与用户控制,以及 Bug Report Tool 的触发路径,帮助你在二次开发或排查问题(包括低权限组件)时快速定位日志、正确使用 Logger 与遥测 API。

一、PowerToys 的四种日志机制

PowerToys 的日志/诊断能力分为四类,各机制面向不同场景:

  1. 文本文件日志:应用直接写入本地文件,是日常排查问题的主要手段,C# 与 C++ 组件都使用;
  2. 遥测/诊断数据(Telemetry):基于 Windows 的 ETW(Event Tracing for Windows)发送,与文本文件日志是完全独立的一套体系;
  3. 事件查看器(Event Viewer)日志:部分工具如 Mouse Without Borders 使用;
  4. 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.csLogDirectoryPath 方法根据 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] [级别] 消息
  • 每级自动 flushflush_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.cppget_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   },
};

从源码看,LogSettingsdefaultLogLevelL"trace"——即默认按 trace 级别记录,所以 info 及以上级别的日志默认都会写入文件;若用户把 logLevel 调成 info/warnLogger::debug/trace 的调用会被 spdlog 过滤掉。这与文档「默认写 info 级日志、debug/trace 可能不写」的描述一致:级别由 settings.json 的 logLevel 决定,取值可用上表七个字符串。

四、C# 日志实现:ManagedCommon 的静态 Logger

C# 项目统一使用 ManagedCommon 中的静态类 Loggersrc/common/ManagedCommon/Logger.cs)。接入方式:

  1. 项目引用 ManagedCommon
  2. 使用日志的文件加上 using ManagedCommon;
  3. 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 / TraceLoggingUnregisterIsDataDiagnosticsEnabled()TraceBase.h)读取注册表:

  • 键:HKCU\Software\Classes\PowerToys
  • 值:AllowDataDiagnostics(DWORD)

也就是说,用户在设置页关闭遥测后,该值变为 0,C++ 侧所有 TraceLoggingWriteWrapper 包裹的写入点都会直接短路——这与文档「检查用户是否禁用诊断」完全对应。模块侧的使用示例(来自 Always On Top):

Trace::AlwaysOnTop::Enable(true);

ETW 定义相关的基础设施集中在 src/common/Telemetry 目录(ProjectTelemetry.hTraceLoggingDefines.hTelemetryBase.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 的 logLeveltrace/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 友好的可观测性体系。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384