diff --git a/LogStudy.EventLog/Assets/Images/1.png b/LogStudy.EventLog/Assets/Images/1.png new file mode 100644 index 0000000..64a28ca Binary files /dev/null and b/LogStudy.EventLog/Assets/Images/1.png differ diff --git a/LogStudy.EventLog/Assets/Images/2.png b/LogStudy.EventLog/Assets/Images/2.png new file mode 100644 index 0000000..c97e88c Binary files /dev/null and b/LogStudy.EventLog/Assets/Images/2.png differ diff --git a/LogStudy.EventLog/Assets/Images/PerView.1.png b/LogStudy.EventLog/Assets/Images/PerView.1.png new file mode 100644 index 0000000..b989998 Binary files /dev/null and b/LogStudy.EventLog/Assets/Images/PerView.1.png differ diff --git a/LogStudy.EventLog/Assets/Images/PerView.2.png b/LogStudy.EventLog/Assets/Images/PerView.2.png new file mode 100644 index 0000000..d9e674a Binary files /dev/null and b/LogStudy.EventLog/Assets/Images/PerView.2.png differ diff --git a/LogStudy.EventLog/Assets/Images/PerView.3.png b/LogStudy.EventLog/Assets/Images/PerView.3.png new file mode 100644 index 0000000..4209947 Binary files /dev/null and b/LogStudy.EventLog/Assets/Images/PerView.3.png differ diff --git a/LogStudy.EventLog/Assets/Images/PerView.4.jpg b/LogStudy.EventLog/Assets/Images/PerView.4.jpg new file mode 100644 index 0000000..c537ad7 Binary files /dev/null and b/LogStudy.EventLog/Assets/Images/PerView.4.jpg differ diff --git a/LogStudy.EventLog/CustomSource.cs b/LogStudy.EventLog/CustomSource.cs deleted file mode 100644 index eca28be..0000000 --- a/LogStudy.EventLog/CustomSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.Tracing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace LogStudy.EventLog -{ - public sealed class CustomSource : EventSource - { - public void OnCommandExecute() - { - - } - } -} diff --git a/LogStudy.EventLog/DatabaseSource.cs b/LogStudy.EventLog/DatabaseSource.cs new file mode 100644 index 0000000..28d6f97 --- /dev/null +++ b/LogStudy.EventLog/DatabaseSource.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.Tracing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace LogStudy.EventLog +{ + /// + /// 单例模式的事件日志源 + /// 即是 ETW Provider + /// + [EventSource(Name ="Andy-Custom-EventSource")] + public sealed class DatabaseSource : EventSource + { + private DatabaseSource() { } + + public static readonly DatabaseSource Instance = new DatabaseSource(); + + public void OnCommandExecute(string logMessage) + { + WriteEvent(1,logMessage); + } + } +} diff --git a/LogStudy.EventLog/LogStudy.EventLog.csproj b/LogStudy.EventLog/LogStudy.EventLog.csproj index 74abf5c..bf552d3 100644 --- a/LogStudy.EventLog/LogStudy.EventLog.csproj +++ b/LogStudy.EventLog/LogStudy.EventLog.csproj @@ -1,4 +1,4 @@ - + Exe @@ -7,4 +7,8 @@ enable + + + + diff --git a/LogStudy.EventLog/Program.cs b/LogStudy.EventLog/Program.cs index 1bc52a6..3cc2250 100644 --- a/LogStudy.EventLog/Program.cs +++ b/LogStudy.EventLog/Program.cs @@ -1 +1,77 @@ -Console.WriteLine("Hello, World!"); +using System; +using System.Collections.Generic; +using System.Text; + +using System.Diagnostics; +using System.Diagnostics.Tracing; + +namespace LogStudy.EventLog +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("======== 事件日志学习 ========"); + + + MinimalEventSource minimalEventSource = MinimalEventSource.Log; + minimalEventSource.Write("MiniEventSource", "-----------------------------------------------------"); + + + var evtSource = new EventSource("AndyEvent"); + evtSource.Write("AndyEvent", "sdfasdfsadfasdfasdfasdf"); + + + DatabaseSource.Instance.OnCommandExecute("我是自定义事件日志!"); + + WriteWindowsOSLog(); + Console.WriteLine("输入回车键,退出程序。"); + Console.ReadLine(); + } + + /// + /// 写入Windows 系统日志 + /// 需要引入 System.Diagnostics.EventLog 包 + /// (需要管理员权限运行) + /// + static void WriteWindowsOSLog() + { + //只有Windows系统,才可以。 + if (OperatingSystem.IsWindows()) + { + var sourceName = "AndySource"; + var logName = "AndyLog"; + + if (!System.Diagnostics.EventLog.SourceExists(sourceName)) + { + System.Diagnostics.EventLog.CreateEventSource(sourceName, logName); + } + + // Create an EventLog instance and assign its source. + System.Diagnostics.EventLog myLog = new System.Diagnostics.EventLog(); + myLog.Source = sourceName; + + // Write an informational entry to the event log. + myLog.WriteEntry($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")}我是执行 .net 程序写入的 Windows 操作系统事件日志!"); + } + else + { + Console.WriteLine("非Windows系统,不写入操作系统事件日志!"); + } + } + } + + [EventSource(Name = "Samples-EventSourceDemos-EventLog")] + public sealed class MinimalEventSource : EventSource + { + private MinimalEventSource() { } + + public static MinimalEventSource Log = new MinimalEventSource(); + + [Event(1, Message = "{0} -> {1}", Channel = EventChannel.Admin)] + public void Load(long baseAddress, string imageName) + { + WriteEvent(1, baseAddress, imageName); + } + } +} diff --git a/LogStudy.EventLog/ReadMe.md b/LogStudy.EventLog/ReadMe.md new file mode 100644 index 0000000..4872bd1 --- /dev/null +++ b/LogStudy.EventLog/ReadMe.md @@ -0,0 +1,99 @@ +事件日志 +====== +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对象捕获的日志事件信息会输出到控制台上。 + + diff --git a/LogStudy.EventLog/收集和查看事件日志.md b/LogStudy.EventLog/收集和查看事件日志.md new file mode 100644 index 0000000..19dc60c --- /dev/null +++ b/LogStudy.EventLog/收集和查看事件日志.md @@ -0,0 +1,193 @@ +收集和查看事件日志 +================== +本文档介绍了不同的工具如何配置在跟踪中收集的事件,然后查看跟踪。[参考官方文档](https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/eventsource-collect-and-view-traces) +> 本档介绍的 EventSource 适用范围为:✔️ .NET Core 3.1 及更高版本 ✔️ .NET Framework 4.5 及更高版本 + +## 示例 +本文档使用以下示例应用为本文档生成事件。 编译包含以下代码的 .NET 控制台应用程序: +```csharp +using System.Diagnostics.Tracing; + +namespace EventSourceDemo +{ + public static class Program + { + public static void Main(string[] args) + { + DemoEventSource.Log.AppStarted("Hello World!", 12); + DemoEventSource.Log.DebugMessage("Got here"); + DemoEventSource.Log.DebugMessage("finishing startup"); + DemoEventSource.Log.RequestStart(3); + DemoEventSource.Log.RequestStop(3); + } + } + + [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); + [Event(4, Keywords = Keywords.Startup, Level = EventLevel.Verbose)] + public void DebugMessage(string message) => WriteEvent(4, message); + + + public class Keywords + { + public const EventKeywords Startup = (EventKeywords)0x0001; + public const EventKeywords Requests = (EventKeywords)0x0002; + } + } +} + +``` + +## 配置要收集的事件 +大多数事件收集工具都使用这些配置选项来确定跟踪中应包括哪些事件: + ++ 提供程序名称 - 这是包含一个或多个 EventSource 名称的列表。 只有在此列表中的 EventSource 上定义的事件才有资格包含在内。 若要从前面的示例应用中的 DemoEventSource 类收集事件,需要在提供程序名称列表中包括 EventSource 名称“Demo”。 ++ 事件详细级别 - 对于每个提供程序,可定义一个详细级别,详细程度高于该级别的事件将从跟踪中排除。 如果指定上述示例应用中的“Demo”提供程序应在信息详细级别进行收集,则将排除 DebugMessage 事件,因为它具有更高的级别。 指定 EventLevel LogAlways(0) 是一种特殊情况,指示应包含任何详细级别的事件。 ++ 事件关键字 - 对于每个提供程序,可定义一组关键字,只有至少标记了其中一个关键字的事件才包含在内。 在上述示例应用中,如果指定了 Startup 关键字,则仅包含 AppStarted 和 DebugMessage 事件。 如果未指定任何关键字,则这是一个特殊情况,表示应包含具有任何关键字的事件。 + +## 用于描述提供程序配置的约定 +尽管每个工具都确定其自己的用户界面来设置跟踪配置,但在将配置指定为文本字符串时,很多工具都使用一种常见约定。 +提供程序列表被指定为一个分号分隔列表,列表中的每个提供程序元素由名称、关键字和级别组成,用冒号分隔。 +例如,“Demo:3:5”使用关键字位掩码 3(Startup 位和 Requests 位)和 EventLevel 5(即 Verbose)标识名为“Demo”的 EventSource。 +许多工具还允许你忽略级别和关键字(如果不需要级别或关键字筛选)。 +例如,“Demo::5”仅执行基于级别的筛选,“Demo:3”仅执行基于关键字的筛选,而“Demo”不执行关键字或级别筛选。 + +## 收集与查看工具 + +### Visual Studio +`Visual Studio 探查器` 支持收集和查看跟踪。 它还可查看其他工具(如 dotnet-trace)提前收集的跟踪。 + ++ 收集跟踪 + + Visual Studio 的大多数分析工具都使用预定义的提供特定用途(例如分析 CPU 使用率或分配)的事件集。 若要使用自定义事件收集跟踪,请使用事件查看器工具。 + 1. 要在 Visual Studio 中打开性能探查器,请选择 Alt+F2 或者 菜单:调试 --> 性能探查器 + 2. 选中“事件查看器”复选框 ![设置](Assets/Images/1.png) + 3. 选择事件查看器右侧的小齿轮图标可打开配置窗口 + ![设置](Assets/Images/2.png) + 4. 选择“确定”以确认配置设置 + 5. 选择“开始”以开始运行应用并收集日志 + 6. 选择“停止收集”或退出应用以停止收集日志并显示收集的数据 + + ++ 查看跟踪 + + Visual Studio 可查看其自身收集的跟踪,也可查看在其他工具中收集的跟踪。 + 若要查看来自其他工具的跟踪,请使用“文件”>“打开”,并在文件选取器中选择跟踪文件。 + Visual Studio 探查器支持 .etl 文件(ETW 的标准格式)、.nettrace 文件(EventPipe 的标准格式)和 .diagsession 文件(Visual Studio的标准格式)。 + 若要了解如何在 Visual Studio 中使用跟踪文件,[请参阅 Visual Studio 文档](https://learn.microsoft.com/zh-cn/visualstudio/profiling/events-viewer?view=vs-2022#understand-your-data)。 + +### PerfView +PerfView 是 .NET 团队创建的一款性能工具,可收集和查看 ETW 跟踪。 +它还可查看其他工具以各种格式收集的跟踪文件。 +在本文档中,你将收集演示应用的 ETW 跟踪,然后在 PerfView 的事件查看器中检查收集的事件。 + ++ 收集跟踪 + 1. 从[发布页](https://github.com/Microsoft/perfview/releases)下载 PerfView。 本教程使用 [PerfView 版本 2.0.76](https://github.com/microsoft/perfview/releases/tag/P2.0.76) 完成,但任何最新版本都应正常工作 + 2. 使用管理员权限启动 PerfView.exe + > 备注: ETW 跟踪集合始终需要管理员权限,但如果仅使用 PerfView 查看预先存在的跟踪,则不需要特殊权限。 + + ![设置](Assets/Images/PerView.1.png) + + 3. 从“收集”菜单中,选择“运行”。 这将打开一个新的对话框,你将在其中输入演示应用的路径 + ![设置](Assets/Images/PerView.2.png) + 4. 若要配置收集的事件,请展开对话框底部的“高级选项”。 在“其他提供程序”文本框中,使用前面所述的常规文本格式输入提供程序。 在本例中,你将输入“Demo:1:4”,这意味着关键字位 1(Startup 事件)和详细程度 4 (Informational) + ![设置](Assets/Images/PerView.3.png) + 5. 若要启动应用并开始收集跟踪,请选择“运行命令”按钮。 当应用退出时,跟踪 PerfViewData.etl 保存在当前目录中 + + 6. 或者菜单:Collent -> Collect 配置运行之后,启动程序,停止运行后,进行查看分析。 + + > 第4步配置收集的事件,必不可少。不配置或配置错误将收集不到数据,多外时,用英文逗号分隔 + ++ 查看跟踪 + 1. 在左上角的主窗口下拉文本框中,选择包含跟踪文件的目录。 然后,双击下面的树视图中的跟踪文件 + ![设置](Assets/Images/PerView.4.jpg) + 2. 若要调出事件查看器,请双击跟踪文件下方树视图中出现的“事件”项.可按需进行过滤 + 3. 跟踪中的所有事件类型都显示在左侧列表中。 双击事件类型(如 Demo\AppStarted),在右侧表中显示该类型的所有事件 + +### dotnet-trace +[dotnet-trace](https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-trace) 是一种跨平台命令行工具,可使用 [EventPipe](https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/eventpipe) 跟踪从 .NET Core 应用中收集跟踪。 +它不支持查看跟踪数据,但它收集的跟踪可由其他工具(如 PerfView 或 Visual Studio)查看。 +dotnet-trace 还支持将其默认的 .nettrace 格式跟踪转换为其他格式,例如 Chromium 或 [Speedscope(https://www.speedscope.app/)]。 + ++ 收集跟踪 + 1. 下载并安装 [dotnet-trace](https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-trace#install) + 2. 在命令行中,运行 [dotnet-trace collect](https://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-trace#dotnet-trace-collecthttps://learn.microsoft.com/zh-cn/dotnet/core/diagnostics/dotnet-trace#dotnet-trace-collect) 命令 + ```.NET CLI + E:\temp\EventSourceDemo\bin\Debug\net6.0>dotnet-trace collect --providers Demo:1:4 -- EventSourceDemo.exe + ``` + 结果: + ```.NET CLI + E:\temp\EventSourceDemo\bin\Debug\net6.0> dotnet-trace collect --providers Demo:1:4 -- EventSourceDemo.exe + + Provider Name Keywords Level Enabled By + Demo 0x0000000000000001 Informational(4) --providers + + Launching: EventSourceDemo.exe + Process : E:\temp\EventSourceDemo\bin\Debug\net6.0\EventSourceDemo.exe + Output File : E:\temp\EventSourceDemo\bin\Debug\net6.0\EventSourceDemo.exe_20220317_021512.nettrace + + [00:00:00:00] Recording trace 0.00 (B) + Press or to exit... + + Trace completed. + ``` + dotnet-trace 使用 常规文本格式来描述 --providers 参数中的提供程序配置。 有关如何使用 dotnet-trace 获取跟踪的更多选项,请参阅 dotnet-trace 文档。 + + 3. 直接或转换后,用其它工具查看 + +### EventListener +System.Diagnostics.Tracing.EventListener 是一个 .NET API,可在进程中使用来接收由某个 System.Diagnostics.Tracing.EventSource 生成的事件的回调。 +此 API 可用于创建自定义日志记录工具,或者用于在不序列化事件的情况下分析内存中的事件。 + +若要使用 EventListener,请声明一个派生自 EventListener 的类型,调用 EnableEvents 来从任何感兴趣的 EventSource 订阅事件,并重写每当有新事件可用时都将调用的 OnEventWritten。 +通常,重写 OnEventSourceCreated 来发现存在哪些 EventSource 对象很有帮助,但这不是必需的。 +下面是一个示例 EventListener 实现,它在收到消息时向控制台输出内容: + +1. 将此代码添加到演示应用 + ```csharp + class ConsoleWriterEventListener : EventListener + { + protected override void OnEventSourceCreated(EventSource eventSource) + { + if(eventSource.Name == "Demo") + { + EnableEvents(eventSource, EventLevel.Informational); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + Console.WriteLine(eventData.TimeStamp + " " + eventData.EventName); + } + } + ``` +1. 修改 Main 方法以创建新侦听器的实例 + ```csharp + public static void Main(string[] args) + { + ConsoleWriterEventListener listener = new ConsoleWriterEventListener(); + + DemoEventSource.Log.AppStarted("Hello World!", 12); + DemoEventSource.Log.DebugMessage("Got here"); + DemoEventSource.Log.DebugMessage("finishing startup"); + DemoEventSource.Log.RequestStart(3); + DemoEventSource.Log.RequestStop(3); + } + ``` +1.构建并运行应用。 以前,它没有输出,但现在它会将事件写入控制台 + ```text + 3/24/2022 9:23:35 AM AppStarted + 3/24/2022 9:23:35 AM RequestStart + 3/24/2022 9:23:35 AM RequestStop + + ``` +