日志、诊断与崩溃上报
本页系统阐述 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 字符串收集型 Logger | Utf8StringLogger |
| 多进程汇聚 | IPC 通道转发 | IpcLogger_ |
| 插件生态兼容 | NLog 桥接(ASF 上游使用 NLog) | NLogLoggerProvider、ArchiLogger |
| 托管前失败诊断 | 原生宿主错误对话框 + host error codes | Program(AppHost) |
诊断方面,项目通过 src/ImplicitUsings.WTTS.cs 中的 global using BD.WTTS.Diagnostics; 将诊断命名空间全局引入,所有业务代码可直接使用诊断类型;崩溃场景则通过 AppDomain 生命周期钩子在 ArchiSteamFarm 插件宿主内做兜底退出处理,并接入 App Center 崩溃聚合(Avalonia.Skia.Internals 中保留了对崩溃错误页的引用注释)。
架构
上图展示了日志子系统的分层结构:
- 调用方层:业务服务通过 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_在子进程中把日志送回主进程,实现多进程日志汇聚。
崩溃与诊断链路则独立于常规日志管道:
原生宿主 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 官方控制台日志保持一致,降低团队认知成本。
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
关键设计意图:
_messagePadding为 6 个空格:消息正文相对类别行缩进 6 格,多行消息(含异常堆栈)的每一行都会被替换为Environment.NewLine + 6 空格,保证堆栈可读性。BeginScope返回NullScope.Instance:默认不支持日志作用域(scope),因为客户端场景下作用域会引入额外分配,移动端(iOS/Android)对此敏感;子类可按需重写。IsEnabled仅过滤LogLevel.None:级别过滤交由LoggerFactory的过滤规则或具体平台决定,基类保持宽容策略。
Log<TState> 是模板方法:先校验 formatter 非空(快速失败),再由 formatter(state, exception) 得到消息,仅当消息非空或存在异常时才进入 WriteMessage,避免空日志浪费 I/O:
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 是实际产出日志文本的核心,产出格式如下(源自源码中的示例注释):
INFO: ConsoleApp.Program[10]
Request received1static 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 行)则把"拼字符串 → 输出"串成完整管线。
类层次关系
值得注意的是分化策略:需要复用统一文本格式的输出(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")]:
[ProviderAlias("NSLog")]
public sealed class NSLoggerProvider : ILoggerProviderSource: 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 行):接收来自子进程的日志并落入主进程日志管道。
子进程侧构造逻辑(从搜索证据可见):
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 的组装:
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 行注册了进程级异常兜底:
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 行的注释记录了一个真实崩溃案例的处理痕迹:
// 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项目将 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 运行时"的最坏情况下运行——这是所有诊断手段的前置条件。
日志流核心时序
时序中值得注意的两处短路:IsEnabled 在 LogLevel.None 时立刻返回(热路径零成本);formatter 为 null 时抛 ArgumentNullException(编程错误快速暴露,而非静默吞掉)。多 Provider 场景下,LoggerFactory 返回的是聚合 Logger,同一条日志会分发到 NSLog、应用内控制台、IPC 等多个目的地——这是"一次记录、多处可见"的架构基础。
配置选项
日志子系统的可配置项分散在 Provider 注册、NLog 配置与编译开关三个层面:
| 选项 / 开关 | 类型 | 默认 | 说明 |
|---|---|---|---|
Logging:NSLog:LogLevel:* | 日志级别过滤 | 未设置(全放行) | 通过 NSLoggerProvider 的 [ProviderAlias("NSLog")] 别名生效,遵循 Microsoft.Extensions.Logging 配置约定 |
NLogManager.Configuration | NLog 配置对象 | null | 子进程中由 objConfig 赋值,决定 NLog 输出目标(文件路径、格式等) |
Log.LoggerFactory | Func<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)让非技术用户也能导出日志,降低远程支持成本;这是把"诊断能力产品化"的典型做法。
扩展点
- 新增平台输出:实现
ILoggerProvider+ 对应 Logger。若需要统一文本格式,继承ClientLogger并只实现WriteMessage(LogLevel, string)(参照NSLogger);若输出是结构化的(如 Windows 事件日志、系统 journal),直接实现ILogger(参照OSLogLogger)。 - 配置别名:为 Provider 标注
[ProviderAlias("...")]即可让用户通过配置节名而非类型名过滤(参照NSLoggerProvider的"NSLog")。 - 作用域支持:重写
BeginScope返回真实的 scope 对象(AsyncLocal方案),可为高并发模块(加速器代理)提供请求维度聚合。 - NLog 生态接入:第三方组件若使用 NLog,通过
NLogManager.Configuration+NLogLoggerProvider桥接进宿主管道,无需改动组件代码(ASF 插件即此路径)。 - 崩溃处理增强:在
ExitHandler中追加上报(App Center / 自建接口)或 dump 收集,钩子位置已由AppDomain注册点固定。
测试
本次源码读取范围内未发现针对 ClientLogger/NSLogger 的单元测试文件;src/BD.WTTS.Client.Tools.HostsTest/Program.cs 第 106 行附近出现的 Source: "System.Diagnostics.Process" 是一个异常输出的注释记录(NativeErrorCode 5 场景),说明仓库中存在以"运行并观察日志/异常文本"为手段的手动诊断样例,而非自动化日志断言。若需要补充测试,格式化算法(CreateDefaultLogMessage 的缩进与区间替换)是最值得覆盖的纯函数边界。
相关链接
- 日志核心基类:ClientLogger.cs
- Apple 平台桥接:NSLogger.cs、NSLoggerProvider.cs、OSLogLogger.cs、OSLogLoggerProvider.cs
- 应用内日志控制台:LogConsoleService.Logger.cs
- IPC 日志转发:IPCSubProcessServiceImpl.cs、IPCSubProcessService.cs、IPCMainProcessServiceImpl.cs
- 崩溃与异常:ArchiSteamFarmServiceImpl.cs、ClassicDesktopStyleApplicationLifetime.cs
- 原生宿主诊断:Program.cs
- IPC 通道与进程管理:见 IPC 相关兄弟页面
- ASF 插件与 Kestrel 反向代理:见对应插件页面