You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

30 KiB

高级事件日志学习教程

基于EventSource 的日志架最初是为 Windows的ETW(Event Tracing for Windows)设计的。由于采用了发布订阅的设计思想,所以这个日志框架在日志生产和消费上是互相独立的。 对于 EventSource 发出的日志事件ETW 并非唯一的消费者我们可以通过创建的EventListener对象来订阅感兴趣的日志事件并对得到的日志消息进行针对性处理。

EventSource

在大部分情况下,我们倾向于定义一个派生于 EventSource 的子类型,并将日志事件的发送实现在某个方法中,不过这项工作也可以直接由创建的 EventSource 对象来完成。 鉴于这两种不同的编程模式EventSource定义了两组(Protected和 Public)构造函数

public class EventSource : IDisposable
{
	public string					Name {get;set;}
	public Guid						Guid {get;set;}
	public Exception				ConstructionException {get;}
	public EventSourceSettings		Settings {get;}

	protected EventSource();
	protected EventSource(bool throwOnEventWriteErrors);
	protected EventSource(EventSourceSettings settings);
	protected EventSource(EventSourceSettings settings, params stringl] traits);

	public EventSource(string eventSourceName);
	public EventSource(string eventSourceName, EventSourceSettings config); 
	public EventSource(string eventSourceName, EventSourceSettings config, params stringl] traits);
}

一个 EventSource 对象要求具有一个明确的名称,所以调用公共的构造函数时必须显式指定比名称。 对于 EventSource 的派生类来说如果类型上没有通过标注的EventSourceAttribute特生对名称进行显式设置则类型的名称将会作为EventSource的名称。 一个 EventSource 对象还要求被赋予一个GUID作为唯一标识。 对于直接创建的 EventSource 对象来说该标识是由指定的名称计算出来的EventSource的派生类型则可以通过标注的EventSourceAttribute特性来对这个标识进行显式设置。 EventSource的Settings 属性用于返回一个EventSourceSettings对象这是一个标注了FlagsAttribute特性的枚举。该枚举对象针对EventSource的“设置”Settings主要体现在两个方法指定输出日志的格式Manifest或者Manifest Self-Describing以及决定是否应该抛出日志写入过程中出现的异常的设置。

[Flags]
public enum EventSourceSettings
{
	Default =0
	ThrowOnEventWriteErrors =1,
	EtwManifestEventFormat =4
	EtwSelfDescribingEventFormat =8
}

如果没有显式设置则EventSource的派生类型的 Settings 属性默认为 EtwManifestEventFormat调用公共构造函数创建的EventSource 对象的 Settings 属性为EtwSelfDescribingEventFormat。除了上述只读属性EventSource还有一个 ConstructionException属性用于返回构造函数执行过程中抛出的异常。 在定义 EventSource 派生类时,虽然类型名称会默认作为 EventSource 的名称,但是通过在类型上标注 EventSourceAttribute特性对名称进行显式设置依然是推荐的做法。这样不但可以定义一个标准的名称如采用公司和项目或者组件名称作为前缀而目类刑改名后也不会造成任何影响。 如下面的代码片段所示除了指定EventSource的名称通过标注EventSourceAttribute特性还可以设置作为唯一标识的GUID以及用于本地化的字符串资源。

[AttributeUsage(AttributeTargets.Class)]
public sealed class EventSourceAttribute: Attribute
{
	public string Name ( get; set;)
	public string Guid ( get; set;)
	public string LocalizationResources ( get; set;)
}

对于EventSource派生类来说它可以通过调用基类的WriteEvent方法来发送日志事件。 我们在调用 WriteEvent方法时必须指定日志事件的ID必需和内容载荷可选。 如下面的代马片段所示EventSource定义了一系列的WriteEvent重载方法来提供具有成员结构的内容载荷

