Repository Wiki
BeyondDimension/SteamTools

日志、诊断与崩溃上报

本页系统阐述 Watt Toolkit(Steam++)客户端中日志记录、诊断信息收集与崩溃上报的完整机制:以 Microsoft.Extensions.Logging(ILogger/ILoggerFactory)抽象为核心的多 Provider 日志管道、Apple 平台原生日志桥接(NSLog/OSLog)、应用内日志控制台、跨进程 IPC 日志转发、NLog 集成(ArchiSteamFarm 插件),以及基于 AppDomain 全局异常钩子与 App Center 的崩溃处理链路。

目的与范围

本页覆盖:

  • 日志抽象基类 ClientLogger 的格式化算法与扩展契约(src/BD.WTTS.Client/Logging/ClientLogger.cs)
  • 平台专用 Logger 与 Provider:NSLogger/NSLoggerProvider、OSLogLogger/OSLogLoggerProvider
  • 应用内日志控制台(LogConsoleService.Logger.cs 中的 Utf8StringLogger/Utf8StringLoggerProvider)
  • IPC 进程间日志转发(IpcLogger_ 在主进程与子进程两侧的实现)
  • NLog 桥接与 ArchiSteamFarm 插件的 ArchiLogger
  • 全局异常处理与崩溃上报:AppDomain.UnhandledException/ProcessExit 钩子、App Center 崩溃聚合(代码注释中引用了 App Center 的崩溃错误页 URL)
  • 原生宿主(AppHost)在托管运行时启动失败时的错误对话框诊断输出

本页不覆盖(留给兄弟页面):

  • IPC 通道本身的传输协议与进程管理 —— 见 IPC 相关页面(本页仅讨论日志如何经 IPC 转发)
  • Kestrel 反向代理服务器 —— 见加速器/反向代理页面(本页仅说明其日志接入方式)
  • ArchiSteamFarm 插件业务逻辑 —— 见 ASF 插件页面(本页仅涉及其 NLog 日志桥接)

概述

Watt Toolkit 是一个跨平台(Windows/macOS/Linux/Android/iOS)的多进程桌面应用,日志子系统面临的典型问题是:不同平台有互不兼容的原生日志设施,多进程架构下子进程日志需要汇聚到主进程,同时用户需要在应用内直接查看运行日志以进行自助诊断。

为此,项目没有直接绑定某个日志框架,而是围绕 .NET 标准 Microsoft.Extensions.Logging 抽象构建:

关注点解决方案关键类型
统一日志门面ILogger/ILoggerFactory(Microsoft.Extensions.Logging)ILoggerFactory、ILogger
统一消息格式抽象基类 ClientLogger 提供 CreateDefaultLogMessage 格式化ClientLogger
Apple 原生输出NSLog 与 OSLog 桥接NSLogger、OSLogLogger
应用内查看UTF-8 字符串收集型 LoggerUtf8StringLogger
多进程汇聚IPC 通道转发IpcLogger_
插件生态兼容NLog 桥接(ASF 上游使用 NLog)NLogLoggerProvider、ArchiLogger
托管前失败诊断原生宿主错误对话框 + host error codesProgram(AppHost)

诊断方面,项目通过 src/ImplicitUsings.WTTS.cs 中的 global using BD.WTTS.Diagnostics; 将诊断命名空间全局引入,所有业务代码可直接使用诊断类型;崩溃场景则通过 AppDomain 生命周期钩子在 ArchiSteamFarm 插件宿主内做兜底退出处理,并接入 App Center 崩溃聚合(Avalonia.Skia.Internals 中保留了对崩溃错误页的引用注释)。

架构

Loading diagram...

