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 文件保存
与操作系统标准工具的整合 无(需要自制查看器) 可用事件查看器、wevtutilGet-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 CoreGeneric 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();

如果省略 EventLogSettingsLogName 默认会是 "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-trace9

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 业务应用程序的日志、事件日志、ETW 基础设施设计,以及以故障排查为前提的观测点设计技术咨询。

参考链接

  1. Microsoft Learn, EventLog.CreateEventSource Method. 说明从 Windows Vista 起,创建事件来源需要管理员权限的原因(必须搜索包含 Security 日志的所有日志)。  2

  2. Microsoft Learn, Getting Started with EventSource. 说明 EventSource 以发布—订阅模式运行,没有订阅者(ETW 或 EventPipe)就不会记录事件,以及用 dotnet-trace collect 采集的方法。  2

  3. Microsoft Learn, CA2254: Template should be a static expression. 说明在日志消息模板中使用字符串拼接或插值,会让占位符名称与值的对应关系丢失。  2

  4. Microsoft Learn, EventLog.MaximumKilobytes Property. 说明事件日志的默认最大容量为 512 KB。  2

  5. Microsoft Learn, EventLog Class. 说明 Application/System/Security 三种默认日志、来源必须在同一台计算机上保持唯一,以及只要有写入权限就能用任意已注册来源名称写入的机制。  2

  6. Microsoft Learn, Logging providers in .NET. 说明 Generic Host 默认的日志提供程序包含 Console、Debug、EventSource、EventLog(仅 Windows)。 

  7. Microsoft Learn, EventLogSettings Class. 说明 AddEventLog 的默认值(LogName 为 “Application”、SourceName 为 “.NET Runtime”)。 

  8. Microsoft Learn, Event Tracing. 说明 Event Tracing for Windows(ETW)是用于启动、停止、埋点与消费应用程序追踪事件的机制。 

  9. Microsoft Learn, dotnet-trace performance analysis utility. 说明基于 EventPipe 的跨平台追踪采集工具 dotnet-trace 的概要与 collect 命令。 

  10. Microsoft Learn, Introduction to WPR. 说明 Windows Performance Recorder(WPR)扩展了 ETW,是提供系统与应用程序详细记录的工具。 

  11. Microsoft Learn, Logging in C# and .NET. 说明日志消息模板的 {PlaceHolder} 语法,以及模板中的键名如何成为日志的属性名称。 

  12. Microsoft Learn, High-performance logging in .NET. 说明通过 LoggerMessageAttribute 生成源代码的日志方式,可以避免装箱和模板解析的开销。 

  13. Microsoft Learn, Comparison of ETW and EventLog logger functionality. 说明在事件查看器中使用 XPath 查询,按事件名称等条件创建自定义视图的方法。 

  14. Microsoft Learn, wevtutil. 说明导出事件日志(epl)、查询(qe)、更改设置(sl)等命令行操作。 

  15. Microsoft Learn, Event Sources. 说明用未注册的来源名称写入时会回退到 Application 日志,但由于没有消息文件而导致事件查看器无法显示说明文字、呈现为错误的情况。 

  16. Microsoft Learn, OverflowAction Enum. 说明事件日志达到容量上限时的行为(覆盖、丢弃),以及 OverwriteOlder 已被标记为弃用。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

有文件日志的话,事件日志就不需要了吗?
如果只是做详细排查,文件日志往往已经足够。但如果希望运维人员能通过操作系统自带的事件查看器或监控工具(例如任务计划程序的失败通知、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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表