public class EventSource : IDisposable
{
	protected void WriteEvent(int eventId);
	protected void WriteEvent(int eventId, int argl);
	protected void WriteEvent(int eventId, long argl);
	protected void WriteEvent(int eventId, string arg1);
	protected void WriteEvent(int eventId, bytel] argl);
	protected void WriteEvent(int eventId, int argl, int arg2);
	protected void WriteEvent(int eventId, int argl, string arg2);
	protected void WriteEvent(int eventId, long argl, long arg2);
	protected void WriteEvent(int eventId, long arg1, string arg2);
	protected void WriteEvent(int eventId, long argl, bytell arg2);
	protected void WriteEvent(int eventId, string argl, int arg2);
	protected void WriteEvent(int eventId, string argl, long arg2);
	protected void WriteEvent(int eventId, string argl, string arg2);
	protected void WriteEvent(int eventId, int argl, int arg2, int arg3);
	protected void WriteEvent(int eventId, long argl, long arg2, long arg3);protected void WriteEvent(int eventId, string argl, int arg2, int arg3);protected void WriteEvent(int eventId, string argl, string arg2, string arg3);
	protected void WriteEvent(int eventId, params objectl] args);
}

由于拥有最后一个 WriteEvent重载方法所以我们可以采用一个或者多个任意类型的对象作为内容载荷这个特性被称为“Rich Event Payload”。 虽然这个特性可以使EventSource的日志编程变得很简洁但是在高频日志写入的应用场景下应该尽可能避免调用这个WriteEvent 方去,因为涉及一个对象数组的创建及对值类型对象的装箱(如果指定值类型参数)。 虽然没有硬性规定但是我们应该尽可能将EventSource派生类定义成封闭类型并且采用单例的方式来使用它这也是出于性能的考虑。 除非显式标注了NonEventAttribute特性否则定义在 EventSource派生类中返回类型为 void 的公共实例方法都将作为日志事件方法。 每个这样的方法关联着固定的事件 ID如果没有利用标注的EventAttribute特性对事件 ID进行显式设置则方法在类型成员中的序号会作为此ID。在调用上面这些 WriteEvent方法时指定的事件ID必须与当前日志事件方法对应的ID保持一致。 除了利用标注的EventAttribute特性指定事件ID还可以利用这个特性进行一些额外的设置。 如下面的代码片段所示,我们可以通过该特性设置日志等级、消息、版本等信息。

[AttributeUsage(AttributeTargets.Method)]
public sealed class EventAttribute :Attribute
{
	public int					EventId {get; private set;}
	public EventLevel			Level {get; set;}
	public string				Message {get; set;}
	public byte					Version {get; set;}
	public EventOpcode			Opcode {get; set;}
	public EventTask			Task {get; set;}
	public EventKeywords		Keywords {get; set;}
	public EventTags			Tags	{get; set;}
	public EventChannel			Channel {get; set;}
	public EventActivityOptions ActivityOptions {get; set;}

	public EventAttribute(int eventId);
}

由 EventSource 对象发出的日志事件同样具有等级之分具体的日志等级可以通过EventLevel枚举来表示从高到低划分为Critical、Error、Warning、Informational和Verbose这5个等级。 如果没有通过 EventAttribute 特性对日志等级进行显式设置则日志事件方法采用的默认等级被设置为Verbose。

public enum EventLevel
{
	LogAlways,
	Critical
	Error,
	Warning,
	Informational,
	Verbose
}

如果对日志内容具有可读性要求则最好提供一个完整的消息文本来描述当前事件EventAttribute的Message提供了一个生成此消息文本的模板。 消息模板可以采用01...n这样的占位符而调用日志事件方法传入的参数将作为替换它们的参数。 EventAttribute特性的Opcode 属性用于返回一个 EventOpcode类型的枚举该枚举表示日志事件对应操作的代码Code。 我们将定义在EventOpcode枚举中的操作代码分成如下几组 第1组的4个操作代码与活动Activity有关分别表示活动的开始Start、结束Stop、中止Suspend和恢复Resume 第2组的两个操作代码基于数据收集的活动 第3组的3个操作代码描述的是与消息交换相关的事件分别表示消息的发送Send、接收Receive和回复Reply 第4组是Info和Extension前者表示一般性的信息的输出后者表示一个扩展事件。

public enum EventOpcode
{
	Start				=1,
	Stop				=2,
	Resume				=7,
	Suspend				=8,

	DataCollectionStart	=3,
	DataCollectionStop	=4,

	Send				=9
	Receive				=240
	Reply				=6

	Info				=0
	Extension			=5,
}

