事件日志 ====== EventSource 最初是微软为Windows自身的日志框架 ETW(Event Tracing for Windows)设计的,目前已经没有平台的限制。 这是一种非常高效地记录日志的方式,它提供的强类型的编程方式可以使记录日志变得很“优雅”。 EventSource所谓的强类型编程模式主要体现在两个万面: 其一,可以继承抽象类EventSource定义一个具体的派生米型,并将发送日志事件的操作实现在它的某个方法中; 其二,日志消息的内容可以通过一个自定义的数据类型来承载。 我们可以将下面演示程序中的 DatabaseSource 视为某个数据库访问组件拥有的 EventSource。 将其定义成一个封闭(Sealed)的类型,并利用静态只读字段Instance以单例的形式来使用这个对象。 “SQL命令执行”这一事件定义了对应的OnCommandExecute方法,该方法的两个参数分别表示 DbCommand的类型(CommandType)和文本(存储过程名称或者SOL语句)。 OnCommandExecute 方法最终调用继承的 WriteEvent 方法来发送日志事件。该方法的第一个参数1表示日志事件的ID。 ```csharp pubiic sealed class DatabaseSource :EventSource public static readonly DatabaseSource Instance = new(); private DatabaseSource() () public void OnCommandExecute(CommandType commandType, string commandText)=> WriteEvent(1, commandType, commandText); ``` 在如下所示的演示程序中,利用Instance 字段得到了对应的DatabaseSource对象,并应的形式调用了它的OnCommandExecute方法。 ```csharp using App; using System.Data; DatabaseSource,Instance,OnCommandExecute(CommandType.Text, "SELECT*FROM T_USER"); ``` 一个EventSource同样具有一个确定的名称。从ETW层面来讲,EventSource 的名称实际就是 ETW Provider 的名称。 自定义的 EventSource 类型默认会以类型名称来命名,所以上面演示程序采用的EventSource名称为“DatabaseSource”。 日志事件需要有一个具有唯一性的整作为ID,如果没有显式设置,则系统会采用从1开始自增的方式为每个日志方法分配一个ID. 由于DatabaseSource中只定义了一个唯一的日志方法OnCommandExecute,所以它被赋予的ID自然是1。 当事件方法在调用 WriteEvent 方法发送日志事件时,需要指定与当前方法匹配的事件ID,这就是该方法在调用WriteEvent方法时将第一个参数设置为1的原因。 由于EventSource具有向ETW日志系统发送日志事件的功能,所以可以利用一些工具来收集这些事件。 作者习惯使用的是一款叫作PerfView的GUI工具,这是一款可以在网上直接下载的性能分析工具,解压缩后就是一个可执行文件。 作者倾向于将该工具所在的目录添加到环境变量PATH中,这样就可以采用命令行的形式进行启动。 我们可以采用Run和Collect这两种模式来启动PerfView:前者利用 PerfView启动和检测某个指定的应用,后者则独立启动PerfView并检测当前运行的所有应用进程。 我们可以将应用所在根目录作为工作目录,并执行“PerfView/onlyproviders=*DatabaseSource run dotnet run"命令来启动PerfView。 为了将自定义的 Trace Provider 纳入 PerfView的检测范围,我们将命令行开关onlyproviders设置为“*DatabaseSource”。执行“dotnet run”命令来启动应用程序PerfView Run,这就意味着演示程序将作为监测程序被启动。 PerfView 会将捕获到的日志打包到当前目录下一个名为 PerfViewData.etl.zip的压缩文件中它左侧的目录结构会以图7-5所示的形式列出该文件。 双击该文件展开其子节点后会看到一个Events节点,PerfView捕捉到的日志就可以通过它来查看。 双击Events节点后,图7-5所示的事件视图将会列出捕获到的所有日志事件。我们可以输入“DatabaseSource”筛选由DatabaseSource发送的事件。 可以看到,DatabaseSource 共发送了两个事件,其中一个就是onCommandExecute。 双击事件视图左侧的“OnCommandExecute”可以查看该事件的详细信息,当调用对应日志方法时提供的数据会包含在Rest列中. ThreadID="17,608commandType="Text"commandText="SELECT *FROM T USER" 虽然系统会根据默认的规则来命名自定义 EventSource的名称和日志输出方法的事件ID,但是对它们进行显式设置是更好的选择。 如下面的代码片段所示,我们在 DatabaseSource 类型上通过标注的 EventSourceAttribute 特性将名称设置为“Artech-Data-SqlClient”。OnCommandExecute方法利用标注的EventAttribute特性将事件ID设置为1。 ```csharp gventSource(Name ="Artech-Data-SqlClient")] oublic sealed class DatabaseSource :EventSource [Event(1)】 public void OnCommandExecute(CommandType commandType, string commandText)-> WriteEvent(1, commandType, commandText); ``` 除了利用PerfView 捕捉 EventSource对象触发的事件,我们还可以通过 EventListener 对象以便达到相同的目的。 定义这个与 DatabaseSource 对应的 DatabaseSourceListener 类型。该类型继承自抽象类EventListener。它的 OnEventSourceCreated 方法能够感知到当前进程中所有EventSource对象的创建。 所以我们重写了该方法对匹配 EventSource实施过滤,并最终通过调用EnableEvents方法订阅由目标EventSource发出的全部或者部分等级的事件。订阅事件的处理实现在重写的OnEventWritten方法中。 ```charp public class DatabaseSourceListener:EventListener protected override void OnEventSourceCreated(EventSource eventSource) if (eventSource.Name =="Artech-Data-SqlClient") EnableEvents (eventSource, EventLevel.LogAlways); ) ) protected override void OnEventWritten(EventWrittenEventArgs eventData) Console.WriteLine($"EventId:(eventData.EventId)"); Console.WriteLine($"EventName:(eventData.EventName)"); Console.WriteLine($"Payload"); var index = 0; if (eventData.PayloadNames != null) ( foreach (var payloadName in eventData.PayloadNames) Console.WriteLine($"\t(payloadName):(eventData.Payload?[index++])"); ``` 在 OnEventSourceCreated方法中调用 EnableEvents方法对由DatabaseSource发出的所有事件(EventLevel.LogAlways)进行了订阅,所以只有DatabaseSource对象发出的日志事件能够被捕捉。在重写的 OnEventWritten方法中,作为唯一参数的 EventWrittenEventArgs对象承载了日志事件的所有信息,并将事件的ID、名称和载荷数据(Payload)输出到控制台上。 ```csharp using App; using System.Data; new DatabaseSourceListener(); DatabaseSource.Instance.OnCommandExecute(CommandType.Text,"SELECT ★ FROM T_USER"); ``` EventListener并不需要显式注册,所以只需要按照如上所示的方式在程序运行时创建DatabaseSourceListener对象。 程序运行之后,由DatabaseSourceListener对象捕获的日志事件信息会输出到控制台上。