6.9 KiB
事件日志
EventSource 最初是微软为Windows自身的日志框架 ETW(Event Tracing for Windows)设计的,目前已经没有平台的限制。
这是一种非常高效地记录日志的方式,它提供的强类型的编程方式可以使记录日志变得很“优雅”。 EventSource所谓的强类型编程模式主要体现在两个万面: 其一,可以继承抽象类EventSource定义一个具体的派生米型,并将发送日志事件的操作实现在它的某个方法中; 其二,日志消息的内容可以通过一个自定义的数据类型来承载。
我们可以将下面演示程序中的 DatabaseSource 视为某个数据库访问组件拥有的 EventSource。 将其定义成一个封闭(Sealed)的类型,并利用静态只读字段Instance以单例的形式来使用这个对象。 “SQL命令执行”这一事件定义了对应的OnCommandExecute方法,该方法的两个参数分别表示 DbCommand的类型(CommandType)和文本(存储过程名称或者SOL语句)。 OnCommandExecute 方法最终调用继承的 WriteEvent 方法来发送日志事件。该方法的第一个参数1表示日志事件的ID。
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方法。
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。
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方法中。
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)输出到控制台上。
using App;
using System.Data;
new DatabaseSourceListener();
DatabaseSource.Instance.OnCommandExecute(CommandType.Text,"SELECT ★ FROM T_USER");
EventListener并不需要显式注册,所以只需要按照如上所示的方式在程序运行时创建DatabaseSourceListener对象。 程序运行之后,由DatabaseSourceListener对象捕获的日志事件信息会输出到控制台上。