如果日志事件关联某项任务Task就可以用Task属性对它进行描述。 我们还可以通过eywords属性和Tags属性为日志事件关联一些关键字Keyword与标签Tag)。 一般来说,件是根据订阅发送的,如果待发送的日志事件没有订阅者,该事件就不应该被发出,所以日事件的订阅原则是尽可能缩小订阅的范围,这样就可以将日志导致的性能影响降到最低。 如为某个日志事件定义了关键字,我们就可以对该关键字进行精准的订阅。 如果当前事件的日具有特殊的输出渠道就可以利用其Channel属性来承载输出渠道信息。 这些属性的返回类都是枚举,如下所示的代码片段展示了这些枚举类型的定义。

[Flags]
public enum EventKeywords : long
{
	A11					= -1L
	None				= OL
	AuditFailure		= 0x10000000000000L
	AuditSuccess		= 0x20000000000000L
	CorrelationHint		= 0x10000000000000L
	EventLogClassic		= 0x80000000000000L
	MicrosoftTelemetry  = 0x2000000000000L
	Sqm					= 0x8000000000000L
	WdiContext			= 0x2000000000000L
	WdiDiagnostic		= 0x4000000000000L
}

public enum EventChannel :byte
{
	None =0
	Admin =10,
	Operational =11
	Analytic 12,
	Debug =13
}

[Flags]
bublic enum EventTask
{
	None
}

[Flags]
public enum EventTags
{
	None
}

从上面的代码片段可以看出,枚举类型 EventTask 和 EventTags 并没有定义任何有意义的枚举选项只定义了一个None选项。 因为枚举的基础类型都是整型,所以日志事件订阅者最终接收的这些数据都是相应的数字,至于不同的数值具有什么样的语义则完全可以由具体的应用来决定,所以我们可以完全不用关心这些枚举的预定义选项。 EventAttribute特性之所以将这些属性定义成枚举并不是要求我们使用预定义的选项而是提供一种强类型的编程方式。 例如我们可以采用如下形式定义4个EventTags常量来表示4种数据库类型。除了 EventTask和EventTags表示关键字的EventKeywords也可以采用这种方式进行自由定义。

public class Tags
{
	public const EventTags MSSql	=(EventKeywords)1;
	public const EventTags Oracle	= (EventKeywords)2;
	public const EventTags Redis	=(EventKeywords)4;
	public const EventTags Mongodb	=(EventKeywords)8;
}

在大部分情况下单一事件的日志数据往往没有实际意义只有将在某个上下文中记录下来的一组相关日志进行聚合分析才能得到有价值的结果采用基于“活动”Activity的跟踪是关联单一日志事件的常用手段。 一个所谓的活动是具有严格的开始和结束边界,并且需要耗费一定时间才能完成的操作。活动具有标准的状态机,状态之间的转换可以通过对应的事件来表示,一个活动的生命周期介于开始事件和结束事件之间。 EventSource日志框架对基于活动的追踪Activity Tracking提供了很好的支持EventAttribute特性的ActivityOptions属性正是用来做这方面设置的。 该属性返回的 EventActivityOptions枚举具有如下选项Disable用于关闭活动追踪特性Recursive和Detachable分别表示是否允许活动以递归或者重叠的方式运行。 我们将在后续部分详细介绍这个话题。

[Flags]
public enum EventActivityoptions
{
	None		= 0,
	Disable		= 2,
	Recursive	= 4,
	Detachable	= 8
}

由上面介绍的内容可知定义在EventSource派生类中的日志方法是通过调用基类的WriteEvent方法来发送日志事件的但是对于一个直接调用公共构造函数创建的EventSource对象来说这个受保护的 WriteEvent方法是无法直接被调用的只能调用下面几个公共的 Write方法和Write方法来发送日志事件。

public class EventSource:IDisposable
{
	public void Write(string eventName);
	public void Write(string eventName, EventSourceOptions options);
	public void Write<T>(string eventName, T data);
	public void Write<T>(string eventName, EventSourceOptions options, T data); 
	public void Write<T>(string eventName, ref EventSourceOptions options, ref T data); 
	public void Write<T>(string eventName, ref EventSourceOptions options, ref Guid activityid, ref Guid relatedActivityId, ref T data);
}