上图展示了日志子系统的分层结构:

  • 调用方层:业务服务通过 IoC 容器获取 ILoggerFactory 并按类别名(name/TAG)创建 Logger;Kestrel 反向代理在 KestrelServerOptionsExtensions 中通过 kestrel.ApplicationServices.GetRequiredService<ILoggerFactory>() 接入同一套工厂。
  • 抽象层:ClientLogger 是项目自定义的抽象基类,实现了 ILogger 接口并固化了消息格式化逻辑,所有需要统一格式的 Logger(如 NSLogger)从它继承;而 OSLogLogger、Utf8StringLogger 则因输出形态不同直接实现 ILogger。
  • Provider 层:ILoggerProvider(带 [ProviderAlias("NSLog")] 特性标注)负责创建 Logger 实例,是接入 LoggerFactory 的标准扩展点。
  • 输出层:各 Logger 将格式化后的消息写入对应目的地;IpcLogger_ 在子进程中把日志送回主进程,实现多进程日志汇聚。

崩溃与诊断链路则独立于常规日志管道:

Loading diagram...

原生宿主 Program 类(src/BD.WTTS.Client.AppHost/Program.cs)在托管代码尚未运行之前负责检测 .NET 运行时(dotnet_version、dotnet_runtime = "Microsoft.NETCore.App"、aspnetcore_runtime = "Microsoft.AspNetCore.App"),失败时通过 OpenCoreByProcess 引导用户打开下载页并用 ShowErrMessageBox 弹出诊断信息——这是"日志系统还没来得及启动"时的最后一道诊断防线。托管进程内,Avalonia 的 ClassicDesktopStyleApplicationLifetime 关机路径上发生的崩溃由 App Center 聚合(源码注释保留了 App Center 错误页链接),ASF 插件宿主则通过 AppDomain 钩子做进程级兜底退出处理。

核心实现:ClientLogger 抽象基类

ClientLogger 位于 src/BD.WTTS.Client/Logging/ClientLogger.cs,是客户端日志的格式化核心。它的设计参考了 dotnet/extensions 官方 ConsoleLogger 的实现(源码注释中直接给出了对照链接),这意味着消息格式与 .NET 官方控制台日志保持一致,降低团队认知成本。

csharp
1public abstract class ClientLogger : ILogger 2{ 3 static readonly string _messagePadding = new string(' ', 6); 4 static readonly string _newLineWithMessagePadding = Environment.NewLine + _messagePadding; 5 6 protected readonly string name; 7 8 public ClientLogger(string name) => this.name = name; 9 10 public virtual IDisposable? BeginScope<TState>(TState state) where TState : notnull 11 { 12 return NullScope.Instance; 13 } 14 15 public virtual bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

Source: ClientLogger.cs

关键设计意图:

  1. _messagePadding 为 6 个空格:消息正文相对类别行缩进 6 格,多行消息(含异常堆栈)的每一行都会被替换为 Environment.NewLine + 6 空格,保证堆栈可读性。
  2. BeginScope 返回 NullScope.Instance:默认不支持日志作用域(scope),因为客户端场景下作用域会引入额外分配,移动端(iOS/Android)对此敏感;子类可按需重写。
  3. IsEnabled 仅过滤 LogLevel.None:级别过滤交由 LoggerFactory 的过滤规则或具体平台决定,基类保持宽容策略。

Log<TState> 是模板方法:先校验 formatter 非空(快速失败),再由 formatter(state, exception) 得到消息,仅当消息非空或存在异常时才进入 WriteMessage,避免空日志浪费 I/O:

csharp
1public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter) 2{ 3 if (!IsEnabled(logLevel)) 4 return; 5 6 if (formatter == null) 7 throw new ArgumentNullException(nameof(formatter)); 8 9 var message = formatter(state, exception); 10 11 if (!string.IsNullOrEmpty(message) || exception != null) 12 WriteMessage(logLevel, name, eventId.Id, message, exception); 13}

Source: ClientLogger.cs

消息格式化算法

CreateDefaultLogMessage 是实际产出日志文本的核心,产出格式如下(源自源码中的示例注释):

