本文适用于:✔️ .NET Core 3.1 及更高版本 ✔️ .NET Framework 4.5 及更高版本
入门指南 介绍了如何创建最小 EventSource 并在跟踪文件中收集事件。 本教程详细介绍了如何使用 System.Diagnostics.Tracing.EventSource创建事件。
最小 EventSource
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
}
派生 EventSource 的基本结构始终相同。 特别是:
- 该类继承自 System.Diagnostics.Tracing.EventSource
- 对于要生成的每种不同类型的事件,需要定义一个方法。 应使用所创建事件的名称命名此方法。 如果事件具有其他数据,则应使用参数传递这些数据。 需要序列化这些事件参数,因此只允许 某些类型。
- 每个方法都有一个调用 WriteEvent 的正文,用于向其传递 ID(表示事件的数值)和事件方法的参数。 ID 需要在 EventSource 中是唯一的。 此 ID 使用 System.Diagnostics.Tracing.EventAttribute 显式分配
- EventSources 旨在作为单一实例。 因此,通过称为
Log
的约定来定义表示此单一实例的静态变量是方便的。
定义事件方法的规则
- 默认情况下,EventSource 类中定义的任何实例(非虚拟的 void 返回方法)都是事件日志记录方法。
- 只有标记为 System.Diagnostics.Tracing.EventAttribute 的虚拟方法或非空返回的方法才会被包含。
- 若要将限定方法标记为非日志记录,则必须使用 System.Diagnostics.Tracing.NonEventAttribute 进行修饰
- 事件日志记录方法具有与其关联的事件 ID。 可以通过使用 System.Diagnostics.Tracing.EventAttribute 修饰方法显式完成此操作,也可通过该类中方法的序列号来隐式完成。 例如,使用隐式编号时,类中的第一个方法 ID 为 1,第二个方法具有 ID 2,依此类而行。
- 事件日志记录方法必须调用 WriteEvent、WriteEventCore、WriteEventWithRelatedActivityId 或 WriteEventWithRelatedActivityIdCore 重载。
- 事件 ID(无论是隐含的还是显式的)必须与传递给它调用的 WriteEvent* API 的第一个参数匹配。
- 传递给 EventSource 方法的参数的数量、类型和顺序必须与传递给 WriteEvent* API 的方式保持一致。 对于 WriteEvent,参数遵循事件 ID,对于 WriteEventWithRelatedActivityId,参数遵循 relatedActivityId。 对于 WriteEvent*Core 方法,参数必须手动序列化为
data
参数。 - 事件名称不能包含
<
或>
字符。 虽然用户定义的方法也不能包含这些字符,但编译器将重写async
方法以包含这些字符。 若要确保这些生成的方法不会成为事件,请使用 NonEventAttribute在 EventSource 上标记所有非事件方法。
最佳做法
- 派生自 EventSource 的类型通常没有层次结构中的中间类型或实现接口。 请参阅下面的 高级自定义 以了解某些可能有用的例外。
- 通常,EventSource 类的名称是 EventSource 的不良公共名称。 公共名称(日志记录配置和日志查看器中显示的名称)应全局唯一。 因此,最好使用 System.Diagnostics.Tracing.EventSourceAttribute为 EventSource 提供公共名称。 上面使用的名称“Demo”较短,不太可能是唯一的,因此不适合在生产环境中使用。 常见的约定是将分层名称与
.
或-
用作分隔符(例如“MyCompany-Samples-Demo”)或 EventSource 为其提供事件的程序集或命名空间的名称。 不建议在公共名称中包含“EventSource”。 - 显式分配事件 ID,这样对源类中的代码(例如重新排列或添加中间的方法)似乎良性更改不会更改与每个方法关联的事件 ID。
- 在创作表示工作单元的开始和结束的事件时,按照约定,这些方法的名称会添加后缀“Start”和“Stop”。 例如,“RequestStart”和“RequestStop”。
- 请勿为 EventSourceAttribute 的 Guid 属性指定显式值,除非出于向后兼容性原因需要它。 默认 Guid 值派生自源的名称,它允许工具接受更可读的名称并派生相同的 Guid。
- 在执行任何与触发事件相关的资源密集型工作之前调用 IsEnabled(),例如在禁用事件时计算不需要的高开销事件参数。
- 尝试使 EventSource 对象恢复兼容并相应地对其进行版本控制。 事件的默认版本为 0。 可以通过设置 EventAttribute.Version来更改版本。 每当更改使用事件进行序列化的数据时,更改该事件的版本。 始终将新的序列化数据添加到事件声明的末尾,即方法参数列表的末尾。 如果无法执行此操作,请使用新的 ID 创建新事件以替换旧事件。
- 声明事件方法时,请先指定固定大小的有效负载数据,然后再指定可变大小的数据。
- 不要使用包含 null 字符的字符串。 生成 ETW EventSource 的清单时,会将所有字符串声明为以 null 结尾,即使 C# 字符串中可能具有 null 字符。 如果字符串包含 null 字符,则整个字符串将写入事件有效负载,但任何分析器都将将第一个 null 字符视为字符串的末尾。 如果字符串后面有有效负载参数,将分析字符串的其余部分,而不是预期值。
典型事件自定义
设置事件详细级别
每个事件都有详细级别,事件订阅者通常会启用 EventSource 上的所有事件,直到达到特定的详细级别。 事件使用 Level 属性声明其详细级别。 例如,在下面的这个 EventSource 中,请求信息级和更低级别事件的订阅者将不会记录详细的 DebugMessage 事件。
[EventSource(Name = "MyCompany-Samples-Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1, Level = EventLevel.Informational)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
[Event(2, Level = EventLevel.Verbose)]
public void DebugMessage(string message) => WriteEvent(2, message);
}
如果未在 EventAttribute 中指定事件的详细级别,则默认为信息级别。
最佳做法
对于相对罕见的警告或错误,请使用小于 Informational 的级别。 如有疑问,请使用“信息”默认值,并对发生频率超过 1000 次事件/秒的事件使用“详细”级别。
设置事件关键字
某些事件跟踪系统支持关键字作为其他筛选机制。 与按细节级别对事件进行分类的冗长性不同,关键词的目的是根据其他标准对事件进行分类,例如代码功能领域,或有助于诊断某些问题。 关键字称为位标志,每个事件都可以应用任意组合的关键字。 例如,下面的 EventSource 定义了一些与请求处理相关的事件,以及与启动相关的其他事件。 如果开发人员希望分析启动的性能,则他们只能允许记录使用 startup 关键字标记的事件。
[EventSource(Name = "Demo")]
class DemoEventSource : EventSource
{
public static DemoEventSource Log { get; } = new DemoEventSource();
[Event(1, Keywords = Keywords.Startup)]
public void AppStarted(string message, int favoriteNumber) => WriteEvent(1, message, favoriteNumber);
[Event(2, Keywords = Keywords.Requests)]
public void RequestStart(int requestId) => WriteEvent(2, requestId);
[Event(3, Keywords = Keywords.Requests)]
public void RequestStop(int requestId) => WriteEvent(3, requestId);
public class Keywords // This is a bitvector
{
public const EventKeywords Startup = (EventKeywords)0x0001;
public const EventKeywords Requests = (EventKeywords)0x0002;
}
}
必须使用名为 Keywords
的嵌套类定义关键字,并且每个关键字由类型化 public const EventKeywords
的成员定义。
最佳做法
在区分大容量事件时,关键字更为重要。 这样,事件使用者可以将详细程度提升到高级别,但通过只启用小部分事件来管理性能开销和日志大小。 每秒触发超过 1,000 次的事件非常适合使用唯一关键字。
支持的参数类型
EventSource 要求可以序列化所有事件参数,因此它只接受一组有限的类型。 以下是:
- 基元:bool、byte、sbyte、char、short、ushort、int、uint、long、ulong、float、double、IntPtr 和 UIntPtr、Guid decimal、string、DateTime、DateTimeOffset、TimeSpan
- 枚举
- 具有 System.Diagnostics.Tracing.EventDataAttribute 特性的结构。 只有具有可序列化类型的公共实例属性将被序列化。
- 所有公共属性为可序列化类型的匿名类型
- 可序列化类型的数组
- Nullable<T>,其中 T 是可序列化的类型
- KeyValuePair<T、U>,其中 T 和 U 都是可序列化的类型
- 仅对 T 一种类型实现 IEnumerable<T> 且 T 为可序列化类型的类型
故障 排除
EventSource 类经过设计,因此默认情况下它永远不会引发异常。 这是一个有用的属性,因为日志记录通常被视为可选,并且通常不希望错误写入日志消息,导致应用程序失败。 但是,这会使在 EventSource 中发现任何错误变得困难。 下面是一些可帮助进行故障排除的技术:
- EventSource 构造函数包含采用 EventSourceSettings 的重载。 请尝试暂时启用 ThrowOnEventWriteErrors 标志。
- EventSource.ConstructionException 属性存储验证事件日志记录方法时生成的任何异常。 这可以揭示各种创作错误。
- EventSource 使用事件 ID 0 记录错误,并且此错误事件具有描述错误的字符串。
- 调试时,也会使用 Debug.WriteLine() 记录相同的错误字符串,并在调试输出窗口中显示。
- 发生错误时,EventSource 会在内部引发异常,然后捕获该异常。 若要观察何时出现这些异常,请在调试器中启用第一次机会异常或使用事件跟踪,并启用 .NET 运行时的异常事件。
高级自定义
设置操作码和任务
ETW 具有 任务和 OpCodes的概念,这些概念是标记和筛选事件的进一步机制。 可以使用 Task 和 Opcode 属性将事件与特定任务和操作码相关联。 下面是一个示例:
[EventSource(Name = "Samples-EventSourceDemos-Customized")]
public sealed class CustomizedEventSource : EventSource
{
static public CustomizedEventSource Log { get; } = new CustomizedEventSource();
[Event(1, Task = Tasks.Request, Opcode=EventOpcode.Start)]
public void RequestStart(int RequestID, string Url)
{
WriteEvent(1, RequestID, Url);
}
[Event(2, Task = Tasks.Request, Opcode=EventOpcode.Info)]
public void RequestPhase(int RequestID, string PhaseName)
{
WriteEvent(2, RequestID, PhaseName);
}
[Event(3, Keywords = Keywords.Requests,
Task = Tasks.Request, Opcode=EventOpcode.Stop)]
public void RequestStop(int RequestID)
{
WriteEvent(3, RequestID);
}
public class Tasks
{
public const EventTask Request = (EventTask)0x1;
}
}
可以通过声明两个具有关联事件 ID 的事件方法来隐式创建 EventTask 对象,事件方法的命名模式为 <EventName>Start 和 <EventName>Stop。 这些事件必须在类定义中彼此旁边声明,<EventName>Start 方法必须首先声明。
自描述(跟踪日志记录)与清单事件格式
仅当从 ETW 订阅 EventSource 时,此概念才重要。 ETW 有两种不同的方法可以记录事件,即清单格式和自描述格式(有时称为踪迹记录)。 基于清单的 EventSource 对象生成并记录一个 XML 文档,该文档表示初始化时在类上定义的事件。 这要求 EventSource 自我反射以生成供应方和事件元数据。 在自我描述格式中,每个事件的元数据都随事件数据内联传输,而不是预先传输。 自我描述方法支持更灵活的 Write 方法,这些方法可以发送任意事件,而无需创建预定义的事件日志记录方法。 它在启动时也会稍微快一点,因为它避免了急切反映。 但是,随每个事件发出的额外元数据会增加较小的性能开销,在发送大量事件时可能不希望发生这种情况。
若要使用自描述事件格式,请使用 EventSource(String) 构造函数、EventSource(String、EventSourceSettings) 构造函数或通过在 EventSourceSettings 上设置 EtwSelfDescribingEventFormat 标志来构造 EventSource。
实现接口的 EventSource 类型
EventSource 类型可以通过实现接口来无缝集成到各种利用接口定义通用日志目标的高级日志记录系统中。 下面是可能使用的一个示例:
public interface IMyLogging
{
void Error(int errorCode, string msg);
void Warning(string msg);
}
[EventSource(Name = "Samples-EventSourceDemos-MyComponentLogging")]
public sealed class MyLoggingEventSource : EventSource, IMyLogging
{
public static MyLoggingEventSource Log { get; } = new MyLoggingEventSource();
[Event(1)]
public void Error(int errorCode, string msg)
{ WriteEvent(1, errorCode, msg); }
[Event(2)]
public void Warning(string msg)
{ WriteEvent(2, msg); }
}
必须在接口方法上指定 EventAttribute,否则(出于兼容性原因),该方法不会被视为日志记录方法。 不允许显式接口方法实现,以防止命名冲突。
EventSource 类层次结构
在大多数情况下,你将能够编写直接派生自 EventSource 类的类型。 但是,有时定义由多个派生 EventSource 类型共享的功能(例如自定义的 WriteEvent 重载)很有用(请参阅下面的为大量事件优化性能)。
只要抽象基类不定义任何关键字、任务、操作码、通道或事件,就可以使用抽象基类。 下面是一个 UtilBaseEventSource 类定义优化的 WriteEvent 重载的示例,同一组件中的多个派生的 EventSource 需要此重载。 下面显示了其中一种派生类型,如 OptimizedEventSource 所示。
public abstract class UtilBaseEventSource : EventSource
{
protected UtilBaseEventSource()
: base()
{ }
protected UtilBaseEventSource(bool throwOnEventWriteErrors)
: base(throwOnEventWriteErrors)
{ }
protected unsafe void WriteEvent(int eventId, int arg1, short arg2, long arg3)
{
if (IsEnabled())
{
EventSource.EventData* descrs = stackalloc EventSource.EventData[2];
descrs[0].DataPointer = (IntPtr)(&arg1);
descrs[0].Size = 4;
descrs[1].DataPointer = (IntPtr)(&arg2);
descrs[1].Size = 2;
descrs[2].DataPointer = (IntPtr)(&arg3);
descrs[2].Size = 8;
WriteEventCore(eventId, 3, descrs);
}
}
}
[EventSource(Name = "OptimizedEventSource")]
public sealed class OptimizedEventSource : UtilBaseEventSource
{
public static OptimizedEventSource Log { get; } = new OptimizedEventSource();
[Event(1, Keywords = Keywords.Kwd1, Level = EventLevel.Informational,
Message = "LogElements called {0}/{1}/{2}.")]
public void LogElements(int n, short sh, long l)
{
WriteEvent(1, n, sh, l); // Calls UtilBaseEventSource.WriteEvent
}
#region Keywords / Tasks /Opcodes / Channels
public static class Keywords
{
public const EventKeywords Kwd1 = (EventKeywords)1;
}
#endregion
}
针对大量事件优化性能
EventSource 类具有多个 WriteEvent 重载,包括一个用于可变数量参数的重载。 当其他重载都不匹配时,将调用 params 方法。 遗憾的是,参数重载相对昂贵。 具体表现在:
- 分配数组以保存变量参数。
- 将每个参数强制转换为对象,这会导致值类型的分配。
- 将这些对象分配给数组。
- 调用函数。
- 找出每个数组元素的类型,以确定如何序列化它。
这可能比专用类型高 10 到 20 倍。 对于低量情况而言,这并不重要,但对于大量事件,这很重要。 以下有两个重要的事项可确保不使用参数重载:
- 确保枚举类型强制转换为“int”,以便它们与其中一个快速重载匹配。
- 为高容量有效负载创建新的快速 WriteEvent 重载。
下面是添加采用四个整数参数的 WriteEvent 重载的示例
[NonEvent]
public unsafe void WriteEvent(int eventId, int arg1, int arg2,
int arg3, int arg4)
{
EventData* descrs = stackalloc EventProvider.EventData[4];
descrs[0].DataPointer = (IntPtr)(&arg1);
descrs[0].Size = 4;
descrs[1].DataPointer = (IntPtr)(&arg2);
descrs[1].Size = 4;
descrs[2].DataPointer = (IntPtr)(&arg3);
descrs[2].Size = 4;
descrs[3].DataPointer = (IntPtr)(&arg4);
descrs[3].Size = 4;
WriteEventCore(eventId, 4, (IntPtr)descrs);
}