检测代码以创建 EventSource 事件

本文适用范围:✔️ .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

用于定义事件方法的规则

  1. 在 EventSource 类中定义的任何实例、非虚拟的返回空值方法都默认为事件日志记录方法。
  2. 仅当虚拟方法或非返回空值方法标记有 System.Diagnostics.Tracing.EventAttribute 时,才将这些方法包含在内
  3. 若要将限定方法标记为非日志记录,则必须使用 System.Diagnostics.Tracing.NonEventAttribute 进行修饰
  4. 事件日志记录方法具有与其关联的事件 ID。 可以通过使用 System.Diagnostics.Tracing.EventAttribute 修饰方法显式完成此操作,也可通过该类中方法的序列号来隐式完成。 例如,若使用隐式编号,该类中的第一个方法的 ID 为 1,第二个方法的 ID 为 2,依此类推。
  5. 事件日志记录方法必须调用 WriteEventWriteEventCoreWriteEventWithRelatedActivityIdWriteEventWithRelatedActivityIdCore 重载。
  6. 无论是隐含还是显式,事件 ID 都必须与传递给它所调用的 WriteEvent* API 的第一个实参匹配。
  7. 传递给 EventSource 方法的实参的数量、类型和顺序必须与传递到 WriteEvent* API 的一致。 对于 WriteEvent,实参跟在事件 ID 后,对于 WriteEventWithRelatedActivityId,实参跟在 relatedActivityId 之后。 对于 WriteEvent*Core 方法,必须手动将实参序列化为 data 形参。
  8. 事件名称不能包含 <> 字符。 虽然用户定义的方法也不能包含这些字符,但编译器将重写 async 方法以包含这些字符。 要确保这些生成的方法不会成为事件,请使用 NonEventAttributeEventSource 上标记所有非事件方法。

最佳做法

  1. 从 EventSource 派生的类型通常在层次结构中没有中间类型并且不实现接口。 请参阅下面的高级自定义,了解一些可能有用的异常。
  2. 通常,EventSource 类的名称是 EventSource 的错误公用名称。 公用名称、日志记录配置和日志查看器中显示的名称都应该是全局唯一的。 因此,最好使用 System.Diagnostics.Tracing.EventSourceAttribute 为 EventSource 指定一个公共名称。 上面使用的名称“Demo”很简短,不太可能是唯一的,因此不适合用于生产。 常见约定是使用以 .- 作为分隔符的分层名称,例如“MyCompany-Samples-Demo”,或者使用 EventSource 为其提供事件的程序集或命名空间的名称。 不建议将“EventSource”作为公共名称的一部分。
  3. 显式分配事件 ID,这样一来,对源类中的代码进行看似良性的更改(如重新排列它或在中间添加方法)不会更改与每个方法关联的事件 ID。
  4. 在创作表示工作单元的开始和结束的事件时,按照约定,这些方法的名称会添加后缀“Start”和“Stop”。 例如“RequestStart”和“RequestStop”。
  5. 不要为 EventSourceAttribute 的 Guid 属性指定显式值,除非出于向后兼容的原因需要它。 默认 Guid 值派生自源的名称,它允许工具接受可读性更强的名称,并派生相同的 Guid。
  6. 在执行任何与触发事件相关的资源密集型工作之前调用 IsEnabled(),例如在禁用事件时计算不需要的高开销事件参数。
  7. 尝试让 EventSource 对象保持向后兼容,并适当地对其进行版本控制。 事件的默认版本为 0。 通过设置 EventAttribute.Version,可以更改版本。 每当更改使用事件进行序列化的数据时,更改该事件的版本。 务必将新的序列化数据添加到事件声明的末尾,即添加到方法参数列表的末尾。 如果无法做到这一点,请使用新 ID 创建新的事件来替换旧的事件。
  8. 声明事件方法时,请在大小不定的数据之前指定固定大小的有效负载数据。
  9. 不要使用包含空字符的字符串。 为 ETW 生成清单时,EventSource 将声明所有字符串都以 null 结尾,不过 C# 字符串中可能包含空字符。 如果某个字符串包含空字符,则会将整个字符串写入事件有效负载,但任何分析程序都会将第一个空字符视为字符串的结尾。 如果字符串后面有有效负载参数,则会分析字符串的其余部分而不是预期值。

典型事件自定义

设置事件详细级别

每个事件都有详细级别,事件订阅者通常会启用 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 中指定事件的详细级别,则默认为“信息”级别。

最佳做法

对于相对较少的警告或错误,使用低于信息的级别。 如有疑问,请使用“信息”默认值,并对发生频率超过 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 中的任何错误。 可以通过以下几种方法来解决问题:

  1. EventSource 构造函数包含采用 EventSourceSettings 的重载。 请尝试暂时启用 ThrowOnEventWriteErrors 标志。
  2. EventSource.ConstructionException 属性存储在验证事件日志记录方法时生成的任何异常。 这可能会揭示各种创作错误。
  3. EventSource 使用事件 ID 0 记录错误,此错误事件具有可描述错误的字符串。
  4. 调试时,同一个错误字符串也将使用 Debug.WriteLine() 进行记录,并显示在调试输出窗口中。
  5. 发生错误时,EventSource 会在内部引发异常,然后捕获该异常。 若要观察何时出现这些异常,请在调试器中启用第一次机会异常或使用事件跟踪,并启用 .NET 运行时的异常事件

高级自定义

设置操作码和任务

ETW 具有任务和操作码的概念,这些概念是标记和筛选事件的进一步机制。 可以使用 TaskOpcode 属性将事件与特定任务和操作码关联。 下面是一个示例:

[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;
    }
}

可以通过使用具有命名模式 <EventName>Start 和 <EventName>Stop 的后续事件 ID 声明两个事件方法,隐式创建 EventTask 对象。 这些事件必须在类定义中依次声明,<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 方法。 遗憾的是,参数重载成本开销较高。 具体表现在:

  1. 分配一个数组来保存变量参数。
  2. 将每个参数强制转换为导致值类型分配的对象。
  3. 将这些对象分配给数组。
  4. 调用 函数。
  5. 弄清楚每个数组元素的类型,以确定如何序列化。

它的开销可能比专用类型高 10 到 20 倍。 对于低容量情况,这并不重要,但对于高容量事件,这一点很重要。 以下有两个重要的事项可确保不使用参数重载:

  1. 确保枚举类型强制转换为“int”,以便它们与其中一个快速重载匹配。
  2. 为高容量有效负载创建新的快速 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);
}