text
INFO: ConsoleApp.Program[10] Request received
csharp
1static void CreateDefaultLogMessage(StringBuilder logBuilder, string logName, int eventId, string? message, Exception? exception) 2{ 3 // Example: 4 // INFO: ConsoleApp.Program[10] 5 // Request received 6 7 // category and event id 8 logBuilder.Append(logName); 9 logBuilder.Append('['); 10 logBuilder.Append(eventId); 11 logBuilder.AppendLine("]"); 12 13 if (!string.IsNullOrEmpty(message)) 14 { 15 // message 16 logBuilder.Append(_messagePadding); 17 18 var len = logBuilder.Length; 19 logBuilder.AppendLine(message); 20 logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length); 21 } 22 23 // Example: 24 // System.InvalidOperationException 25 // at Namespace.Class.Function() in File:line X 26 if (exception != null) 27 // exception message 28 logBuilder.AppendLine(exception.ToString()); 29} 30 31public abstract void WriteMessage(LogLevel logLevel, string message);

Source: ClientLogger.cs

算法要点:

  • 先记录 len = logBuilder.Length 再追加消息,随后对 [len, message.Length) 区间做 Replace。这是一个 O(消息长度) 的局部替换技巧,只缩进本条消息内部换行,不会误伤之前已写入缓冲区的内容——这是从官方 ConsoleLogger 移植的关键细节。
  • 异常直接 AppendLine(exception.ToString()),完整堆栈落在类别行下方,无需额外缩进处理(异常文本自带堆栈缩进)。
  • 抽象方法 WriteMessage(LogLevel, string) 是唯一的平台输出出口:基类管"长什么样",子类管"写到哪里"。WriteMessage(LogLevel logLevel, string logName, int eventId, string? message, Exception? exception) 这个虚方法重载(第 38-46 行)则把"拼字符串 → 输出"串成完整管线。

类层次关系

Loading diagram...

值得注意的是分化策略:需要复用统一文本格式的输出(NSLog)继承 ClientLogger;输出形态结构化/非文本(OSLog 有自己的子系统与类别概念、Utf8StringLogger 面向应用内 UI 展示)则直接实现 ILogger。这种"格式与目的地解耦"的分层让同一份格式化逻辑被多种平台复用,又不会强迫所有目标地接受纯文本。

平台 Logger 实现

NSLogger 与 NSLoggerProvider(Apple NSLog)

NSLogger 是 sealed partial class,继承 ClientLogger,位于 src/BD.WTTS.Client/Logging/NSLogger.cs,其 XML 文档注释为 <inheritdoc cref="ClientLogger"/>——即完全复用基类格式化,只重写 WriteMessage(LogLevel, string) 将最终字符串交给原生 NSLog。partial 修饰暗示平台互操作部分(P/Invoke 或绑定)在另一个分部文件中。

NSLoggerProvider 标注了 [ProviderAlias("NSLog")]:

csharp
[ProviderAlias("NSLog")] public sealed class NSLoggerProvider : ILoggerProvider

Source: NSLoggerProvider.cs

ProviderAlias 的作用是让配置过滤规则(如 Logging:NSLog:LogLevel:Default)可以通过别名而非完整类型名引用该 Provider,这是 Microsoft.Extensions.Logging 的标准约定。

OSLogLogger 与 OSLogLoggerProvider

OSLogLogger 声明为 internal sealed class OSLogLogger : ILogger<object>, ILogger,实现泛型与非泛型两个接口;OSLogLoggerProvider 同为 public sealed class ... : ILoggerProvider(src/BD.WTTS.Client/Logging/OSLogLoggerProvider.cs)。OSLog 是 Apple 统一日志系统(Unified Logging),适合在 Console.app / log 命令行中长期留存与检索,与 NSLog(即时控制台输出)形成互补:NSLog 便于调试期观察,OSLog 便于系统级采集与事后取证。

应用内日志控制台:Utf8StringLogger

Utf8StringLogger(internal sealed class Utf8StringLogger(string name) : ILogger,位于 src/BD.WTTS.Client/Services/Mvvm/LogConsoleService.Logger.cs 第 268 行起)是应用内日志查看窗口的数据源,配合同文件第 341 行的 Utf8StringLoggerProvider 工作。它把日志收集为 UTF-8 字符串供 UI(LogConsoleService)展示,是"用户自助诊断"功能的核心:用户无需访问系统日志即可在应用内复制、导出日志。该文件还使用 C# 12 主构造函数(primary constructor)语法 (string name),体现项目对现代 C# 特性的采用。

多进程日志汇聚:IpcLogger_

Watt Toolkit 采用主进程 + 子进程架构(加速器反向代理、ASF 等运行于独立进程)。两侧均有 sealed class IpcLogger_ : IpcLogger:

  • 子进程侧(src/BD.WTTS.Client.IPC/Services.Implementation/IPCSubProcessServiceImpl.cs 第 23 行):子进程内的 IPCSubProcessServiceImpl 持有主进程注入的 ILoggerFactory,把本进程日志经 IPC 通道送回主进程。
  • 主进程侧(src/BD.WTTS.Client/Services.Implementation/IPC/IPCMainProcessServiceImpl.cs 第 29 行):接收来自子进程的日志并落入主进程日志管道。

子进程侧构造逻辑(从搜索证据可见):

csharp
1public IpcLogger_(ILoggerFactory loggerFactory, string name) : base(name) 2{ 3 logger = loggerFactory.CreateLogger(name); 4}

Source: IPCSubProcessServiceImpl.cs

设计意图:IpcLogger_ 继承 IpcLogger(后者应是格式化契约),在子进程内用主进程传递的 loggerFactory.CreateLogger(name) 创建本地 Logger。这样日志的类别名(name)在两个进程中保持一致,主进程汇聚后仍能按原始类别过滤/展示,日志语义不因跨进程而丢失。IPCSubProcessService.cs 第 216 行展示了工厂的获取方式:var loggerFactory = Ioc.Get<ILoggerFactory>();,即通过项目 IoC 容器解析,而非构造注入。

子进程日志初始化与 NLog 桥接

src/BD.WTTS.Client.IPC/Services/IPCSubProcessService.cs 第 388-389 行揭示了 NLog 桥接与 LoggerFactory 的组装:

csharp
NLogManager.Configuration = objConfig; Log.LoggerFactory = () => new LoggerFactory(new[] { new NLogLoggerProvider() });

Source: IPCSubProcessService.cs

NLogManager.Configuration = objConfig 加载 NLog 配置(文件目标等),Log.LoggerFactory 委托则在需要时惰性创建 LoggerFactory 并仅注册 NLogLoggerProvider。原因在于 ASF(ArchiSteamFarm)上游生态使用 NLog,ArchiLogger(src/BD.WTTS.Client.Plugins.ArchiSteamFarmPlus/_/ArchiSteamFarm/NLog/ArchiLogger.cs)作为 NLog 目标/包装把 ASF 的日志接入宿主;通过 NLogLoggerProvider 又能把 NLog 流量桥回 Microsoft.Extensions.Logging,实现双向兼容。紧随其后的 #if (STARTUP_WATCH_TRACE || DEBUG) 条件编译表明该初始化路径与启动性能跟踪(StartupWatch)共享编译开关,调试构建下会输出更详细的启动耗时日志。

崩溃上报与全局异常处理

ArchiSteamFarm 插件宿主的全局钩子

src/BD.WTTS.Client.Plugins.ArchiSteamFarmPlus/Services.Implementation/ArchiSteamFarmServiceImpl.cs 第 117-118 行注册了进程级异常兜底:

csharp
AppDomain.CurrentDomain.ProcessExit += ExitHandler; AppDomain.CurrentDomain.UnhandledException += ExitHandler;

Source: ArchiSteamFarmServiceImpl.cs

两个事件指向同一个 ExitHandler:无论进程是正常退出(ProcessExit)还是未捕获异常导致崩溃(UnhandledException),都执行统一的清理/退出流程。这对 ASF 尤其重要——ASF 内部管理 Steam 会话与挂卡任务,异常退出若不清理,可能残留进程或锁文件。

App Center 崩溃聚合

src/Avalonia.Skia.Internals/ClassicDesktopStyleApplicationLifetime.cs 第 151 行的注释记录了一个真实崩溃案例的处理痕迹:

csharp
// https://appcenter.ms/orgs/BeyondDimension/apps/Steam/crashes/errors/3780037517u/overview // ClassicDesktopStyleApplicationLifetime.DoShutdown (ShutdownRequestedEventArgs e, Boolean force, Int32 exitCode) /_/src/Avalonia.Controls/ApplicationLifetimes/ClassicDesktopStyleApplicationLifetime.cs, line 147

Source: ClassicDesktopStyleApplicationLifetime.cs

项目将 Avalonia 的 ClassicDesktopStyleApplicationLifetime 复制进 Avalonia.Skia.Internals 内部维护,正是因为 App Center 上报的崩溃(此处为 DoShutdown 路径,第 147 行)需要修复——团队据此在内部副本中加入防御性处理。这体现了崩溃上报的闭环价值:崩溃数据 → 定位到上游框架缺陷 → 内部 fork 修复。App Center 的应用标识为 orgs/BeyondDimension/apps/Steam,即 Watt Toolkit 的发布主体。

原生宿主(AppHost)的启动期诊断

托管日志系统启动之前若发生失败(.NET 运行时缺失、加载错误),诊断只能依赖原生宿主。src/BD.WTTS.Client.AppHost/Program.cs 实现了完整的引导诊断链:

  • 第 3-4 行注释引用了 .NET 官方 host error codes 文档与 nativehost 示例,说明错误码体系与官方宿主保持一致。
  • 静态构造中维护 dotnet_version 系列字段(如 11.0.0-preview.7.26381.103)与 dotnet_runtime = "Microsoft.NETCore.App"、aspnetcore_runtime = "Microsoft.AspNetCore.App" 常量,用于探测所需运行时。
  • GetProcessArchitecture()(第 107-129 行)在低版本框架(NET35/NET40)下通过反射读取 RuntimeInformation.ProcessArchitecture,反射失败时退化为按 IntPtr.Size/Environment.Is64BitProcess 判断 X86/X64——保证在极端老旧环境下仍能给出"架构不匹配"的诊断信息。
  • DownloadDotNetRuntime()(第 185-190 行)构造 https://dotnet.microsoft.com/{lang}/download/dotnet/{major}.{minor} 下载地址并按用户语言本地化。
  • OpenCoreByProcess(第 159-175 行)用 Process.Start 打开下载页,捕获 Win32Exception 并把 NativeErrorCode 以十六进制格式化进错误对话框(ShowErrMessageBox),让用户可截图反馈。

这一段代码大量使用条件编译(#if NETFRAMEWORK、NET35、NET7_0_OR_GREATER && WINDOWS)和 LibraryImport/DllImport(如 shlwapi.dll 的 PathIsDirectoryEmptyW),因为 AppHost 必须能在"目标机器可能没有任何 .NET 运行时"的最坏情况下运行——这是所有诊断手段的前置条件。

日志流核心时序

Loading diagram...

时序中值得注意的两处短路:IsEnabled 在 LogLevel.None 时立刻返回(热路径零成本);formatter 为 null 时抛 ArgumentNullException(编程错误快速暴露,而非静默吞掉)。多 Provider 场景下,LoggerFactory 返回的是聚合 Logger,同一条日志会分发到 NSLog、应用内控制台、IPC 等多个目的地——这是"一次记录、多处可见"的架构基础。

配置选项

日志子系统的可配置项分散在 Provider 注册、NLog 配置与编译开关三个层面:

选项 / 开关类型默认说明
Logging:NSLog:LogLevel:*日志级别过滤未设置(全放行)通过 NSLoggerProvider 的 [ProviderAlias("NSLog")] 别名生效,遵循 Microsoft.Extensions.Logging 配置约定
NLogManager.ConfigurationNLog 配置对象null子进程中由 objConfig 赋值,决定 NLog 输出目标(文件路径、格式等)
Log.LoggerFactoryFunc<LoggerFactory> 委托由 IPC 初始化设置惰性创建 LoggerFactory,仅注册 NLogLoggerProvider
STARTUP_WATCH_TRACE编译常量未定义与 DEBUG 组合控制子进程启动阶段的耗时跟踪日志
DEBUG编译常量仅 Debug 构建与 STARTUP_WATCH_TRACE 联合启用详细启动诊断
dotnet_version*(AppHost)静态字段如 11.0.0-preview.7.26381.103原生宿主探测 .NET 运行时版本,不匹配时引导下载
ProviderAlias(代码内固定)特性"NSLog"编译期固定,非运行时配置

说明:项目未发现集中式 appsettings.json 日志配置节的直接证据(配置可能通过 LoggerFilterOptions/代码注册完成),上表中 Logging:* 行描述的是 ProviderAlias 机制所支持的配置面,实际默认值以代码注册为准。NLog 的 objConfig 具体内容(文件目标路径、滚动策略)在本次读取范围之外,未在源码中直接确认。

API 参考

ClientLogger.Log<TState>(LogLevel, EventId, TState, Exception?, Func<TState, Exception?, string>)

描述:ILogger 模板方法入口,执行级别过滤、格式化并派发到抽象输出。

参数:

  • logLevel (LogLevel):日志级别,LogLevel.None 时被 IsEnabled 拒绝
  • eventId (EventId):事件标识,仅取 .Id 输出到类别行
  • state (TState):状态对象,交由 formatter 渲染
  • exception (Exception?):可选异常,非空时整段堆栈附加输出
  • formatter (Func<TState, Exception?, string>):消息格式化委托,null 抛异常

返回:void

抛出:

  • ArgumentNullException:formatter == null 时

Source: ClientLogger.cs

ClientLogger.WriteMessage(LogLevel, string logName, int eventId, string?, Exception?)

描述:虚方法,组装 StringBuilder 并调用 CreateDefaultLogMessage,再交给单参抽象重载输出。

Source: ClientLogger.cs

ClientLogger.WriteMessage(LogLevel, string)(抽象)

描述:子类必须实现的最终输出出口;NSLogger 在此将字符串交给 NSLog。

Source: ClientLogger.cs

ClientLogger.IsEnabled(LogLevel)

描述:默认仅拒绝 LogLevel.None;子类可重写以接入平台级别过滤。

返回:bool

Source: ClientLogger.cs

ClientLogger.BeginScope<TState>(TState)

描述:默认返回 NullScope.Instance(无操作作用域);如需作用域支持需子类重写。

返回:IDisposable?

Source: ClientLogger.cs

IpcLogger_(ILoggerFactory, string)(构造函数)

描述:子进程侧 IPC 日志构造器,调用 base(name) 保留类别名,同时用主进程工厂创建本地 Logger,保证跨进程类别一致。

Source: IPCSubProcessServiceImpl.cs

故障模式、边界与并发

  • formatter == null:Log<TState> 立即抛 ArgumentNullException。这是刻意设计——formatter 缺失属于调用方编程错误,静默忽略会掩盖 bug。
  • 空消息且无异常:!string.IsNullOrEmpty(message) || exception != null 不满足时整条日志被丢弃,避免输出只有类别头的"空壳"行。
  • 多行消息缩进边界:StringBuilder.Replace 限定在 [len, message.Length) 区间,只处理当前消息内部换行;若消息本身以 Environment.NewLine 结尾,AppendLine 与 Replace 的组合不会产生双重缩进,因为 Replace 作用于追加前的长度记录之后。
  • 跨平台换行差异:缩进替换使用 Environment.NewLine,Windows(\r\n)与 Unix(\n)行为一致,日志文件跨平台迁移后缩进仍正确。
  • 进程级异常兜底:ProcessExit 与 UnhandledException 共用 ExitHandler,可能被并发触发(UnhandledException 后紧跟 ProcessExit);ExitHandler 需幂等,否则重复清理会二次抛异常。本次读取范围内未展开其内部实现,该并发要求是从注册方式推导的边界条件。
  • 原生宿主最坏情况:AppHost 代码假设目标机器可能完全没有 .NET 运行时,故 GetProcessArchitecture 在反射失败时仍有 IntPtr.Size 兜底分支;OpenCoreByProcess 捕获 Win32Exception 而非让其逃逸,因为此时没有任何托管日志系统可记录该异常。
  • 作用域缺失的影响:BeginScope 默认 NullScope 意味着不能用 scope 携带请求上下文;需要上下文的调用方必须把信息编码进消息或类别名。

性能与运维要点

  • 热路径零分配过滤:IsEnabled 是简单比较,级别关闭时无字符串构造;formatter 仅在通过过滤后才被调用(由 LoggerFactory 聚合器保证)。
  • StringBuilder 复用模式:WriteMessage 每次新建 StringBuilder(未使用池化)。对桌面客户端日志量而言可接受;若引入高吞吐日志场景(如 ASF 挂卡日志),可考虑对象池优化。
  • 多目的地扇出:一次 Log 调用由工厂聚合分发到 NSLog/OSLog/应用内控制台/IPC 多个目的地,其中 IPC 目的地涉及序列化与跨进程传输,是延迟最敏感的一环——子进程侧在本地 CreateLogger 后再转发的设计缓解了主进程往返开销。
  • 启动性能跟踪:STARTUP_WATCH_TRACE || DEBUG 编译开关将启动耗时日志与常规日志隔离,Release 构建默认不付出该成本。
  • 崩溃闭环:App Center 上报 → 内部 fork 修复(Avalonia.Skia.Internals)→ 发布验证,运维上应以 App Center 的 BeyondDimension/Steam 应用为崩溃监控入口。
  • 用户自助诊断:应用内日志控制台(Utf8StringLogger)让非技术用户也能导出日志,降低远程支持成本;这是把"诊断能力产品化"的典型做法。

扩展点

  1. 新增平台输出:实现 ILoggerProvider + 对应 Logger。若需要统一文本格式,继承 ClientLogger 并只实现 WriteMessage(LogLevel, string)(参照 NSLogger);若输出是结构化的(如 Windows 事件日志、系统 journal),直接实现 ILogger(参照 OSLogLogger)。
  2. 配置别名:为 Provider 标注 [ProviderAlias("...")] 即可让用户通过配置节名而非类型名过滤(参照 NSLoggerProvider 的 "NSLog")。
  3. 作用域支持:重写 BeginScope 返回真实的 scope 对象(AsyncLocal 方案),可为高并发模块(加速器代理)提供请求维度聚合。
  4. NLog 生态接入:第三方组件若使用 NLog,通过 NLogManager.Configuration + NLogLoggerProvider 桥接进宿主管道,无需改动组件代码(ASF 插件即此路径)。
  5. 崩溃处理增强:在 ExitHandler 中追加上报(App Center / 自建接口)或 dump 收集,钩子位置已由 AppDomain 注册点固定。

测试

本次源码读取范围内未发现针对 ClientLogger/NSLogger 的单元测试文件;src/BD.WTTS.Client.Tools.HostsTest/Program.cs 第 106 行附近出现的 Source: "System.Diagnostics.Process" 是一个异常输出的注释记录(NativeErrorCode 5 场景),说明仓库中存在以"运行并观察日志/异常文本"为手段的手动诊断样例,而非自动化日志断言。若需要补充测试,格式化算法(CreateDefaultLogMessage 的缩进与区间替换)是最值得覆盖的纯函数边界。

相关链接

Sources

(2 files)
src/BD.WTTS.Client.AppHost
src/BD.WTTS.Client/Logging