如何利⽤ETW(EventTracingforWindows)记录⽇志
ETW是Event Tracing for Windows的简称,它是Windows提供的原⽣的事件跟踪⽇志系统。由于采⽤内核(Kernel)层⾯的缓冲和⽇志记录机制,所以ETW提供了⼀种⾮常⾼效的事件跟踪⽇志解决⽅案。
⼀、ETW模型
事件监测(Event Instrumentation)总会包含两个基本的实体,事件的提供者(ETW Provider)和消费者(ETW Consumer),ETW框架可以视为它们的中介。ETW Provider会预先注册到ETW框架上,提供者程序在某个时刻触发事件,并将标准化定义的事件提供给ETW框架。Consumer同样需要注册到ETW框架上,在注册的时候可以设置事件的删选条件和接收处理事件的回掉。对于接收到的事件,如果它满⾜某个注册ETW Consumer的筛选条件,ETW会调⽤相应的回调来处理该事件。
ETW针对事件的处理是在某个会话(ETW Session)中进⾏的,ETW Session提供了⼀个接收、存储、处理和分发事件的执⾏上下⽂。ETW框架可以创建多⼀个会话来处理由提供者程序发送的事件,但是ETW Session并不会与某个单⼀的提供者绑定在⼀起,多个提供者程序可以向同⼀个ETW Session发送事件。对于接收到的事件,ETW Session可以将它保存在创建的⽇志⽂件中,也可以实时地分发给注册的消费者应⽤。ETW会话的开启和终⽌是通过 Session的开启和终⽌是通过ETW控制器
(ETW Controller)进⾏管理的。除了管理ETW Session之
外,ETW Controller还可以禁⽤(Disable)或者恢复(Enable)注册到某个ETW Session上的ETW Provider。
综上所述,整个ETW模型由ETW框架本⾝和ETW Provider、ETW Consumer以及ETW Controller组成,上图很好地展⽰了这四者之间的关系。出于篇幅的限制,我们只能对ETW作⼀个粗略的介绍,实际上ETW⾃⾝是⼀个⾮常强⼤的事件跟踪⽇志系统,有兴趣的朋友可以参阅相关的⽂档进⾏系统学习。
⼆、针对ETW的编程
.NET提供了⼀种⽐较独特并且简单的编程模式编写针对ETW的事件提供和消费程序。所谓的ETW Provider就是事件的提供者,它体现了事件的来源,对应着⼀个EventSource对象。EventSource类型定义在“System.Diagnostics.Tracing”这个NuGet包中。如下⾯的代码⽚段所⽰,EventSource类型中定义了⼀系列WriteEvent⽅法重载,这些⽅法会触发⼀个事件并将其递交给ETW框架。这些⽅法具有⼀个代表事件ID的参数,必须是⼀个介于[0, 65535] 之间的整数,⽽其他参数将作为负载(Payload)附加到事件对象上。
1:public class EventSource : IDisposable
2: {
3:    ...
4:protected void WriteEvent(int eventId);
5:protected void WriteEvent(int eventId, int arg1);
6:protected void WriteEvent(int eventId, long arg1);
抽象类的使用
7:protected void WriteEvent(int eventId, string arg1);
8:protected void WriteEvent(int eventId, byte[] arg1);
9:protected void WriteEvent(int eventId, params object[] args);
10:protected void WriteEvent(int eventId, int arg1, int arg2);
11:protected void WriteEvent(int eventId, int arg1, string arg2);
12:protected void WriteEvent(int eventId, long arg1, long arg2);
13:protected void WriteEvent(int eventId, long arg1, string arg2);
14:protected void WriteEvent(int eventId, long arg1, byte[] arg2);
15:protected void WriteEvent(int eventId, string arg1, int arg2);
16:protected void WriteEvent(int eventId, string arg1, long arg2);
17:protected void WriteEvent(int eventId, string arg1, string arg2);
18:protected void WriteEvent(int eventId, int arg1, int arg2, int arg3);
19:protected void WriteEvent(int eventId, long arg1, long arg2, long arg3);
20:protected void WriteEvent(int eventId, string arg1, int arg2, int arg3);
21:protected void WriteEvent(int eventId, string arg1, string arg2, string arg3);
22: }
由于其中⼀个WriteEvent⽅法具有⼀个通过params关键字定义的参数,并且参数的类型为对象数组,
所以我们可以在编程的时候可以将任意类型和不限数量的对象作为参数,但实际上作为负载的这些参数只能是原⽣类型(Primative Type)、字符串、GUID和枚举这些简单的类型⽽已。其实EventSource类型还具有很多其他成员,但是篇幅所限,我们就不对它详细介绍了。
虽然EventSource并不是⼀个抽象类,但是我们在具体项⽬开发中都不会直接创建⼀个EventSource对象并调⽤它的WriteEvent⽅法触发并向ETW框架⼀个事件。⼀般来说,我们会为⼀组相关的事件定义⼀个继承于EventSource的⼦类,⽐如我们定义了如下⼀个SecurityEventSource的类型来写⼊与安全相关的事件。
1: [EventSource(Name = "MyApp.Security")]
2:public class SecurityEventSource : EventSource
3: {
4:    ...
5:private SecurityEventSource() {}
6:public static readonly SecurityEventSource Instance = new SecurityEventSource();
7: }
⼀个EventSource对应着注册到ETW框架上的某⼀个ETW Provider。每个EventSource对象都具有⼀个可读的名称和⼀个作为唯⼀标识的ID,EventSource的名称和ID实际上就是对应ETW Provider的名称和ID。EventSource的名称和ID都派⽣于⾃定义EventSource的类型,换句话说,同⼀类型的多个EventSource具有相同的名称和ID。如果在应⽤中使⽤到了多个具有相同类型的EventSource对象,这必然引起命名冲突和识别上问题,所以我们总是采⽤Singleton的模式来使⽤⾃定义EventSource,这也体现在上⾯定义的这个SecurityEventSource类型中。
在默认情况下,⾃定义EventSource的类型(不含命名空间)将作为对应ETW Provider的名称。但是在很多情况下,我们希望采⽤⼀种路径话的⽅式对ETW Provider进⾏命名以便我们可以看出多个ETW Provider之间的关系,⽐如我们希望源⾃当前应⽤的所有ETW Provider都采⽤应⽤的名称作为前缀,默认的明明策略就满⾜不了我们的问题了。在这种情况下,我们可以在⾃定义的EventSource类型上标注⼀个EventSourceAttribute特性来指定⼀个不同的名称,上⾯定义的这个SecurityEventSource正是采⽤这种⽅式为对应的ETW Provider起了⼀个与类型不同的名称(“MyApp.Security”)。
标注在⾃定义EventSource类型上的EventSourceAttribute特性除了可以指定名称之外,还可以通过设置Guid属性为EventSource指定⼀个唯⼀标准。如果没有显式指定,EventSource的唯⼀标识将派⽣于
当前的类型,所以我们其实并不推荐显式指定这个Guid属性。从下⾯的代码⽚段还可以看出这个EventSourceAttribute还具有⼀个名为LocalizationResources的属性,我们可以利⽤这个属性指定的资源⽂件实现本地化。
1: [AttributeUsage(AttributeTargets.Class)]
2:public sealed class EventSourceAttribute : Attribute
3: {
4:public string Guid { get; set; }
5:public string Name { get; set; }
6:public string LocalizationResources { get; set; }
7: }
在默认情况下,⾃定义EventSource类型上每⼀个返回类型的void的实例⽅法都将对应⼀个事件,⽽且⽅法名称就是事件名称。我们可以在这些⽅法上应⽤⼀个类型为EventAttribute特性来为⽬标事件定义
⼀些描述信息。如果我们不希望某个⽅法映射为⼀个事件,可以⽅法上标注⼀个NonEventAttribute特性。
1: [AttributeUsage(AttributeTargets.Method)]
2:public sealed class NonEventAttribute : Attribute
3: {}
4:
5: [AttributeUsage(AttributeTargets.Method)]
6:public sealed class EventAttribute : Attribute
7: {
8:public EventAttribute(int eventId);
9:
10:public int                      EventId { get; }
11:public EventLevel              Level { get;  set; }
12:public string                  Message { get; set; }
13:public EventKeywords            Keywords { get; set; }
14:public EventTags                Tags { get; set; }
15:public EventOpcode              Opcode { get; set; }
16:public EventTask                Task { get; set; }
17:public EventChannel            Channel { get; set; }
18:public byte                    Version { get; set; }
19:public EventActivityOptions    ActivityOptions { get;  set; }
20: }
如上⾯的代码⽚段所⽰,我们可以通过设置EventAttribute的属性为⽬标事件提供很多额外的描述。⽐
如我们设置事件的ID(EventId)、等级(Level)和消息(Message)。其中事件等级通过具有如下定义的EventLevel枚举来表⽰,该枚举为我们定义了6个事件等级(由⾼到低依次是LogAlways、Critical、Error、Warning、Informational和Verbose)。⾄于EventAttribute的Message属性,除了指定⼀个完整的描述当前消息的⽂本之外,我们还可以在其中定义{0}、{1}和{2}这样的占位符,执⾏⽬标⽅法指定的参数将依次替换这些占位符。
1:public enum EventLevel
2: {
3:    LogAlways,
4:    Critical,
5:    Error,
6:    Warning,
7:    Informational,
8:    Verbose
9: }
EventAttribute的Keywords和EventTags属性分别代表为事件指定的关键字和标签,它的Opcode和Task属性帮助我们将事件映射为⼀个预定义的操作和任务。这四个属性的返回类型都是枚举,但是实际上在很多情况下我们都是将它们当作⼀个整数在⽤。举个简单的例⼦,表⽰操作代码的EventOpcode枚举中定义了如下11个选项,并且每个选项都具有⼀个具体的值,但是我们可以完全忽略它们。我们的应⽤完全可以按照⾃⼰的标准来定义操作代码,我们可以将整数1映射任何⼀个操作(⽐如“登录”),虽然它在EventOpcode中表⽰“开启(Start)”。
1:public enum EventOpcode
2: {
3:    Info                    = 0,
4:    Start                    = 1,
5:    Stop                    = 2,
6:    DataCollectionStart      = 3,
7:    DataCollectionStop      = 4,
8:    Extension                = 5,
9:    Reply                    = 6,
10:    Resume                  = 7,
11:    Suspend                  = 8,
12:    Send                    = 9,
13:    Receive                  = 240,
14: }
ETW Provider提供的事件还可以写⼊Event Log,EventAttribute的Channel属性⽤于设置Event Log通道(Event Log Channel,可以理解为事件从最初的发布到最终被写⼊⽇志⽂件所经历的传输通道)的类型,具体的通道类型定义在如下⼀个EventChannel枚举中。如果时间具有版本,我们将版本号定义在EventAttribute的Version属性上。
1:public enum EventChannel : byte
2: {
3:    None          = 0,
4:    Admin        = 0x10,
5:    Operational  = 0x11,
6:    Analytic      = 0x12,
7:    Debug        = 0x13
8: }
我们可以利⽤ETW事件来追踪(Track)某个活动(Activity)的开始和终⽌,⽽活动开始与终⽌的追踪⾏为可以通过EventAttribute的ActivityOptions属性来设置。这个属性对应的EventActivityOptions为我们定义了四种活动追踪⾏为,其中None表⽰默认⾏为,⽽Disable则意味着禁⽌追踪。Recursive表⽰可以“递归”的⽅式开启和终⽌活动,⽐如“开始A”-“开始B”-“终⽌B”-“终⽌A”。另⼀个选项Detachable表⽰多个活动之间可以有重叠,⽐如“开始A”-“开始B”-“终⽌A”-“终⽌B”。
1: [Flags]
2:public enum EventActivityOptions
3: {
4:    None            = 0,
5:    Disable        = 2,
6:    Recursive      = 4,
7:    Detachable      = 8,
8: }
接下来我们通过⼀个简单的实例来演⽰如何利⽤⾃定义的EventSource来编写ETW事件提供程序。如下所⽰的是上⾯介绍的这个⽤于跟踪安全相关事件的SecurityEventSource的完整定义,我们在这个类型上应⽤了⼀个EventSourceAttribute特性并名称设置为“MyApp.Security”。SecurityEventSource之中定义了⼀个内嵌的Keywords类,她定义了两个表⽰ETW事件关键字(Authentication和Authorization)的常量,我们⽤它来区分“认证”和“授权”相关的事件。
1: [EventSource(Name ="MyApp.Security")]
2:public class SecurityEventSource : EventSource
3: {
4:public class Keywords
5:    {
6:public const EventKeywords Authentication = (EventKeywords)1;
7:public const EventKeywords Authorization = (EventKeywords)2;
8:    }
9:
10:private SecurityEventSource()
11:    {}
12:
13:public static readonly SecurityEventSource Instance = new SecurityEventSource();
14:
15:    [Event(1,Keywords=Keywords.Authentication, Level = EventLevel.Informational, Message ="⽤户{0}认证成功")]
16:public void SignInSuccess(string userName)
17:    {
18:this.WriteEvent(1, userName);
19:    }
20:
21:    [Event(2, Keywords = Keywords.Authentication, Level = EventLevel.Warning, Message = "⽤户{0}认证失败")]
22:public void SignInFail(string userName)
23:    {
24:this.WriteEvent(2, userName);
25:    }
26:
27:    [Event(3, Keywords = Keywords.Authorization, Level = EventLevel.Warning, Message = "⽤户{0}执⾏未授权操作{1}")]
28:public void DenyAccess(string userName, string operation)
29:    {
30:this.WriteEvent(3, userName, operation);
31:    }
32: }
定义在SecurityEventSource中的三个实例⽅法对应着三个ETW事件,其中⽅法SignInSuccess和SignInFail与认证相关,表⽰认证成功和认证失败。通过标注的EventAttribute特性可以看出,这两个ETW事件采⽤的等级分别为Informational和Warning。我们为这个特性的Message属性设置了⼀个包含占位符的模板,占位符代表作为参数传⼊的⽤户名。另⼀个⽅法DenyAccess与授权相关,表⽰某个⽤户试图执⾏⼀个未被授权的操作,我们依然通过标注的EventAttribute事件设置了ETW事件的关键字、等级和消息。我们最终在Main⽅法中按照如下的⽅式调⽤这三个⽅法。
1: SecurityEventSource.Instance.SignInSuccess("张三");
2: SecurityEventSource.Instance.SignInFail("李四");
3: SecurityEventSource.Instance.DenyAccess("王五", "修改密码");
三、查看⽇志记录
为了查看写⼊的事件,我们在这⾥需要隆重推荐⼀款叫做PerfView的⼯具,这是⼀款免费的性能分析⼯具,下载地址为。接下来我们就来简单介绍⼀下如何利⽤PerfView来启动上⾯这个程序,并收集运⾏时的性能数据。开启PerfView之后,我们选择菜单“Collect”>“Run”打开ETW数据收集窗⼝。
假设程序所在项⽬的根⽬录为“c:\app”,我们在“Current Directory”⽂本框中将这个⽬录设置为当前⽬录,
⽽“Command”⽂本框中则直接设置为“dotnet run”,即直接执⾏dotnet命令来启动我们的程序。为了让PerfView能搜集⾃定义EventSource写⼊的事件,我们需要将SecurityEventSource对应的ETW Provider的名称添加到“Additional Providers”⽂本框中。在这⾥指定的
是“*MyApp.Security”,不要忘了前置的“*”。
设置完成之后我们直接点击“Run Command”,我们的程序将会被启动,由它产⽣的ETW相关的性能数据将会被PerfView收集起来,我们利⽤SecurityEventSource提供的事件信息⾃然也包括其中。收集的性能数据最终会打包⽣成⼀个.etl⽂件,⽂件的路径可以通过“Data File”⽂本框进⾏设置。ETW性能数据的收集以及⽂件的⽣成⼤概需要⼏⼗秒的时间。整个过程完成之
后,PerfView将会按照如下图所⽰的⽅式⾃动打开⽣成的⽂件。
如果要查看收集的ETW事件,我们只需要点击“Events”节点开启事件窗⼝。由于具有很多默认注册的ETW Provider,所以我们会看到左侧会显⽰⾮常多的事件,为了查看由SecurityEventSource提供的事件,我们将EventSource的名称前缀(“MyApp”)设置为过滤条件。我们可以看到⼀共具有四条相关的事件,其中三条是通过定义在SecurityEventSource中对应三个对应⽅法写⼊的。对于收集的每⼀个事件,我们可以得到与之相关的时间名称、执⾏时间、进程、线程、消息以及其他相关信息,具体的信息如下表所⽰。
Event Name Time MSec Process Name Rest
MyApp.Security/SignInSuccess4,818.583dotnet (8660)ThreadID="16,088" FormattedMessage="⽤户"张三"认证成功 " userName="张三"
MyApp.Security/SignInFail4,818.583dotnet (8660)ThreadID="16,088" FormattedMessage="⽤户"李四"认证失败 " userName="李四"
MyApp.Security/DenyAccess4,818.583dotnet (8660)ThreadID="16,088" FormattedMessage="⽤户"王五"执⾏未授权操作"修改密码""
userName="李四"

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。