Windows 事件日志・ETW 入门 ── 让业务应用程序的日志接入操作系统标准机制
· 小村 豪 · CSharp, .NET, 事件日志, ETW, EventSource, 日志设计, 故障排查, Windows开发
在用 C# 开发业务应用程序或 Windows 服务时,通常会用自己写的文件记录器,或 Serilog、NLog 这类库来输出日常日志。那么 Windows 事件日志或 ETW(Event Tracing for Windows)是不是已经不需要了呢?并非如此。这两者并不是「文件日志的替代品」,而是运维人员或操作系统标准工具所能看到的另一层记录。
本文将整理文件日志、事件日志、ETW 的分工方式,.NET 中的实现方法,ETW 的最基本要点,收集与排查的实务做法,以及现场常踩的坑。
1. 先说结论
- 三种手段不是替代关系,而是分工。 文件日志是开发者事后追查细节用的记录,事件日志是让运维人员或操作系统标准的事件查看器、监控工具察觉异常用的记录,ETW 则是为性能排查或难以复现的故障分析,按需启用的高频率追踪。基本上三者会一起使用,只是各自写入的信息量不同。
- 事件日志只写「启动、停止、严重错误」。 如果把所有日志都灌进事件日志,运维人员真正该看到的异常反而会被淹没。文件日志一如既往写得详细,事件日志则要收窄范围。
- 注册事件来源需要管理员权限。
EventLog.CreateEventSource从 Windows Vista 开始,没有管理员权限就无法调用。应避免设计成在运行时首次访问才注册,而是在安装程序阶段就提前完成注册。1 - ETW 并不是「随时都在采集」。 即使用
EventSource定义好了事件,只要没有订阅者(dotnet-trace、PerfView、基于 ETW 的工具),就什么都不会被记录。基本用法是只在做性能分析或特定故障排查时,才指定目标 provider 进行采集。2 - 消息要用模板格式书写,而不是字符串拼接或字符串插值。 像
logger.LogInformation("Order {OrderId} failed", orderId)这样写,就能以结构化日志的形式,用具名占位符检索 OrderId。用字符串拼接或插值组装出来的消息,会让这个结构信息丢失。3 - 日志膨胀问题,往往是因为默认值太小。 事件日志的默认最大容量只有 512KB,如果业务应用程序会日常使用事件日志,就应该显式扩大容量,并在设计阶段就确定容量达到上限后的行为(覆盖旧数据,还是丢弃新数据)。4
以下是判断表。
| 维度 | 文件日志 | 事件日志 | ETW |
|---|---|---|---|
| 主要读者 | 开发者、维护人员 | 运维人员、监控工具、操作系统标准事件查看器 | 性能排查人员、支持工程师 |
| 输出量参考 | 详细(可以包含 Trace/Debug) | 少(仅启动、停止、严重错误) | 启用时才大量产生。默认情况下什么都不会记录 |
| 生命周期・保留 | 依轮转设置,容易长期保留 | 达到日志容量上限后,会从旧的开始覆盖或丢弃 | 以会话为单位。采集结束后以 .etl/.nettrace 文件保存 |
| 与操作系统标准工具的整合 | 无(需要自制查看器) | 可用事件查看器、wevtutil、Get-WinEvent 等标准方式查看 |
用 dotnet-trace、PerfView、WPR/WPA 查看 |
| 权限 | 仅需应用程序运行用户的写入权限 | 注册来源需要管理员权限(若来源已注册,写入本身标准用户也可以) | 开始采集大多需要管理员权限 |
2. Windows 事件日志的基础
Windows 默认有 Application、System、Security 三种日志,其中 Security 为只读。业务应用程序或服务会写入 Application 日志,或应用程序自定义的日志(通道)。设备驱动程序则习惯上写入 System 日志。5
写入的单位是「事件来源(event source)」。来源是用来标识应用程序的名称,需要先用 EventLog.CreateEventSource 注册。从 Windows Vista 开始,注册来源需要管理员权限。 这是因为要确认来源名称的唯一性,必须搜索包含 Security 日志的所有事件日志,而标准用户没有访问 Security 日志的权限。1
using System.Diagnostics;
// 在安装程序或配置流程中,在拥有管理员权限期间执行
const string SourceName = "KomuraSoft.OrderService";
const string LogName = "Application";
if (!EventLog.SourceExists(SourceName))
{
EventLog.CreateEventSource(SourceName, LogName);
}
如果想在运行时首次访问才创建来源,在应用程序以标准用户身份运行的环境中,这个调用就会失败。关于何时需要管理员权限的一般性说明,整理在「Windows 什么时候需要管理员权限」一文中,事件来源的注册正好符合这个模式:在安装程序中完成注册,应用程序本体只向已注册好的来源写入,才是安全的设计。
事件会附带「级别」与「事件 ID」两种标识信息。级别是 EventLogEntryType(Information、Warning、Error、SuccessAudit、FailureAudit),会用于事件查看器的图标与默认筛选条件。事件 ID 是应用程序自行定义的整数,用作日后检索、统计同类事件的键值。
3. 从 .NET 写入事件日志
.NET 写入事件日志的路径大致有两种。
3.1 直接使用 System.Diagnostics.EventLog
这是低层级 API,适合需要精细控制(分类、附加二进制数据等)的场景。在 .NET(非 .NET Framework)中,需要引用 System.Diagnostics.EventLog 这个 NuGet 包。
using System.Diagnostics;
public sealed class OrderServiceEventLog
{
private const string SourceName = "KomuraSoft.OrderService";
public void WriteServiceStarted()
{
EventLog.WriteEntry(SourceName, "已启动 OrderService。",
EventLogEntryType.Information, eventID: 1000);
}
public void WriteFatalError(Exception ex)
{
EventLog.WriteEntry(SourceName,
$"发生严重错误,无法继续处理。详细内容请查看文件日志。{ex.GetType().Name}: {ex.Message}",
EventLogEntryType.Error, eventID: 1999);
}
}
3.2 使用 ILogger + AddEventLog
如果已经用 ILogger 构建了结构化日志,使用 Microsoft.Extensions.Logging.EventLog 包的 AddEventLog,会更顺畅地接入现有的日志管道。基于 ASP.NET Core 或 Generic Host 的 Worker Service,在 Windows 上运行时默认就会启用 EventLog provider。6
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Logging.AddEventLog(settings =>
{
settings.SourceName = "KomuraSoft.OrderService";
settings.LogName = "Application";
// 除了默认的分类过滤器之外,事件日志只让
// Warning 及以上级别通过(详细内容交给文件日志那边)
settings.Filter = (_, level) => level >= LogLevel.Warning;
});
using IHost host = builder.Build();
await host.RunAsync();
如果省略 EventLogSettings,LogName 默认会是 "Application",SourceName 默认会是 ".NET Runtime"。7 如果保留 .NET Runtime 这种通用的来源名称,在事件查看器中会很难分辨究竟是自己应用程序的日志,还是其他 .NET 应用程序的日志,因此请务必把 SourceName 明确设置为自家应用程序专用的名称。
在 Windows 服务或任务计划程序上运行的批处理中,只把「启动」「停止」「严重错误」写入事件日志,其他详细内容交给文件日志,是实务上比较均衡的做法。服务的创建方式与运维,整理在「Windows 服务的创建方式与运维」;通过任务计划程序执行时的问题排查,整理在「任务计划程序的任务不执行、或以 0x1 结束」。这两种场景下,异常发生时运维人员最先查看的往往是事件日志,这里是否留有要点,会直接影响初期排查的速度。
4. 什么是 ETW ── 贯穿整个操作系统的追踪基础设施
Event Tracing for Windows(ETW)是一套能以统一基础设施,从内核到用户模式应用程序进行埋点的追踪机制。它最大的特点,是能把内核事件(磁盘 I/O、进程创建等)和应用程序自定义的事件放在同一条时间线上,一起记录与分析。8
.NET 可以通过创建继承自 System.Diagnostics.Tracing.EventSource 的类,定义自己的 ETW provider。ETW 以发布—订阅(Publish-Subscribe)模式运行,没有订阅者的话事件就不会被记录。也就是说,仅仅实现 EventSource 并不会发生任何事情,只有等采集工具显式启用之后,事件才会真正被记录下来。2
using System.Diagnostics.Tracing;
[EventSource(Name = "KomuraSoft-OrderService")]
internal sealed class OrderServiceEventSource : EventSource
{
public static readonly OrderServiceEventSource Log = new();
[Event(1, Level = EventLevel.Informational, Message = "已开始处理订单。OrderId={0}")]
public void OrderProcessingStart(string orderId)
{
if (IsEnabled())
{
WriteEvent(1, orderId);
}
}
[Event(2, Level = EventLevel.Informational, Message = "订单处理已完成。OrderId={0}")]
public void OrderProcessingStop(string orderId)
{
if (IsEnabled())
{
WriteEvent(2, orderId);
}
}
}
调用方的用法如下。在真正写入事件之前,先用 IsEnabled() 确认是否已启用,可以避免在未采集时产生多余的开销。
OrderServiceEventSource.Log.OrderProcessingStart(order.Id);
try
{
ProcessOrder(order);
}
finally
{
OrderServiceEventSource.Log.OrderProcessingStop(order.Id);
}
要采集这个事件,最简单的方式是使用跨平台工具 dotnet-trace。9
dotnet-trace collect --providers KomuraSoft-OrderService -- OrderService.exe
采集到的 .nettrace 文件,可以用 Visual Studio 或 PerfView 打开查看内容。如果想做更深入的性能排查,例如连内核事件都要一并分析,就会变成使用 Windows Assessment and Deployment Kit(ADK)内含的 Windows Performance Recorder(WPR)采集,再用 Windows Performance Analyzer(WPA)查看的方案。10 不过,本文的范围仅限于「ETW 能做什么、什么时候用」,不会深入 PerfView 或 WPA 的详细分析步骤。就开发和维护业务应用程序的角度而言,只要掌握到能用 EventSource 定义自己的 provider、并能用 dotnet-trace 采集,就已经足够。
5. 结构化日志的实务
ILogger 的消息模板,要写成包含 {PlaceHolder} 这种具名占位符的固定字符串。这不只是表面上的写法习惯,而是意味着在不改变模板本身的前提下,只传递参数,就能让日志基础设施那一侧维持占位符名称与值之间的对应关系(结构化数据)。11
// 推荐:模板固定,参数单独传递
logger.LogWarning("Order {OrderId} 库存预留失败。仓库={WarehouseId}", orderId, warehouseId);
// 不推荐:字符串拼接、插值会破坏模板与值的对应关系
logger.LogWarning("Order " + orderId + " 库存预留失败。仓库=" + warehouseId);
logger.LogWarning($"Order {orderId} 库存预留失败。仓库={warehouseId}");
代码分析器规则 CA2254,可以检测到这种不推荐的模式(模板会随每次调用而改变的写法)。3 在调用频率高的热路径(hot path)上,进一步使用 LoggerMessageAttribute 做源代码生成,可以避免装箱和模板解析的开销。12
using Microsoft.Extensions.Logging;
internal static partial class Log
{
[LoggerMessage(
EventId = 2001,
Level = LogLevel.Warning,
Message = "Order {OrderId} 库存预留失败。仓库={WarehouseId}")]
public static partial void StockReservationFailed(
this ILogger logger, string orderId, string warehouseId);
}
// 调用方
logger.StockReservationFailed(orderId, warehouseId);
采用这种架构,同一次日志调用就能分别把详细内容发送到文件日志(Serilog/NLog 等 sink)、把收窄后的内容发送到事件日志、把低开销的追踪通过 EventSourceLoggerProvider 发送到 ETW。基本做法是注册多个 ILoggerProvider,再用 Filter 按 provider 分别调整输出级别。自制记录器的最低要求,整理在「自制记录器的最低要求与集成测试检查清单」;崩溃时该如何把日志与转储文件关联起来保留,整理在「Windows 应用程序崩溃时保留日志与转储的设计」,可供参考。
6. 采集与排查 ── 读取已写入日志的一方
在事件查看器中,可以从日志列表使用「筛选当前日志」或「创建自定义视图」,只筛出特定来源、级别、事件 ID 的项目。经常使用的条件,建议保存为自定义视图,之后排查会更快。自定义视图是用 XPath 查询定义的,举例来说,仅筛出特定事件名称的查询大致如下。13
<QueryList>
<Query Id="0" Path="Application">
<Select Path="Application">
*[System[Provider[@Name='KomuraSoft.OrderService'] and (Level=2 or Level=3)]]
</Select>
</Query>
</QueryList>
在命令行中,可以用 wevtutil 导出日志或更改设置。作为故障排查的一环,也可以把事件发生前后的事件日志导出,发给支持团队,这种运维方式很实用。14
:: 将 Application 日志整体导出为 evtx 文件
wevtutil epl Application C:\logs\application_20260707.evtx
:: 仅显示特定来源的事件(最近 10 条)
wevtutil qe Application /q:"*[System[Provider[@Name='KomuraSoft.OrderService']]]" /c:10 /rd:true /f:text
在崩溃排查中,基本做法是把事件日志、文件日志、崩溃转储这三者按时间对齐。以事件日志中留下的「严重错误」时间戳为起点,比对同一时刻的文件日志细节,以及用 WER/ProcDump 采集到的转储文件。转储文件的采集方式整理在「Windows 崩溃转储采集入门 - WER/ProcDump/WinDbg」,采集后的实际分析步骤整理在「用 WinDbg + SOS 读取崩溃转储」。
7. 常见陷阱
- 没注册来源就想写入,结果失败。 用未注册的来源名称进行相当于
RegisterEventSource的调用时,事件本身确实会写入 Application 日志,但由于没有对应的消息资源 DLL,事件查看器无法显示说明文字,会呈现为错误。15 更进一步,如果设计成在运行时用CreateEventSource自动注册,很容易在非管理员用户运行的进程首次启动时因异常而崩溃。来源注册务必在安装程序端就完成。 - 「居然可以用别的来源名称写入」的混乱。 事件日志只要有写入权限,就能用应用程序从未注册过的其他来源名称写入。来源名称理应在一台计算机上保持唯一,但操作系统并没有强制这一点的机制,全靠应用程序端自律。5 如果选用过于通用的来源名称(不含公司名或产品名的短名称),就容易和其他厂商的应用程序冲突,或不小心沿用了其他应用程序的来源名称。
- 日志膨胀与保留策略。 事件日志的默认最大容量仅有 512KB。4 如果业务应用程序会日常使用事件日志,就要用
wevtutil sl或安装程序显式扩大容量,并在设计时明确达到上限后要「从旧数据开始覆盖」还是「丢弃新数据」。还要注意OverwriteOlder已经被标记为弃用,即使指定了,实际上仍可能变成「不覆盖」的行为。16 - 多语言环境下的消息显示。 如果采用本地化的消息资源文件(
MessageResourceFile),一旦在缺少对应资源 DLL、或版本不一致的环境中查看日志,就会显示为「无法找到事件 ID ○○ 的说明」。像是把转发过来的事件日志拿到别的服务器上看,或应用程序已经卸载、却只留下日志,都是容易出现这类问题的场景。如果采用直接用WriteEntry写入字符串(不使用资源文件)的方案,就可以从根本上避免这类问题。 - 因权限不足而无法写入的情况。 这里常被误解:注册来源需要管理员权限,但向已注册的来源写入,标准用户也可以做到。请不要一看到「没有管理员权限就跑不动」就急着把整个应用程序改成以管理员身份运行,先分清楚实际上是哪个操作需要权限。
相关文章
- Windows 什么时候需要管理员权限 - UAC、受保护区域与设计上的判断方式
- Windows 服务的创建方式与运维 ── 从与任务计划程序的分工到将 BackgroundService 服务化
- 任务计划程序的任务不执行、或以 0x1 结束 ── 原因排查与安全的运维设计
- 自制记录器的最低要求与集成测试检查清单
- Windows 应用程序崩溃时保留日志与转储的设计
- Windows 崩溃转储采集入门 - WER/ProcDump/WinDbg
- 用 WinDbg + SOS 读取崩溃转储 ── 采集之后的实务分析入门
相关咨询领域
合同会社小村软件承接 Windows 业务应用程序的日志、事件日志、ETW 基础设施设计,以及以故障排查为前提的观测点设计技术咨询。
参考链接
-
Microsoft Learn, EventLog.CreateEventSource Method. 说明从 Windows Vista 起,创建事件来源需要管理员权限的原因(必须搜索包含 Security 日志的所有日志)。 ↩ ↩2
-
Microsoft Learn, Getting Started with EventSource. 说明 EventSource 以发布—订阅模式运行,没有订阅者(ETW 或 EventPipe)就不会记录事件,以及用 dotnet-trace collect 采集的方法。 ↩ ↩2
-
Microsoft Learn, CA2254: Template should be a static expression. 说明在日志消息模板中使用字符串拼接或插值,会让占位符名称与值的对应关系丢失。 ↩ ↩2
-
Microsoft Learn, EventLog.MaximumKilobytes Property. 说明事件日志的默认最大容量为 512 KB。 ↩ ↩2
-
Microsoft Learn, EventLog Class. 说明 Application/System/Security 三种默认日志、来源必须在同一台计算机上保持唯一,以及只要有写入权限就能用任意已注册来源名称写入的机制。 ↩ ↩2
-
Microsoft Learn, Logging providers in .NET. 说明 Generic Host 默认的日志提供程序包含 Console、Debug、EventSource、EventLog(仅 Windows)。 ↩
-
Microsoft Learn, EventLogSettings Class. 说明 AddEventLog 的默认值(LogName 为 “Application”、SourceName 为 “.NET Runtime”)。 ↩
-
Microsoft Learn, Event Tracing. 说明 Event Tracing for Windows(ETW)是用于启动、停止、埋点与消费应用程序追踪事件的机制。 ↩
-
Microsoft Learn, dotnet-trace performance analysis utility. 说明基于 EventPipe 的跨平台追踪采集工具 dotnet-trace 的概要与 collect 命令。 ↩
-
Microsoft Learn, Introduction to WPR. 说明 Windows Performance Recorder(WPR)扩展了 ETW,是提供系统与应用程序详细记录的工具。 ↩
-
Microsoft Learn, Logging in C# and .NET. 说明日志消息模板的 {PlaceHolder} 语法,以及模板中的键名如何成为日志的属性名称。 ↩
-
Microsoft Learn, High-performance logging in .NET. 说明通过 LoggerMessageAttribute 生成源代码的日志方式,可以避免装箱和模板解析的开销。 ↩
-
Microsoft Learn, Comparison of ETW and EventLog logger functionality. 说明在事件查看器中使用 XPath 查询,按事件名称等条件创建自定义视图的方法。 ↩
-
Microsoft Learn, wevtutil. 说明导出事件日志(epl)、查询(qe)、更改设置(sl)等命令行操作。 ↩
-
Microsoft Learn, Event Sources. 说明用未注册的来源名称写入时会回退到 Application 日志,但由于没有消息文件而导致事件查看器无法显示说明文字、呈现为错误的情况。 ↩
-
Microsoft Learn, OverflowAction Enum. 说明事件日志达到容量上限时的行为(覆盖、丢弃),以及 OverwriteOlder 已被标记为弃用。 ↩
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
用 PerfView 与 dotnet-trace 定位“变慢”的原因 ── .NET 性能排查实务入门
当业务应用出现“变慢”“CPU 占满”“偶尔卡死”时,该用哪个工具看什么?本文梳理 PerfView 与 dotnet-trace 的分工、CPU 采样的解读方法(inclusive/exclusive)、用 ThreadTime 排查阻塞时间,以及与 EventSourc...
用 WinDbg + SOS 解读崩溃转储 ── 采集之后的实务分析入门
本文说明如何用 WinDbg 与 SOS 扩展实际读取已采集的 Windows 崩溃转储。内容涵盖符号路径设置、以 !clrstack、!pe、!dumpheap -stat、!gcroot 追踪异常与内存泄漏的方法、原生崩溃的 !analyze -v,以及与 dotnet...
不只是 appsettings.json ── Windows 业务应用的配置管理实务(环境差异化设置・敏感信息・写入位置)
本文从实务角度整理 Windows 业务应用的配置管理,涵盖 appsettings.json 的分层、IConfiguration 与 IOptions/IOptionsSnapshot/IOptionsMonitor 的选用方式、环境差异化设置、可写入配置的存放位置、敏...
Windows 业务应用程序的打印与 PDF 输出 ── System.Drawing.Printing / WPF / 报表库的取舍选型
本文按需求整理成判断表,梳理 PrintDocument 的 WinForms 打印、WPF 的 FlowDocument/FixedDocument 打印,以及 PDF 输出的可选方案。并从实务角度讲解分页控制、DPI 偏差等实现陷阱,以及从 Windows 服务打印、依...
在 C# 中安全调用 Win32 API —— P/Invoke 实务指南(DllImport / LibraryImport / CsWin32)
本文整理在 C# 中通过 P/Invoke 调用 Win32 API 或原生 DLL 时的实务要点:DllImport 与 LibraryImport 的区别、用 CsWin32 自动生成签名、字符串封送的陷阱、用 SafeHandle 管理句柄、SetLastError ...
常见问题
汇总了咨询这一主题时常见的问题。
- 有文件日志的话,事件日志就不需要了吗?
- 如果只是做详细排查,文件日志往往已经足够。但如果希望运维人员能通过操作系统自带的事件查看器或监控工具(例如任务计划程序的失败通知、SCOM 等)察觉应用程序的异常,同时把要点写入事件日志会更实用。两者不是替代关系,而是分属不同层级的记录。
- 事件 ID 的编号规则该怎么定?
- 没有固定答案,但实务中遵循以下三点可以减少事故:『按功能领域分段(例如启动类用 1000 段、通信类用 2000 段等)』、『一旦分配出去的 ID 不再改变含义、也不重复使用』、『允许出现空号』。ILogger 的 EventId 也会被当作结构化日志的检索键,因此采用便于事后归档的编号方式,会对维护很有帮助。
- 我们在用 Serilog 或 NLog,这篇文章和它们有什么关系?
- Serilog 和 NLog 是在 ILogger 之下,把同一条日志分发到文件、事件日志、ETW、外部 SaaS 等多个 sink 的一种实现方式。本文提到的『事件日志只写要点』『不要破坏消息模板』这些设计方针,无论是直接使用 ILogger,还是通过 Serilog/NLog,同样适用。至于是用事件日志专用的 sink(例如 Serilog.Sinks.EventLog),还是用标准的 AddEventLog,属于实现层面的选择。
- ETW 学到什么程度才算够?
- 如果主要目的是开发和维护业务应用程序,只要掌握到能用 EventSource 创建自己的 provider、能用 dotnet-trace 采集自己 provider 的事件,就已经足够。用 PerfView 做调用栈分析,或用 WPR 采集内核事件,属于专门做性能排查阶段才需要学习的内容,本文刻意把它们排除在范围之外。
作者简介
本文作者的个人简介页面。
Go Komura
小村软件有限公司 代表
以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。