在调用Write方法和Write方法时需要设置事件的名称(必须)和其他相关设置(可选),还可以指定一个对象作为内容载荷。 EventSource 相关的配置选项通过具有如下定义的EventSourceOptions结构来表示。 我们可以利用EventSourceOptions设置事件等级、操作代码、关键字、标签和活动追踪的其他选项在调用上述两个方法时也可以将EventSourceOptions作为参数。

[StructLayout(LayoutKind.Sequential)]
public struct EventSourceOptions
{
	public EventLevel Level						{get;set;}
	public EventOpcode Opcode					{get;set;}
	public EventKeywords Keywords				{get;set;}
	public EventTags Tags						{get;set;}
	public EventActivityOptions ActivityOptions {get;set;}
}

ETW 的两种格式(Manifest 和 Manifest Self-Describing分别对应枚举类型EventSourceSettings的EtwManifestEventFormat选项和EtwSelfDescribingEventFormat选项后者提供了Rich Event Payload支持。 由于泛型的Write方法采用的正是对该特性的体现所以通过公共构造函数创建的EventSource对象的Settings 属性会被自动设置为EtwSelfDescribingEventFormat。 只有在EventSource对象被订阅的前提下针对它发送日志事件才有意义为了避免无谓操作造成的性能影响我们在发送日志事件之前应该调用如下几个IsEnabled方法。 它们可以确认 EventSource对象的日志等级、关键字或者输出渠道是否具有订阅者。

public class EventSource: IDisposable
{
	public bool IsEnabled();
	Public bool IsEnabled(EventLevel level, EventKeywords keywords);
	Public bool IsEnabled(EventLevel level, EventKeywords keywords, EventChannel channel);
}

EventListener

EventListener提供了一种在进程内In-Process)订阅和处理日志事件的手段。 EventListener对象能够接收由 EventSource分发的日志事件的前提预先进行了订阅的事件。 EventListener向 EventSource就某种日志事件类型的订阅通过如下几个EnableEvents重载方法来完成。 我们在调用这些方法时可以通过指定日志等级、关键字和命令参数的方式对分发的日志事件进行过滤。 EventListener还定义了DisableEvents方法来解除订阅。

public abstract class EventListener :IDisposable
{
	public void EnableEvents(EventSource eventSource, EventLevel level);
	public void EnableEvents(EventSource eventSource, EventLevel level,EventKeywords matchAnyKeyword);
	public void EnableEvents(EventSource eventSource, EventLevel level,EventKeywords matchAnyKeyword, IDictionary<string, string> arguments);
	public void DisableEvents(EventSource eventSource);
}

虽然 EventListener 需要通过调用EnableEvents方法显式地就感兴趣的日志事件向EventSource发起订阅但是由于EventListener能够自动感知当前进程内任意一个EventSource对象的创建所以订阅变得异常容易。 EventListener类型定义了 OnEventSourceCreated 和 OnEventWritten 这两个受保护的虚方法前者会在任何一个EventSource对象创建时被调用。我们通过重写这个方法完成对目标 EventSource的订阅。 目标EventSource对象发出日志事件后相关信息会被封装成一个 EventWrittenEventArgs对象作为参数调用EventListener 的OnEventWritten方法。我们通过重写 OnEventWritten方法完成日志事件的处理。 实现在基类EventListener的这两个方法分别触发 EventSourceCreated事件和EventWritten事件所以重写这两个方法时最好能够调用基类的同名方法。

public class EventListener :IDisposable
{
	public event EventHandler<EventSourceCreatedEventArgs> EventSourceCreated;
	public event EventHandler<EventWrittenEventArgs> EventWritten;
	protected internal virtual void OnEventSourceCreated(EventSource eventSource);
	protected internal virtual void OnEventWritten(EventWrittenEventArgs eventData);
}

如下所示的代码片段是EventWrittenEventArgs类型的定义我们不仅可以通过它获取包括内容载荷在内的用于描述当前日志事件的所有信息还可以得到对应的EventSource对象。 日志事件可以采用不同的形式来指定内容载荷它们最终会转换成一个ReadOnlyCollection