首页
/ PowerToys 日志系统实战指南:日志存储路径、spdlog 与 ManagedCommon Logger 的完整实现

PowerToys 日志系统实战指南:日志存储路径、spdlog 与 ManagedCommon Logger 的完整实现

2026-09-04 19:40:44作者:卓炯娓

本文以 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.csLogDirectoryPath 方法根据 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.csDeleteOldVersionLogFolders)。

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_SUPPORTSPDLOG_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

  1. 日志级别来自 JSON 配置getLogLevel 从配置文件读取 logLevel 字段,支持 tracedebuginfowarnerrcriticaloff 七个取值;配置缺失或取值非法时回退到默认级别 trace(映射表见 logger.cpp)。配置文件的读写由 logger_settings.cpp 完成——首次调用时若文件不存在会自动创建默认配置,所以每个日志根目录下的配置 JSON 是运行时自动生成的。
  2. 按天滚动的文件 sink:使用 daily_file_sink_mt 创建 logger,保留天数由 LogSettings::retention 控制,当前值为 30 天(见 logger_settings.h)。
  3. 调试器附加时双写:当 IsDebuggerPresent() 为真时,额外挂一个 msvc_sink_mt,把同一份日志同时输出到 VS 输出窗口,方便开发期实时观察。
  4. 统一格式与自动刷盘:日志格式为 [%Y-%m-%d %H:%M:%S.%f] [p-%P] [t-%t] [%l] %v,包含毫秒级时间戳、进程 ID、线程 ID 和级别;并设置 flush_on(logLevel),保证每条日志立即落盘。
  5. 失败兜底:若创建 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)。接入步骤:

  1. 给项目添加对 ManagedCommon 的项目引用;
  2. 在需要使用日志的文件中加 using ManagedCommon;
  3. Main 函数(或 App.xaml.cs 中的 App 等入口函数)调用 InitializeLogger,传入相对模块名的日志子路径(注意开头带反斜杠的路径方案):
Logger.InitializeLogger("\\FancyZones\\Editor\\Logs");
  1. 低权限进程(例如以低权限运行的文件预览组件)必须把第二个参数置为 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.csLog 私有方法)。

用 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 一键打包完整日志集。

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